代码如下
class Base
{
public :
Base()
{
printf("Base");
}
~Base()
{
printf("~Base");
}
void SetNumber(int number)
{
m_nBase=number;
}
int GetNumber()
{
return m_nBase;
}
private:
int m_nBase;
};
class Derived:public Base
{
public:
void ShowNumber(int number)
{
SetNumber(number);
m_nDerived=number+1;
printf("%d",GetNumber());
printf("%d",m_nDerived);
}
int m_nDerived;
};
int main()
{
Derived deriv;
deriv.ShowNumber(5);
return 0;
}
通过反汇编初步结果为
首先我们先看主函数,
00401070 push ebp
00401071 mov ebp,esp
00401073 push 0FFh
00401075 push offset __ehhandler$_main (0041f279)
0040107A mov eax,fs:[00000000]
00401080 push eax
00401081 mov dword ptr fs:[0],esp
00401088 sub esp,4Ch
0040108B push ebx
0040108C push esi
0040108D push edi
0040108E lea edi,[ebp-58h]
00401091 mov ecx,13h
00401096 mov eax,0CCCCCCCCh
0040109B rep stos dword ptr [edi]
这里的栈初始化有点不一样,规定基址ebp后,却压入了三个DWORD,也就是FFh, offset __ehhandler$_main (0041f279),还有eax,这点要注意。
Derived deriv;
0040109D lea ecx,[ebp-14h]
004010A0 call @ILT+45(Derived::Derived) (00401032)
004010A5 mov dword ptr [ebp-4],0
往下走,就是子类的构造函数,00401032是个跳转语句,跳转到了
Derived::Derived:
00401220 push ebp
00401221 mov ebp,esp
00401223 sub esp,44h
00401226 push ebx
00401227 push esi
00401228 push edi
00401229 push ecx
0040122A lea edi,[ebp-44h]
0040122D mov ecx,11h
00401232 mov eax,0CCCCCCCCh
00401237 rep stos dword ptr [edi]
00401239 pop ecx
0040123A mov dword ptr [ebp-4],ecx
0040123D mov ecx,dword ptr [ebp-4]
00401240 call @ILT+15(Base::Base) (00401014)
00401245 mov eax,dword ptr [ebp-4]
00401248 pop edi
00401249 pop esi
0040124A pop ebx
0040124B add esp,44h
0040124E cmp ebp,esp
00401250 call __chkesp (00408810)
00401255 mov esp,ebp
00401257 pop ebp
这里面我们要插入一句,C++里面总是说什么this指针,指向的是本对象的地址,这样讲有点云里雾里,不如我们用汇编来分析
00401088 sub esp,4Ch
这一句代码是主函数的,意思是申请一个大小为0x4C的栈空间,栈空间存放的是子类对象。既然有了子类对象,如何初始化呢
0040109D lea ecx,[ebp-14h]
004010A0 call @ILT+45(Derived::Derived) (00401032)
这里是讲ebp-14处的地址,其实也就是ebp-14h这个值,付给ecx,然后调用子类构造函数,为什么是ebp-14h呢,因为前面压入了三个双字变量,这里子类对象大小我们知道是父类大小+新增成员变量大小,为8,所以子类的对象的起始地址为ebp-14h(这里的14h,换成十进制,就是20)
图如下
44h的空间(填充了0xCC) |
子类对象首地址 (ebp-14h) this (这里其实存放的是从父类继承的m_nBase) |
子类对象首地址+4 (ebp-10h) this+4 (这里存储的是子类新增的m_nDerived)红色的两个框,8个字节,就是子类对象 |
压入的第三个双字(ebp-C) |
压入的第二个双字(ebp-8) |
压入的第一个双字(ebp-4) |
ebp |
本图从下往上,是栈的增长方向。
我们看主函数这一句,0040109D lea ecx,[ebp-14h],意思是把地址为ebp-14h处的单元的地址付给ecx,也就是ebp-14h付给ecx,ebp-14h是子类的首地址,连接往下四个字节,一共八个字节,是子类的总大小。
然后进入 004010A0 call @ILT+45(Derived::Derived) (00401032)
在子类的构造函数中我们可以看到,
00401239 pop ecx
0040123A mov dword ptr [ebp-4],ecx
0040123D mov ecx,dword ptr [ebp-4]
00401240 call @ILT+15(Base::Base) (00401014)
将传入的子类的首地址用ecx传入,在弹出,在保存在ebp-4h处,再传给ecx,并调用基类的构造函数。
我们这里的ecx,实际上存储的就是传说中的this指针了,也就是子类对象的首地址,通过传递这个地址,我们可以方便的对子类进行操作,比如对其成员变量进行赋值。
这一点算是分析完毕了,跳回去,我们接着分析子类构造函数。
看这句
00401240 call @ILT+15(Base::Base) (00401014)
这个意思是调用父类的构造函数,明显,此时ecx存储的还是子类传过来的地址
Base()
00401270 push ebp
00401271 mov ebp,esp
00401273 sub esp,44h
00401276 push ebx
00401277 push esi
00401278 push edi
00401279 push ecx
0040127A lea edi,[ebp-44h]
0040127D mov ecx,11h
00401282 mov eax,0CCCCCCCCh
00401287 rep stos dword ptr [edi]
00401289 pop ecx
0040128A mov dword ptr [ebp-4],ecx
19: {
20: printf("Base");
0040128D push offset string "Base" (00431020)
00401292 call printf (00408940)
00401297 add esp,4
21: }
这个就是我们在构造函数中调用的父类构造函数,可以看到这里面打印了一个字符串,并没有进行赋值什么的,如果进行赋值呢?比如Base构造函数里把从父类继承过来的m_nBase=2,这样应该会变为,mov [ecx],2
有兴趣的可以修改下源程序,然后看下这里的反汇编代码。
到这里,子类的构造函数就分析完了,我们接着分析
deriv.ShowNumber(5);
void ShowNumber(int number)
41: {
00401110 push ebp
00401111 mov ebp,esp
00401113 sub esp,44h
00401116 push ebx
00401117 push esi
00401118 push edi
00401119 push ecx
0040111A lea edi,[ebp-44h]
0040111D mov ecx,11h
00401122 mov eax,0CCCCCCCCh
00401127 rep stos dword ptr [edi]
00401129 pop ecx
0040112A mov dword ptr [ebp-4],ecx
42: SetNumber(number);
0040112D mov eax,dword ptr [ebp+8]
00401130 push eax
00401131 mov ecx,dword ptr [ebp-4]
00401134 call @ILT+10(Base::SetNumber) (0040100f)
43: m_nDerived=number+1;
00401139 mov ecx,dword ptr [ebp+8]
0040113C add ecx,1
0040113F mov edx,dword ptr [ebp-4]
00401142 mov dword ptr [edx+4],ecx
44: printf("%d",GetNumber());
00401145 mov ecx,dword ptr [ebp-4]
00401148 call @ILT+35(Base::GetNumber) (00401028)
0040114D push eax
0040114E push offset string "%d" (0043101c)
00401153 call printf (00408940)
00401158 add esp,8
45: printf("%d",m_nDerived);
0040115B mov eax,dword ptr [ebp-4]
0040115E mov ecx,dword ptr [eax+4]
00401161 push ecx
00401162 push offset string "%d" (0043101c)
00401167 call printf (00408940)
0040116C add esp,8
46:
47: }
可以看到里面又调用了继承自父类的Setnumber函数,这里面还是用的ebp-14h进行传递,不多讲,SetNumber执行完后,ebp-14h这个地方,就变成了5,我们可以知道,这个双字,实际上是继承的父类的成员变量。
往下走,就是
m_nDerived=number+1;
00401139 mov ecx,dword ptr [ebp+8]
0040113C add ecx,1
0040113F mov edx,dword ptr [ebp-4]
00401142 mov dword ptr [edx+4],ecx
这里可以看到,首先将形参自增1,变成6,然后
mov edx,dword ptr [ebp-4]
将首地址付给edx寄存器,然后将6,付给了edx+4处的内存,edx+4处的内存是哪里?我们看图,就知道,是首地址往下四个字节,也就是ebp-10h,也就是this+4处,也就是子类新增的成员变量的位置。
后面就基本结束了。
反汇编确实是利器,剖开C++晦涩的语法糖,直窥本源。