首页
论坛
课程
招聘
[原创]从反汇编的角度学C/C++之多重继承与多继承
2021-10-3 17:15 3405

[原创]从反汇编的角度学C/C++之多重继承与多继承

2021-10-3 17:15
3405

一.多重继承

    在类的继承关系中,我们可以进行多重继承,那么它和单继承相比数据排布等等有什么不同,通过如下定义的代码来观察多重继承的特点。

class Base1
{
public:
	Base1()
	{
		this->x = 1;
	}
	virtual void fun1()
	{
		printf("Base1 fun 1\n");
	}
	virtual void fun2()
	{
		printf("Base1 fun 2\n");
	}
	virtual void fun3()
	{
		printf("Base1 fun 3\n");
	}
private:
	int x;
};

class Base2 : public Base1
{
public:
	Base2()
	{
		this->y = 2;
	}
	virtual void fun1()
	{
		printf("Base2 fun1\n");
	}
	virtual void fun2()
	{
		printf("Base2 fun2\n");
	}
private:
	int y;
};

class Sub : public Base2
{
public:
	Sub()
	{
		this->z = 3;
	}
	virtual void fun1()
	{
		printf("Sub fun 1\n");
	}
	virtual void fun4()
	{
		printf("Sub fun 4\n");
	}
private:
	int z;
};

    首先我们看看构造函数的反汇编结果

	Sub()
004310C0  push        ebp  
004310C1  mov         ebp,esp  
004310C3  sub         esp,0CCh  
004310C9  push        ebx  
004310CA  push        esi  
004310CB  push        edi  
004310CC  push        ecx  
004310CD  lea         edi,[ebp-0CCh]  
004310D3  mov         ecx,33h  
004310D8  mov         eax,0CCCCCCCCh  
004310DD  rep stos    dword ptr es:[edi]  
004310DF  pop         ecx  
004310E0  mov         dword ptr [this],ecx  
004310E3  mov         ecx,offset _D3472036_test@cpp (04C200Bh)  
004310E8  call        __CheckForDebuggerJustMyCode (04316C0h)  
004310ED  mov         ecx,dword ptr [this]          //把类变量地址赋给ecx
004310F0  call        Base2::Base2 (0431060h)       //调用父类构造函数
004310F5  mov         eax,dword ptr [this]  
004310F8  mov         dword ptr [eax],offset Sub::`vftable' (04A321Ch) //将虚函数吧表赋给类变量地址偏移0处  
	{
		this->z = 3;
004310FE  mov         eax,dword ptr [this]  
00431101  mov         dword ptr [eax+0Ch],3     //为z变量赋值,此时它的偏移是0xC,也就是12的位置 
	}
00431108  mov         eax,dword ptr [this]  
0043110B  pop         edi  
0043110C  pop         esi  
0043110D  pop         ebx  
0043110E  add         esp,0CCh  
00431114  cmp         ebp,esp  
00431116  call        _RTC_CheckEsp (0431680h)  
0043111B  mov         esp,ebp  
0043111D  pop         ebp  
0043111E  ret

    可以看到,在虚函数中,首先调用父类Base2的构造函数,随后初始化虚表指针然后在进行类变量赋值,接着看下父类构造函数

	Base2()
00431060  push        ebp  
00431061  mov         ebp,esp  
00431063  sub         esp,0CCh  
00431069  push        ebx  
0043106A  push        esi  
0043106B  push        edi  
0043106C  push        ecx  
0043106D  lea         edi,[ebp-0CCh]  
00431073  mov         ecx,33h  
00431078  mov         eax,0CCCCCCCCh  
0043107D  rep stos    dword ptr es:[edi]  
0043107F  pop         ecx  
00431080  mov         dword ptr [this],ecx  
00431083  mov         ecx,offset _D3472036_test@cpp (04C200Bh)  
00431088  call        __CheckForDebuggerJustMyCode (04316C0h)  
0043108D  mov         ecx,dword ptr [this]                     
00431090  call        Base1::Base1 (0431000h)                               //调用父类构造函数
00431095  mov         eax,dword ptr [this]  
00431098  mov         dword ptr [eax],offset Base2::`vftable' (04A31F4h)    //将虚表地址赋值给虚表指针
	{
		this->y = 2;
0043109E  mov         eax,dword ptr [this]  
004310A1  mov         dword ptr [eax+8],2                  //初始化类变量y,此时这个地址是类地址偏移为8的地方
	}
004310A8  mov         eax,dword ptr [this]  
004310AB  pop         edi  
004310AC  pop         esi  
004310AD  pop         ebx  
004310AE  add         esp,0CCh  
004310B4  cmp         ebp,esp  
004310B6  call        _RTC_CheckEsp (0431680h)  
004310BB  mov         esp,ebp  
004310BD  pop         ebp  
004310BE  ret

    接着继续看祖父类Base1的构造函数

Base1()
00431000  push        ebp  
00431001  mov         ebp,esp  
00431003  sub         esp,0CCh  
00431009  push        ebx  
0043100A  push        esi  
0043100B  push        edi  
0043100C  push        ecx  
0043100D  lea         edi,[ebp-0CCh]  
00431013  mov         ecx,33h  
00431018  mov         eax,0CCCCCCCCh  
0043101D  rep stos    dword ptr es:[edi]  
0043101F  pop         ecx  
00431020  mov         dword ptr [this],ecx  
00431023  mov         ecx,offset _D3472036_test@cpp (04C200Bh)  
00431028  call        __CheckForDebuggerJustMyCode (04316C0h)  
0043102D  mov         eax,dword ptr [this]  
00431030  mov         dword ptr [eax],offset Base1::`vftable' (04A31B4h)      //赋值虚表指针
	{
		this->x = 1;
00431036  mov         eax,dword ptr [this]                  
00431039  mov         dword ptr [eax+4],1          //为x赋值,此时x的偏移是4
	}
00431040  mov         eax,dword ptr [this]  
00431043  pop         edi  
00431044  pop         esi  
00431045  pop         ebx  
00431046  add         esp,0CCh  
0043104C  cmp         ebp,esp  
0043104E  call        _RTC_CheckEsp (0431680h)  
00431053  mov         esp,ebp  
00431055  pop         ebp  
00431056  ret

    由上可以得出

  1. 多重继承的构造函数执行流程是:祖父类构造函数(Base1)->父类构造函数(Base2)->子类构造函数(Sub)。在每个构造函数开始之前都会把自己的虚函数表赋值给虚表指针。

  2. 类变量地址中的数据是按照:虚表指针->按照祖父类(Base1)的类变量->父类类变量(Base2)->子类类变量(Sub)依次排布


    此时查看类地址在内存中存储的数据可以看到如下的内容

       

    接着我们看一下虚函数表在多重继承中有什么特点,IDA查看结果如下

      

    为了方便展示,同样根据函数后重命名,结果如下

      

    可以看到,在多重继承中,子类中的虚函数表最终存储的虚函数地址是低一层子类重载的地址。比如fun1被Sub重载了,那么它就会报错Sub_fun1,而fun2没被Sub重载,但是被Base2重载,所以它不是保存Base1_fun2而是Base2_fun2,而fun3没被Base2和Sub1重载,那么保存的就是Base1_fun3。

二.多继承

    C++和其他面向对象不同的一大特点就是可以多继承,修改定义如下

class Base1
{
public:
	Base1()
	{
		this->x = 1;
	}
	virtual void fun1()
	{
		printf("Base1 fun 1\n");
	}
	virtual void fun2()
	{
		printf("Base1 fun 2\n");
	}
	virtual void fun3()
	{
		printf("Base1 fun 3\n");
	}
private:
	int x;
};

class Base2
{
public:
	Base2()
	{
		this->y = 2;
	}
	virtual void fun1()
	{
		printf("Base2 fun1\n");
	}
	virtual void fun2()
	{
		printf("Base2 fun2\n");
	}
private:
	int y;
};

class Sub : public Base1, public Base2
{
public:
	Sub()
	{
		this->z = 3;
	}
	virtual void fun1()
	{
		printf("Sub fun 1\n");
	}
	virtual void fun4()
	{
		printf("Sub fun 4\n");
	}
private:
	int z;
};

    接着查看子类构造函数

	Sub()
000B10C0  push        ebp  
000B10C1  mov         ebp,esp  
000B10C3  sub         esp,0CCh  
000B10C9  push        ebx  
000B10CA  push        esi  
000B10CB  push        edi  
000B10CC  push        ecx  
000B10CD  lea         edi,[ebp-0CCh]  
000B10D3  mov         ecx,33h  
000B10D8  mov         eax,0CCCCCCCCh  
000B10DD  rep stos    dword ptr es:[edi]  
000B10DF  pop         ecx  
000B10E0  mov         dword ptr [this],ecx  
000B10E3  mov         ecx,offset _D3472036_test@cpp (014200Bh)  
000B10E8  call        __CheckForDebuggerJustMyCode (0B1700h)  
000B10ED  mov         ecx,dword ptr [this]            //类变量地址赋给ecx
000B10F0  call        Base1::Base1 (0B1000h)          //调用Base1构造函数
000B10F5  mov         ecx,dword ptr [this]  
000B10F8  add         ecx,8                          //类变量地址+8
000B10FB  call        Base2::Base2 (0B1060h)         //调用Base2构造函数
000B1100  mov         eax,dword ptr [this]  
000B1103  mov         dword ptr [eax],offset Sub::`vftable' (0123218h)   //类变量地址偏移0处赋值第一个虚函数表
000B1109  mov         eax,dword ptr [this]  
000B110C  mov         dword ptr [eax+8],offset Sub::`vftable' (012322Ch)  //类变量地址偏移4处赋值第二个虚函数表
	{
		this->z = 3;
000B1113  mov         eax,dword ptr [this]  
000B1116  mov         dword ptr [eax+10h],3          //为类成员赋值,此时偏移为0x10
	}
000B111D  mov         eax,dword ptr [this]  
000B1120  pop         edi  
000B1121  pop         esi  
000B1122  pop         ebx  
000B1123  add         esp,0CCh  
000B1129  cmp         ebp,esp  
000B112B  call        _RTC_CheckEsp (0B16C0h)  
000B1130  mov         esp,ebp  
000B1132  pop         ebp  
000B1133  ret

    可以看到和多重继承相比,在多继承中,子类构造函数会依照继承顺序从左到右调用父类构造函数。而且调用父类构造函数的时候,传的地址是经过计算的,在调用Base2的构造函数的时候,这里ecx之所以加8是因为虚表指针占4个字节,Base1类中的x占4个字节。其次就是会赋值两个虚表地址,而被赋值的位置也是经过计算的,第二个虚表指针赋值的时候eax+8,这个8和意思和add ecx, 8一样。

    在依次查看Base1和Base2的构造函数

	Base1()
000B1000  push        ebp  
000B1001  mov         ebp,esp  
000B1003  sub         esp,0CCh  
000B1009  push        ebx  
000B100A  push        esi  
000B100B  push        edi  
000B100C  push        ecx  
000B100D  lea         edi,[ebp-0CCh]  
000B1013  mov         ecx,33h  
000B1018  mov         eax,0CCCCCCCCh  
000B101D  rep stos    dword ptr es:[edi]  
000B101F  pop         ecx  
000B1020  mov         dword ptr [this],ecx  
000B1023  mov         ecx,offset _D3472036_test@cpp (014200Bh)  
000B1028  call        __CheckForDebuggerJustMyCode (0B1700h)  
000B102D  mov         eax,dword ptr [this]  
000B1030  mov         dword ptr [eax],offset Base1::`vftable' (01231B4h)  
	{
		this->x = 1;
000B1036  mov         eax,dword ptr [this]  
000B1039  mov         dword ptr [eax+4],1  
	}
000B1040  mov         eax,dword ptr [this]  
000B1043  pop         edi  
000B1044  pop         esi  
000B1045  pop         ebx  
000B1046  add         esp,0CCh  
000B104C  cmp         ebp,esp  
000B104E  call        _RTC_CheckEsp (0B16C0h)  
000B1053  mov         esp,ebp  
000B1055  pop         ebp  
000B1056  ret

Base2()
000B1060  push        ebp  
000B1061  mov         ebp,esp  
000B1063  sub         esp,0CCh  
000B1069  push        ebx  
000B106A  push        esi  
000B106B  push        edi  
000B106C  push        ecx  
000B106D  lea         edi,[ebp-0CCh]  
000B1073  mov         ecx,33h  
000B1078  mov         eax,0CCCCCCCCh  
000B107D  rep stos    dword ptr es:[edi]  
000B107F  pop         ecx  
000B1080  mov         dword ptr [this],ecx  
000B1083  mov         ecx,offset _D3472036_test@cpp (014200Bh)  
000B1088  call        __CheckForDebuggerJustMyCode (0B1700h)  
000B108D  mov         eax,dword ptr [this]  
000B1090  mov         dword ptr [eax],offset Base2::`vftable' (01231F4h)  
	{
		this->y = 2;
000B1096  mov         eax,dword ptr [this]  
000B1099  mov         dword ptr [eax+4],2  
	}
000B10A0  mov         eax,dword ptr [this]  
000B10A3  pop         edi  
000B10A4  pop         esi  
000B10A5  pop         ebx  
000B10A6  add         esp,0CCh  
000B10AC  cmp         ebp,esp  
000B10AE  call        _RTC_CheckEsp (0B16C0h)  
000B10B3  mov         esp,ebp  
000B10B5  pop         ebp  
000B10B6  ret

    可以看到Base1和Base2由于没用父类,所以他们的构造函数就是首先赋值虚表指针,然后赋值类成员变量。所以数据的排布以及在内存中的情况会如下图所示

                    

    接下来就看看两个虚函数表中的内容具体是什么,同样用IDA查看。首先看看这两个虚函数表

    

    为了方便展示就改名如下

    

    然后查看所有虚函数表如下

    

    一样跟进去然后改名如下

   

    其中sub_40122E的调用如下,可以看到是对Sub_fun1的调用

    

    所以可以得出结论,所有的父类的虚函数都会保存下来,每个父类对应一张虚函数表,如果在子类中有重载,那么虚表中的虚函数的地址就会换成重载的地址。对于父类中没有的子类虚函数,它会按顺序保存在第一个虚函数表中。


【看雪培训】《Adroid高级研修班》2022年春季班招生中!

最后于 2021-12-31 10:08 被1900编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回