-
-
[原创]从反汇编的角度学C/C++之多重继承与多继承
-
2021-10-3 17:15 5658
-
一.多重继承
在类的继承关系中,我们可以进行多重继承,那么它和单继承相比数据排布等等有什么不同,通过如下定义的代码来观察多重继承的特点。
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
由上可以得出
多重继承的构造函数执行流程是:祖父类构造函数(Base1)->父类构造函数(Base2)->子类构造函数(Sub)。在每个构造函数开始之前都会把自己的虚函数表赋值给虚表指针。
类变量地址中的数据是按照:虚表指针->按照祖父类(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的调用
所以可以得出结论,所有的父类的虚函数都会保存下来,每个父类对应一张虚函数表,如果在子类中有重载,那么虚表中的虚函数的地址就会换成重载的地址。对于父类中没有的子类虚函数,它会按顺序保存在第一个虚函数表中。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法