C++菱形继承逆向分析
- 实验环境:
- 操作系统: Windows XP Professional Service Pack 3
- 集成开发环境: Microsoft Visual C++ 6.0
- 构建版本: Debug版本
- 既然要讨论菱形继承, 那么就要先说说为什么会出现菱形继承, 看下面代码:
-
1 #include <iostream> 2 3 // Subject class 4 class Subject 5 { 6 protected: 7 // subject id 8 unsigned int id; 9 public: 10 Subject() { std::cout << "Subject constructor" << std::endl; } 11 ~Subject() { std::cout << "Subject destructor" << std::endl; } 12 }; 13 14 // Programming class 15 class Programming : public Subject 16 { 17 public: 18 Programming() { std::cout << "Programming constructor" << std::endl; } 19 ~Programming() { std::cout << "Programming destructor" << std::endl; } 20 21 void setProgrammingId(unsigned int id) { this->id = id; } 22 unsigned int getProgrammingId() { return this->id; } 23 }; 24 25 // Math class 26 class Math : public Subject 27 { 28 public: 29 Math() { std::cout << "Math constructor" << std::endl; }; 30 ~Math() { std::cout << "Math destructor" << std::endl; } 31 32 void setMathId(unsigned int id) { this->id = id; } 33 unsigned int getMathId() { return this->id; } 34 }; 35 36 // Assembly class 37 class Assembly : public Programming, public Math 38 { 39 public: 40 Assembly() { std::cout << "Assembly constructor" << std::endl; } 41 ~Assembly() { std::cout << "Assembly destructor" << std::endl; } 42 43 // void setAssemblyId(unsigned int id) { this->id = id; } 44 // unsigned int getAssemblyId() { return this->id; } 45 }; 46 47 int main(int argc, char **argv, const char **envp) 48 { 49 // create a Assembly instance 50 Assembly obj; 51 52 obj.setProgrammingId(0); 53 obj.setMathId(1); 54 55 std::cout << obj.getProgrammingId() << std::endl; 56 std::cout << obj.getMathId() << std::endl; 57 58 // obj.setAssemblyId(2); 59 60 // std::cout << obj.getAssemblyId() << std::endl; 61 62 return 0; 63 }
- 注释掉的部分是带有二义性的代码, 编译期间不能通过.这几个类之间的结构图如下:
![]()
- 也就是说如果创建一个Assembly类的实例, 那么这个实例中就会存在2个id成员, 如果要使用这个id的时候不能简单的this->id, 因为如果不明确的指出到底是Programming类的id还是Math类的id, 那么编译器就会报二义性错误.使用的时候必须this->Programming::id或者this->Math::id显式指明.这很麻烦, 而且有的时候我们并不需要在内存中保存2个id, 所以就有了虚继承.
- 通过反汇编上述代码也可以发现, 这2个id是不同的(排除了无关代码):
-
55: std::cout << obj.getProgrammingId() << std::endl; ; 用寄存器传参的方式传入this指针, 该this指针指向父类的Programming部分 00401645 lea ecx,[ebp-14h] ; 调用getProgrammingId函数 00401648 call @ILT+240(Programming::getProgrammingId) (004010f5)
-
22: unsigned int getProgrammingId() { return this->id; } ; 将this指针存放到ecx中 00401749 pop ecx ; 将this指针存放到[ebp-4]中 0040174A mov dword ptr [ebp-4],ecx ; 将this指针存放到eax中 0040174D mov eax,dword ptr [ebp-4] ; 将this指针指向的1个双字存放到eax中, 此处即为Programming部分的id 00401750 mov eax,dword ptr [eax]
- getMathId函数部分也是类似的, 只不过this指针为[ebp -18h]. 可以看出, 这2个id, 由于this指针指向的是不同的部分, 所以id也是不同的.
- 接下来看下使用菱形继承的代码:
-
1 #include <iostream> 2 3 // Subject class 4 class Subject 5 { 6 protected: 7 // subject id 8 unsigned int id; 9 public: 10 Subject() { std::cout << "Subject constructor" << std::endl; } 11 ~Subject() { std::cout << "Subject destructor" << std::endl; } 12 }; 13 14 // Programming class 15 class Programming : virtual public Subject 16 { 17 public: 18 Programming() { std::cout << "Programming constructor" << std::endl; } 19 ~Programming() { std::cout << "Programming destructor" << std::endl; } 20 21 void setProgrammingId(unsigned int id) { this->id = id; } 22 unsigned int getProgrammingId() { return this->id; } 23 }; 24 25 // Math class 26 class Math : virtual public Subject 27 { 28 public: 29 Math() { std::cout << "Math constructor" << std::endl; }; 30 ~Math() { std::cout << "Math destructor" << std::endl; } 31 32 void setMathId(unsigned int id) { this->id = id; } 33 unsigned int getMathId() { return this->id; } 34 }; 35 36 // Assembly class 37 class Assembly : public Programming, public Math 38 { 39 public: 40 Assembly() { std::cout << "Assembly constructor" << std::endl; } 41 ~Assembly() { std::cout << "Assembly destructor" << std::endl; } 42 43 // void setAssemblyId(unsigned int id) { this->id = id; } 44 // unsigned int getAssemblyId() { return this->id; } 45 }; 46 47 int main(int argc, char **argv, const char **envp) 48 { 49 // create a Assembly instance 50 Assembly obj; 51 52 obj.setProgrammingId(0); 53 obj.setMathId(1); 54 55 std::cout << obj.getProgrammingId() << std::endl; 56 std::cout << obj.getMathId() << std::endl; 57 58 // obj.setAssemblyId(2); 59 60 // std::cout << obj.getAssemblyId() << std::endl; 61 62 return 0; 63 }
- 整个代码几乎和上一个代码一模一样, 只是15行和26行都多了个virtual关键字, 这样就使用的是虚继承. 至于为什么要在父类上写virtual关键字, 是因为, Assembly之所以会有2个id, 是因为它继承的是有公共父类的2个类, 所以它只是果, 而不是因, 要杜绝这种重复现象, 应该从因入手, 而不是从果, 所以virtual关键字加在父类的继承关系上.
![]()
- 下面再来看看, 用了虚继承之后的汇编代码有什么不同:
-
22: unsigned int getProgrammingId() { return this->id; } ; 将this指针存放到ecx中 00401759 pop ecx ; 将this指针存放到[ebp - 4]中 0040175A mov dword ptr [ebp-4],ecx ; 将this指针存放到eax中 0040175D mov eax,dword ptr [ebp-4] ; 将this指针指向的1个双字存放到ecx中 00401760 mov ecx,dword ptr [eax] ; 将[ecx + 4]的内从容存放到edx中 00401762 mov edx,dword ptr [ecx+4] ; 将[ebp - 4]中的this指针存放到eax中 00401765 mov eax,dword ptr [ebp-4] ; 将[eax + edx]的内容存放到eax中 00401768 mov eax,dword ptr [eax+edx]
- 这里可能会有疑惑, this指针的首地址中的第一个双字存放的是什么, 这里存放了一个地址, 查看这个地址的数据, 可以看到, 在偏移+4的地方存放了一个双字的0x8, 这个其实是父类的成员相对于当前this指针的偏移值, 也就是说Programming部分的首地址比父类的id成员地址小8个字节.所以eax + edx就是父类的id成员的地址, 取出放到eax中. 这里的id成员的地址是0x0012FF70.
![]()
![]()
- 可以顺便看看getMathId的反汇编代码:
-
33: unsigned int getMathId() { return this->id; } ; 代码解释同上 004017EA mov dword ptr [ebp-4],ecx 004017ED mov eax,dword ptr [ebp-4] 004017F0 mov ecx,dword ptr [eax] 004017F2 mov edx,dword ptr [ecx+4] 004017F5 mov eax,dword ptr [ebp-4] 004017F8 mov eax,dword ptr [eax+edx]
- 看下此时的内存数据图: 可以看到, 大体结构与上面getProgrammingId没有区别, 再到相应的地址看看, 会发现, 这里的偏移值是0x4, 而不是上面的0x8, 原因是0x0012FF6C + 0x4 == 0x0012FF68 + 0x8 == 0x0012FF70, 看到这里明白了吧? 就是说, 内存中只有一份id的地址, 但是在每一个具有公共父类的直接父类部分的this指针的首部都保留了一个指针, 这个指针偏移0x4处存放了一个偏移量, 这个偏移量就是当前this指针与父类成员的偏移值, 所以, 每个部分根据不同的this指针和不同的偏移值, 就可以访问同一个共同父类部分的成员了.
![]()
![]()
- 如果错误, 欢迎指正, 谢谢.







浙公网安备 33010602011771号