-
-
[原创]从反汇编的角度学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春季赛【最佳人气奖】火热评选中!快来投票吧~