您的位置:首页 > 编程语言 > C语言/C++

浅析C++继承的内存布局

2015-11-04 12:56 567 查看
代码如下

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++晦涩的语法糖,直窥本源。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: