首页
论坛
课程
招聘
[原创]从反汇编的角度学C/C++之虚继承
2021-10-4 11:05 3446

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

2021-10-4 11:05
3446

一.虚继承

    先修改定义如下,看不是虚继承的情况下的内存情况

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

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

class Base2 : public Base
{
public:
	Base2()
	{
		this->z = 3;
	}
	virtual void fun2()
	{
		printf("Base2 fun2\n");
	}
private:
	int z;
};

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

    先看sub构造函数

    Sub()
002F1120  push        ebp  
002F1121  mov         ebp,esp  
002F1123  sub         esp,0CCh  
002F1129  push        ebx  
002F112A  push        esi  
002F112B  push        edi  
002F112C  push        ecx  
002F112D  lea         edi,[ebp-0CCh]  
002F1133  mov         ecx,33h  
002F1138  mov         eax,0CCCCCCCCh  
002F113D  rep stos    dword ptr es:[edi]  
002F113F  pop         ecx  
002F1140  mov         dword ptr [this],ecx  
002F1143  mov         ecx,offset _D3472036_test@cpp (038200Bh)  
002F1148  call        __CheckForDebuggerJustMyCode (02F1760h)  
002F114D  mov         ecx,dword ptr [this]  
002F1150  call        Base1::Base1 (02F1000h)   //调用Base1构造函数
002F1155  mov         ecx,dword ptr [this]  
002F1158  add         ecx,0Ch  
002F115B  call        Base2::Base2 (02F1060h)   //这里的偏移是0xC,是因为Base1有父类Base,所以他的数据是虚函数表指针,x,y三个
002F1160  mov         eax,dword ptr [this]  
002F1163  mov         dword ptr [eax],offset Sub::`vftable' (0363220h)  
002F1169  mov         eax,dword ptr [this]  
002F116C  mov         dword ptr [eax+0Ch],offset Sub::`vftable' (0363234h)  //这里偏移0xC与上相同
	{
		this->v = 4;
002F1173  mov         eax,dword ptr [this]  
002F1176  mov         dword ptr [eax+18h],4  //这里偏移0x18是因为Base1和Base2都会在地址中初始话x,加上虚表地址和自身的成员变量都是0xC
	}                                    //所以此时两个父类占有0x18的数据
002F117D  mov         eax,dword ptr [this]  
002F1180  pop         edi  
002F1181  pop         esi  
002F1182  pop         ebx  
002F1183  add         esp,0CCh  
002F1189  cmp         ebp,esp  
002F118B  call        _RTC_CheckEsp (02F1720h)  
002F1190  mov         esp,ebp  
002F1192  pop         ebp  
002F1193  ret

    可以看到这里和多继承时候的调用是一样的,只是由于成员变量变多偏移不同罢了,在看看Base1和Base2的构造函数

    Base1()
002F1000  push        ebp  
002F1001  mov         ebp,esp  
002F1003  sub         esp,0CCh  
002F1009  push        ebx  
002F100A  push        esi  
002F100B  push        edi  
002F100C  push        ecx  
002F100D  lea         edi,[ebp-0CCh]  
002F1013  mov         ecx,33h  
002F1018  mov         eax,0CCCCCCCCh  
002F101D  rep stos    dword ptr es:[edi]  
002F101F  pop         ecx  
002F1020  mov         dword ptr [this],ecx  
002F1023  mov         ecx,offset _D3472036_test@cpp (038200Bh)  
002F1028  call        __CheckForDebuggerJustMyCode (02F1760h)  
002F102D  mov         ecx,dword ptr [this]  
002F1030  call        Base::Base (02F10C0h)  
002F1035  mov         eax,dword ptr [this]  
002F1038  mov         dword ptr [eax],offset Base1::`vftable' (03631E8h)  
	{
		this->y = 2;
002F103E  mov         eax,dword ptr [this]  
002F1041  mov         dword ptr [eax+8],2  //由于偏移为4的地方要用来给x赋值,所以y的偏移为8
	}
002F1048  mov         eax,dword ptr [this]  
002F104B  pop         edi  
002F104C  pop         esi  
002F104D  pop         ebx  
002F104E  add         esp,0CCh  
002F1054  cmp         ebp,esp  
002F1056  call        _RTC_CheckEsp (02F1720h)  
002F105B  mov         esp,ebp  
002F105D  pop         ebp  
002F105E  ret

    Base2()
002F1060  push        ebp  
002F1061  mov         ebp,esp  
002F1063  sub         esp,0CCh  
002F1069  push        ebx  
002F106A  push        esi  
002F106B  push        edi  
002F106C  push        ecx  
002F106D  lea         edi,[ebp-0CCh]  
002F1073  mov         ecx,33h  
002F1078  mov         eax,0CCCCCCCCh  
002F107D  rep stos    dword ptr es:[edi]  
002F107F  pop         ecx  
002F1080  mov         dword ptr [this],ecx  
002F1083  mov         ecx,offset _D3472036_test@cpp (038200Bh)  
002F1088  call        __CheckForDebuggerJustMyCode (02F1760h)  
002F108D  mov         ecx,dword ptr [this]  
002F1090  call        Base::Base (02F10C0h)  
002F1095  mov         eax,dword ptr [this]  
002F1098  mov         dword ptr [eax],offset Base2::`vftable' (0363204h)  
	{
		this->z = 3;
002F109E  mov         eax,dword ptr [this]  
002F10A1  mov         dword ptr [eax+8],3          //由于偏移为4的地方要用来给x赋值,所以z的偏移为8
	}
002F10A8  mov         eax,dword ptr [this]  
002F10AB  pop         edi  
002F10AC  pop         esi  
002F10AD  pop         ebx  
002F10AE  add         esp,0CCh  
002F10B4  cmp         ebp,esp  
002F10B6  call        _RTC_CheckEsp (02F1720h)  
002F10BB  mov         esp,ebp  
002F10BD  pop         ebp  
002F10BE  ret

    接下来看看Base的构造函数

	Base()
002F10C0  push        ebp  
002F10C1  mov         ebp,esp  
002F10C3  sub         esp,0CCh  
002F10C9  push        ebx  
002F10CA  push        esi  
002F10CB  push        edi  
002F10CC  push        ecx  
002F10CD  lea         edi,[ebp-0CCh]  
002F10D3  mov         ecx,33h  
002F10D8  mov         eax,0CCCCCCCCh  
002F10DD  rep stos    dword ptr es:[edi]  
002F10DF  pop         ecx
002F10E0  mov         dword ptr [this],ecx  
002F10E3  mov         ecx,offset _D3472036_test@cpp (038200Bh)  
002F10E8  call        __CheckForDebuggerJustMyCode (02F1760h)  
002F10ED  mov         eax,dword ptr [this]  
002F10F0  mov         dword ptr [eax],offset Base::`vftable' (03631B4h)  
	{
		this->x = 1;
002F10F6  mov         eax,dword ptr [this]  
002F10F9  mov         dword ptr [eax+4],1      //为x赋值
	}
002F1100  mov         eax,dword ptr [this]  
002F1103  pop         edi  
002F1104  pop         esi  
002F1105  pop         ebx  
002F1106  add         esp,0CCh  
002F110C  cmp         ebp,esp  
002F110E  call        _RTC_CheckEsp (02F1720h)  
002F1113  mov         esp,ebp  
002F1115  pop         ebp  
002F1116  ret

    可以看到不是虚继承的情况下,程序会分别调用两个父类的构造函数,由于两个父类又同时有父类,它们都会在内存中留空间给父类成员也就是x赋值。所以数据的排布以及在内存中的情况会如下图所示

               

    可以看到在没有使用虚继承的情况下程序会生成两份Base类的数据成员。接下来看看虚函数表的情况,根据上面的方法重命名以后结果如下

 

    其中的sub_40137E函数情况如下

    由此可以看出虚函数表内容和前面的多继承是一样的。接下来看看虚继承的情况,修改定义如下

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

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

class Base2 : virtual public Base
{
public:
	Base2()
	{
		this->z = 3;
	}
	virtual void fun2()
	{
		printf("Base2 fun2\n");
	}
private:
	int z;
};

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

    首先看看转换成虚继承以后,对构造函数的调用有什么改变

	Sub sub;
006415C8  push        1  //压入参数0
006415CA  lea         ecx,[sub]     //将类变量地址赋给ecx
006415CD  call        Sub::Sub (06411A0h) //调用Sub构造函数

    可以看到,此时在调用构造函数的时候,除了会给ecx赋值为类变量的地址以外,还会压入一个参数,值为1。这里需要补充一点的是,在虚继承中,编译器会产生一个叫虚基类偏移表的东西。简单来说,上面的Base1和Base2都会产生,这张表偏移为4的地方存储的是类的变量在内存中的地址和虚基类,也就是Base的类变量的地址的偏移。

    接下来首先看看Sub构造函数的内容

    Sub()
005211A0  push        ebp  
005211A1  mov         ebp,esp  
005211A3  sub         esp,0CCh  
005211A9  push        ebx  
005211AA  push        esi  
005211AB  push        edi  
005211AC  push        ecx  
005211AD  lea         edi,[ebp-0CCh]  
005211B3  mov         ecx,33h  
005211B8  mov         eax,0CCCCCCCCh  
005211BD  rep stos    dword ptr es:[edi]  
005211BF  pop         ecx  
005211C0  mov         dword ptr [this],ecx  
005211C3  mov         ecx,offset _D3472036_test@cpp (05B200Bh)  
005211C8  call        __CheckForDebuggerJustMyCode (0521860h)  
005211CD  cmp         dword ptr [ebp+8],0          //判断参数是否为0 
005211D1  je          Sub::Sub+52h (05211F2h)      //等0则跳转
005211D3  mov         eax,dword ptr [this]  
005211D6  mov         dword ptr [eax+4],offset Sub::`vbtable' (0593244h)  //类地址+4,赋值虚基类偏移表
005211DD  mov         eax,dword ptr [this]  
005211E0  mov         dword ptr [eax+0Ch],offset Sub::`vbtable' (059324Ch)  //类地址+0xC,赋值虚基类偏移表
005211E7  mov         ecx,dword ptr [this]  //将类变量地址赋给ecx
005211EA  add         ecx,1Ch               //地址增加0x1C,这个地址就是Base类变量的地址
005211ED  call        Base::Base (0521140h) //调用Base构造函数
005211F2  push        0                     //压入参数0
005211F4  mov         ecx,dword ptr [this]  //取得类变量地址
005211F7  add         ecx,4                 //地址+4,这个地址就是Base1类变量的地址
005211FA  call        Base1::Base1 (0521000h)  //调用Base1的构造函数
005211FF  push        0                      //压入参数0
00521201  mov         ecx,dword ptr [this]   //将类变量地址赋给ecx
00521204  add         ecx,0Ch                //地址+0xC,这个地址就是Base2类变量地址
00521207  call        Base2::Base2 (05210A0h)  //调用Base2构造函数
0052120C  mov         eax,dword ptr [this]    //类变量地址赋给eax
0052120F  mov         dword ptr [eax],offset Sub::`vftable' (0593230h) //赋值虚函数表地址给类变量地址偏移为0的地址  
00521215  mov         eax,dword ptr [this]    //类变量地址赋给eax
00521218  mov         ecx,dword ptr [eax+4]   //eax+4的地方保存了虚基类偏移表
0052121B  mov         edx,dword ptr [ecx+4]   //虚基类偏移表偏移为4的地方保存了类变量和基类的偏移,将它赋值给edx
0052121E  mov         eax,dword ptr [this]    //类变量地址赋给eax
00521221  mov         dword ptr [eax+edx+4],offset Sub::`vftable' (0593238h)  //类变量地址+4的地方是类Base1的变量地址加上偏移就是基类变量的地址,在这个地址这里赋值虚函数表
00521229  mov         eax,dword ptr [this]      //取出类变量地址给eax
0052122C  mov         ecx,dword ptr [eax+4]     //地址+4的地方就是Base1类变量地址,保存了虚基类偏移表
0052122F  mov         edx,dword ptr [ecx+4]     //地址+4的地方就是Base1类变量和Base类变量的偏移,赋给edx
00521232  sub         edx,18h                   //减掉0x18
00521235  mov         eax,dword ptr [this]      
00521238  mov         ecx,dword ptr [eax+4]     
0052123B  mov         eax,dword ptr [ecx+4]  
0052123E  mov         ecx,dword ptr [this]     //这些操作与上面相同
00521241  mov         dword ptr [ecx+eax],edx  //将edx赋给类变量地址加上偏移,此时是Base类变量地址-4的地方
	{
		this->v = 4;
00521244  mov         eax,dword ptr [this]    //取出类变量地址
00521247  mov         dword ptr [eax+14h],4   //为v赋值为4
	}
0052124E  mov         eax,dword ptr [this]  
00521251  pop         edi  
00521252  pop         esi  
00521253  pop         ebx  
00521254  add         esp,0CCh  
0052125A  cmp         ebp,esp  
0052125C  call        _RTC_CheckEsp (0521820h)  
00521261  mov         esp,ebp  
00521263  pop         ebp  
00521264  ret         4

    上面的虚偏移表内容如下

    

    接下来继续看Base的构造函数

    Base()
00521140  push        ebp  
00521141  mov         ebp,esp  
00521143  sub         esp,0CCh  
00521149  push        ebx  
0052114A  push        esi  
0052114B  push        edi  
0052114C  push        ecx  
0052114D  lea         edi,[ebp-0CCh]  
00521153  mov         ecx,33h  
00521158  mov         eax,0CCCCCCCCh  
0052115D  rep stos    dword ptr es:[edi]  
0052115F  pop         ecx  
00521160  mov         dword ptr [this],ecx  
00521163  mov         ecx,offset _D3472036_test@cpp (05B200Bh)  
00521168  call        __CheckForDebuggerJustMyCode (0521860h)  
0052116D  mov         eax,dword ptr [this]  
00521170  mov         dword ptr [eax],offset Base::`vftable' (05931B4h)  //赋值虚函数表
	{
		this->x = 1;
00521176  mov         eax,dword ptr [this]  
00521179  mov         dword ptr [eax+4],1  //为x赋值为1
	}
00521180  mov         eax,dword ptr [this]  
00521183  pop         edi  
00521184  pop         esi  
00521185  pop         ebx  
00521186  add         esp,0CCh  
0052118C  cmp         ebp,esp  
0052118E  call        _RTC_CheckEsp (0521820h)  
00521193  mov         esp,ebp  
00521195  pop         ebp  
00521196  ret

    接着看Base1和Base2的构造函数

    Base1()
00521000  push        ebp  
00521001  mov         ebp,esp  
00521003  sub         esp,0CCh  
00521009  push        ebx  
0052100A  push        esi  
0052100B  push        edi  
0052100C  push        ecx  
0052100D  lea         edi,[ebp-0CCh]  
00521013  mov         ecx,33h  
00521018  mov         eax,0CCCCCCCCh  
0052101D  rep stos    dword ptr es:[edi]  
0052101F  pop         ecx  
00521020  mov         dword ptr [this],ecx  
00521023  mov         ecx,offset _D3472036_test@cpp (05B200Bh)  
00521028  call        __CheckForDebuggerJustMyCode (0521860h)  
0052102D  cmp         dword ptr [ebp+8],0      //判断参数是否为0
00521031  je          Base1::Base1+47h (0521047h)   //此时为0,则跳转
00521033  mov         eax,dword ptr [this]  
00521036  mov         dword ptr [eax],offset Base1::`vbtable' (05931F4h)  
0052103C  mov         ecx,dword ptr [this]  
0052103F  add         ecx,0Ch  
00521042  call        Base::Base (0521140h)  
00521047  mov         eax,dword ptr [this]   //取出地址中的值赋给eax,此时这个值是虚偏移表地址
0052104A  mov         ecx,dword ptr [eax]    //将虚偏移表地址赋给ecx
0052104C  mov         edx,dword ptr [ecx+4]  //虚偏移表地址+4的地方保存了偏移,将偏移赋给edx
0052104F  mov         eax,dword ptr [this]   //或者类变量地址
00521052  mov         dword ptr [eax+edx],offset Base1::`vftable' (05931E8h) //此时算出的地址是Base类变量地址,在地址处赋值虚函数表  
00521059  mov         eax,dword ptr [this]  
0052105C  mov         ecx,dword ptr [eax]  
0052105E  mov         edx,dword ptr [ecx+4]  //edx与上面相同
00521061  sub         edx,0Ch                //edx减去0xC
00521064  mov         eax,dword ptr [this]  
00521067  mov         ecx,dword ptr [eax]  
00521069  mov         eax,dword ptr [ecx+4]  
0052106C  mov         ecx,dword ptr [this]  //ecx与上面相同
0052106F  mov         dword ptr [ecx+eax-4],edx  //类变量地址+偏移-4的地方赋值为edx
	{
		this->y = 2;
00521073  mov         eax,dword ptr [this]  
00521076  mov         dword ptr [eax+4],2  //为局部变量赋值
	}
0052107D  mov         eax,dword ptr [this]  
00521080  pop         edi  
00521081  pop         esi  
00521082  pop         ebx  
00521083  add         esp,0CCh  
00521089  cmp         ebp,esp  
	}
0052108B  call        _RTC_CheckEsp (0521820h)  
00521090  mov         esp,ebp  
00521092  pop         ebp  
00521093  ret         4



    Base2()
005210A0  push        ebp  
005210A1  mov         ebp,esp  
005210A3  sub         esp,0CCh  
005210A9  push        ebx  
005210AA  push        esi  
005210AB  push        edi  
005210AC  push        ecx  
005210AD  lea         edi,[ebp-0CCh]  
005210B3  mov         ecx,33h  
005210B8  mov         eax,0CCCCCCCCh  
005210BD  rep stos    dword ptr es:[edi]  
005210BF  pop         ecx  
005210C0  mov         dword ptr [this],ecx  
005210C3  mov         ecx,offset _D3472036_test@cpp (05B200Bh)  
005210C8  call        __CheckForDebuggerJustMyCode (0521860h)  
005210CD  cmp         dword ptr [ebp+8],0  
005210D1  je          Base2::Base2+47h (05210E7h)  
005210D3  mov         eax,dword ptr [this]  
005210D6  mov         dword ptr [eax],offset Base2::`vbtable' (0593218h)  
005210DC  mov         ecx,dword ptr [this]  
005210DF  add         ecx,0Ch  
005210E2  call        Base::Base (0521140h)  
005210E7  mov         eax,dword ptr [this]  
005210EA  mov         ecx,dword ptr [eax]  
005210EC  mov         edx,dword ptr [ecx+4]  
005210EF  mov         eax,dword ptr [this]  
005210F2  mov         dword ptr [eax+edx],offset Base2::`vftable' (059320Ch)  
005210F9  mov         eax,dword ptr [this]  
005210FC  mov         ecx,dword ptr [eax]  
005210FE  mov         edx,dword ptr [ecx+4]  
00521101  sub         edx,0Ch  
00521104  mov         eax,dword ptr [this]  
00521107  mov         ecx,dword ptr [eax]  
00521109  mov         eax,dword ptr [ecx+4]  
0052110C  mov         ecx,dword ptr [this]  
0052110F  mov         dword ptr [ecx+eax-4],edx  
	{
		this->z = 3;
00521113  mov         eax,dword ptr [this]  
00521116  mov         dword ptr [eax+4],3          //所执行内容与上面相同
	}
0052111D  mov         eax,dword ptr [this]  
00521120  pop         edi  
00521121  pop         esi  
00521122  pop         ebx  
00521123  add         esp,0CCh  
00521129  cmp         ebp,esp  
0052112B  call        _RTC_CheckEsp (0521820h)  
00521130  mov         esp,ebp  
00521132  pop         ebp  
00521133  ret         4

    由上可以得出,在虚继承中,为了保证只有一个Base类,所以调用构造函数的时候会传入参数来决定是否调用Base类的构造函数,1为调用0为不调用。而在类变量的内存空间中,Base1和Base2类变量会首先保存虚基类偏移表地址。最终的数据布局和内存中的内容如下

         

    最后用IDA看下虚函数表内容

        

 

    可以看出Base1和Base2的虚表保存的都是重载后的函数或者父类未重载函数。而对于Sub,如果函数没被重载,那它就会被保存在第一张表中,如果被重载,最近重载的函数就会被保存在第二张表中。


2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

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