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指针和不同的偏移值, 就可以访问同一个共同父类部分的成员了.
  • 如果错误, 欢迎指正, 谢谢.
posted @ 2017-04-19 20:21  fishbool  阅读(429)  评论(0编辑  收藏  举报