C++逆向分析——this指针
this指针
概述
C++是对C的拓展,C原有的语法C++都支持,并在此基础上拓展了一些语法:封装、继承、多态、模板等等。C++拓展新的语法是为了让使用更加方便、高效,这样就需要编译器多做了很多事情,接下来我们就需要一一学习这些概念。
封装
之前我们学习过结构体这个概念,那么结构体可以做参数传递吗?我们来看一下如下代码:
struct Student {
int
a;
int
b;
int
c;
int
d;
};
int
Plus(Student s) {
return
s.a + s.b + s.c + s.d;
}
void
main() {
Student s = {
1
,
2
,
3
,
4
};
int
res = Plus(s);
return
;
}
上面这段代码是定义一个结构体,然后将该结构体传入Plus函数(将结构体成员相加返回),那么问题来了,结构体它是否跟数组一样,传递的是指针呢?来看一下反汇编代码:
可以很清晰的看见,结构体作为参数传递时栈顶(ESP)提升了0x10(16个字节,也就是结构体的四个成员【int】的宽度),而后将ESP的值给了EAX,再通过EAX(ESP)将结构体的成员传入函数,结构体成员从左到右依次从栈顶向下复制进入堆栈。
也就是说当我们将结构体作为参数传递时与我们传整数什么的是没有本质区别的,唯一的区别就是传递结构体时不是使用的push来传递的,而是一次性的提升堆栈,然后mov赋值。
虽然我们可以使用结构体进行传参,但是这也存在一个问题,就是当我们使用结构体传参时,假设结构体有40个成员,那么就存在着大量的内存被复制,这样效率很低,是不推荐使用的。
那如果非要这样使用该怎么办呢?我们可以使用指针传递的方式来,修改一下代码:
struct Student {
int
a;
int
b;
int
c;
int
d;
};
int
Plus(Student* p) {
return
p->a + p->b + p->c + p->d;
}
void
main() {
Student s = {
1
,
2
,
3
,
4
};
int
res = Plus(&s);
return
;
}
这样我们就可以使用指针的方式来避免内存的重复使用,效率更高。
可能很多人看到这就很疑惑了,那这跟C++有什么关系呢?我们之前说过C++和C的本质区别,就是编译器替代我们做了很多事情;别着急,慢慢来看。
我们使用指针优化过的代码,实际上还是存在小缺陷的,当结构体成员很多的时候,我们在Plus函数体内就要用指针的调用方式,一堆成员相加...
那么是否可以让我们调用更加简单,更加方便呢?如下代码就可以:
struct Student {
int
a;
int
b;
int
c;
int
d;
int
Plus() {
return
a + b + c + d;
}
};
void
main() {
Student s = {
1
,
2
,
3
,
4
};
int
res = s.Plus();
return
;
}
将函数放在结构体内,就不需要我们再去写传参、再去使用指针的调用方式了,因为这些工作编译器帮我们完成了,而本质上这与指针调用没有区别:
而这种写法就是C++的概念:封装;也就是说将函数写在结构体内的形式就称之为封装,其带来的好处就是我们可以更加方便的使用结构体的成员。
讲到了封装,我们就要知道另外两个概念:
-
类:带有函数的结构体,称为类;
-
成员函数:结构体里的函数,称为成员函数
-
函数本身不占用结构体的空间(函数不属于结构体)
-
调用成员函数的方法与调用结构体成员的语法是一样的 → 结构体名称.函数名()
-
this指针
之前我们学过了封装,如下代码:
struct Student {
int
a;
int
b;
int
c;
int
d;
int
Plus() {
return
a + b + c + d;
}
};
void
main() {
Student s = {
1
,
2
,
3
,
4
};
int
res = s.Plus();
return
;
}
其对应的反汇编代码如下:
可以看见我们使用s.Plus()的时候,传递的参数是一个指针,这个指针就是当前结构体的地址,这个指针就是this指针。(通常情况下编译器会使用ecx来传递当前结构体的指针)
我自己实验如下:
#include "StdAfx.h" struct Student { int a; int b; int c; int d; int Plus() { return a + b + c + d; } }; void main() { Student s = {1, 2, 3, 4}; int res = s.Plus(); return; }
那么当我们将Plus函数修改成无返回值,不调用结构体成员后,这个指针还会传递过来么?
struct Student {
int
a;
int
b;
int
c;
int
d;
void
Plus() {
}
};
void
main() {
Student s = {
1
,
2
,
3
,
4
};
s.Plus();
return
;
}
我们看下反汇编代码,发现指针依然会传递过来:
那也就是说this指针是编译器默认传入的,通常会通过ecx进行参数的传递,不管你用还是不用,它都存在着。
既然this指针会作为参数传递,我们是否也可以直接使用这个指针呢?答案是可以的:
struct Student {
int
a;
int
b;
void
Init(
int
a,
int
b) {
this
->a = a;
this
->b = b;
}
};
我们在结构体的成员函数内使用this这个关键词就可以调用了,如上代码所示。
那么this指针有什么作用呢?我们可以看下如下代码:
struct Student {
int
a;
int
b;
void
Init(
int
a,
int
b) {
a = a;
b = b;
}
};
void
main() {
Student s;
s.Init(
1
,
2
);
return
;
}
这段代码我们要实现的就是,使用成员函数初始化成员的值,但是实际运行却不符合我们的预期:
跟进反汇编代码发现,这里就是将传入的参数赋值给了参数本身,并没有改变成员的值,这是因为编译器根本不知道你这里的a到底是谁,所以我们就需要借助this指针来实现:
#include <stdio.h>
struct Student {
int
a;
int
b;
void
Init(
int
a,
int
b) {
this
->a = a;
this
->b = b;
}
void
Print() {
printf(
"%d %d"
,
this
->a,
this
->b);
}
};
void
main() {
Student s;
s.Init(
1
,
2
);
s.Print();
return
;
}
为了方便,添加一个成员函数,用于打印输出成员的值:
可以看见,这里成功进行初始化了。
总结:
-
this指针是编译器默认传入的,通常会使用ecx进行参数的传递
-
成员函数都有this指针,无论是否使用
-
this指针不能做++ --等运算,也不可以被重新赋值
-
this指针不占用结构体的宽度
this指针和函数都不占用struct的空间,我们验证下:
#include <cstdio> struct A { char* hello() { return "hi"; } }; int main() { A a; printf("empty struct size=%d\n", sizeof(a)); }
输出为1。
所以可以知道,没有任何成员变量的struct大小为1.
恶意代码分析实战里20.1.1章节有一个例子:
#include "StdAfx.h" class S { public: int x; void hello() { if (x == 10) printf("x is 10\n"); } }; void main() { S s1; s1.x = 9; s1.hello(); S s2; s2.x = 10; s2.hello(); return; }
我贴下其汇编结果(vc6下的)
13: void main() { 0040D400 push ebp 0040D401 mov ebp,esp 0040D403 sub esp,48h 0040D406 push ebx 0040D407 push esi 0040D408 push edi 0040D409 lea edi,[ebp-48h] 0040D40C mov ecx,12h 0040D411 mov eax,0CCCCCCCCh 0040D416 rep stos dword ptr [edi] 14: S s1; 15: s1.x = 9; 0040D418 mov dword ptr [ebp-4],9 16: s1.hello(); 0040D41F lea ecx,[ebp-4] 0040D422 call @ILT+10(S::hello) (0040100f) 17: 18: S s2; 19: s2.x = 10; 0040D427 mov dword ptr [ebp-8],0Ah 20: s2.hello(); 0040D42E lea ecx,[ebp-8] 0040D431 call @ILT+10(S::hello) (0040100f) 21: return; 22: }
8: void hello() { 0040D4D0 push ebp 0040D4D1 mov ebp,esp 0040D4D3 sub esp,44h 0040D4D6 push ebx 0040D4D7 push esi 0040D4D8 push edi 0040D4D9 push ecx 0040D4DA lea edi,[ebp-44h] 0040D4DD mov ecx,11h 0040D4E2 mov eax,0CCCCCCCCh 0040D4E7 rep stos dword ptr [edi] 0040D4E9 pop ecx 0040D4EA mov dword ptr [ebp-4],ecx 9: if (x == 10) printf("x is 10\n"); 0040D4ED mov eax,dword ptr [ebp-4] 0040D4F0 cmp dword ptr [eax],0Ah 0040D4F3 jne S::hello+32h (0040d502) 0040D4F5 push offset string "x is 10\n" (0042201c) 0040D4FA call printf (0040d740) 0040D4FF add esp,4 10: } 0040D502 pop edi 0040D503 pop esi 0040D504 pop ebx 0040D505 add esp,44h 0040D508 cmp ebp,esp 0040D50A call __chkesp (0040d490) 0040D50F mov esp,ebp 0040D511 pop ebp 0040D512 ret
注意,和书里的汇编代码还不一样。
奇怪,为啥我的vc6出来的结果和书里不一样。。。
https://www.jaiminton.com/Tutorials/PracticalMalwareAnalysis/# 也说使用的vc6编译。