zoukankan      html  css  js  c++  java
  • 菱形继承,多继承,虚继承、虚表的内存结构全面剖析(逆向分析基础)

    // 声明:以下代码均在Win32_Sp3   VC6.0_DEBUG版中调试通过..

    在逆向还原代码的时候,必须得掌握了菱形继承,多继承,虚继承虚函数的内存虚表结构。所以,这篇文章献给正在学习C++的朋友们。

    当然,由于水平有限,必定错漏百出!所以,希望耽误您的时间,恳求您的指点。在这里万分感谢!

    首先,我们定义如下类:

    class A
    {
    public:
      A()
      {
        m_nData = 1;
      }
      virtual void fun()
      {
    
      }
      int m_nData;
    };
    
    class B
    {
    public:
      B()
      {
        m_nData = 2;
      }
      virtual void fun()
      {
        
      }
      int m_nData;
    };
    
    class AB :public A, public B
    {
    public:
      AB()
      {
        m_nData = 3;
      }
      virtual void fun()
      {
        
      }
      int m_nData;
    };
    
      
    int main(int argc, char* argv[])
    {
      AB the;
    
      return 0;
    }

    类的构造顺序:先基类-->在成员对象-->在派生类(自己)

    所以the对象的构造过程如下:

    按照继承定义时写的顺序:

    1、基类A构造:虚表赋值,成员数据

    2、基类B构造:虚表赋值,成员数据

    3、派生类AB构造:虚表覆盖,成员数据

    内存中结构如下图:

    在做如下修改:

    class A
    {
    public:
      A()
      {
        m_nData = 1;
      }
      virtual void fun()
      {
    
      }
      virtual void fun1()  // 新增加
      {
        
      }
      int m_nData;
    };
    
    class B
    {
    public:
      B()
      {
        m_nData = 2;
      }
      virtual void fun()
      {
        
      }
      virtual void fun2()  // 新增加
      {
        
      }
      int m_nData;
    };
    
    class AB :public A, public B
    {
    public:
      AB()
      {
        m_nData = 3;
      }
      virtual void fun()
      {
        
      }
      virtual void fun3()  // 新增加
      {
        
      }
      int m_nData;
    };
    
      
    int main(int argc, char* argv[])
    {
      AB the;
    
      return 0;
    }

    对于the对象来说,它的内存结构的内存结构还是不会改变,但是虚表的内容会改变,改变后的虚表如下:

    1、A::Vtable如下:

    &AB::fun  &A::fun1  AB::fun3

    2、B::Vtable如下:

    &AB::fun  &B::fun2

    总结一下:先按继承声明顺序依次构造虚表,如果子类有虚函数,并且不同名,则填写到声明顺序首位的基类虚表中的末尾项。

    我们从浅到深慢慢的剖析虚继承的内存结构,首先看源码如下:

    class A
    {
    public:
      A()
      {
        m_nDataA = 1;
      }
      int m_nDataA;
    };
    
    class B :virtual public A
    {
    public:
      B()
      {
        m_nDataB = 2;
      }
      int m_nDataB;
    };
    
    int main(int argc, char* argv[])
    {
      B the;
      return 0;
    }

    假设没有virtual虚继承关键字,the对象在内存中的结构如下:

    A::m_nDataA

    B::m_nDataB

    现在我们的源码中有virtual继承关键字,那么内存结构必然会有区别,那么内存结构是怎么样的呢?如下:

    B::base Offset m_nDataA     // A::m_nDataA数据的偏移

    B::m_nDataB

    A::m_nDataA

    编译器为什么要这么做呢?这个偏移值是什么?这么做的意义又何在?

    首先,这么做是为了只存在于一份虚基类数据。后面会讲解。

    B::base Offset 偏移的值,一般为全局数据区中。编译器为了和虚表区别。这个指针指向的地址的值一般为:0x00000000, 或者某些特殊值

    而在他后面的4个字节中。才是真正数据的偏移地址

    为什么取这两个值?为什么不直接写偏移呢?

    编译器为了在内存中只产生一份基类数据,当然就必须得写偏移值,可是又为了和虚表区分。所以只能取特殊值作为区分。(当然这里仅个人猜想,不作参考)

    继续看源码:

    class A
    {
    public:
      A()
      {
        m_nDataA = 1;
      }
      virtual void fun()
      {
      }
      int m_nDataA;
    };
    
    class B :virtual public A
    {
    public:
      B()
      {
        m_nDataB = 2;
      }
      virtual void fun()
      {
      }
      int m_nDataB;
    };
    
    int main(int argc, char* argv[])
    {
      B the;
      return 0;
    }

    现在多了虚表的加入。内存结构有了大的变化

    the对象的内存结构如下:

    B::base Offset A   // B的父类A的偏移

    B::m_nDataB

    0x00000000  // 虚基类的非虚基类的分隔符

    B::Virtual

    A::m_nDataA

    划红线的地方,产生覆盖,我们慢慢剖析编译器构造的过程。

    the对象初始化空间如下:

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    先是虚基类的构造。内存结构如下:

    1、第一步

    B::base Offset A      // 填入A类的偏移 

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    2、第二步

    B::base Offset A      // 由此处指向内容向下4个字节为B的父类A的偏移。取出内容偏移地址后。当前地址 + 偏移地址  ==  填写A类虚表的地址

    0xCCCCCCCC

    0xCCCCCCCC

    A::virtual        // A类的虚表

    0xCCCCCCCC

    3、第三步

    B::base Offset A      

    0xCCCCCCCC

    0xCCCCCCCC

    A::virtual        

    A::m_nDataA

    4、第四步(程序流程返回到派生类B构造函数)

    B::base Offset A      

    0xCCCCCCCC

    0x00000000      // 填充全0,作为虚基类和非基类的分隔符

    A::virtual        

    A::m_nDataA

    5、第五步(虚表赋值)

    A::Offset A      

    0xCCCCCCCC

    0x00000000

    B::virtual           // 由于派生类的有写fun虚函数。构成覆盖关系。所以覆盖A的虚表

    A::m_nDataA

    6、第六步

    B::base Offset A     

    B::m_nDataB    // B::m_nDataB

    0x00000000

    B::virtual

    A::m_nDataA

    我们继续看源码:

    class A
    {
    public:
      A()
      {
        m_nDataA = 1;
      }
      virtual void fun()
      {
      }
      virtual void fun1()
      {
      }
      int m_nDataA;
    };
    
    class B :virtual public A
    {
    public:
      B()
      {
        m_nDataB = 2;
      }
      virtual void fun()
      {
      }
      virtual void fun2()
      {
      }
      int m_nDataB;
    };
    
    int main(int argc, char* argv[])
    {
      B the;
      return 0;
    }

    加入了虚函数,构成多个虚表的the对象内存结构如下:

    B::Vtable

    B::base Offset A

    B::m_nDataB

    0x00000000

    A::Vtable

    A::m_nDataA

    我们继续慢慢剖析内存结构。the对象初始化内存空间如下:

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    0xCCCCCCCC

    1、第一步

    0xCCCCCCCC

    B::base offset A   // B的虚基类A的偏移地址. 规律找出来了。不用看偏移基本也可以推测出以下上个区域

    0xCCCCCCCC

    0xCCCCCCCC     // 这里应该是虚基类和非虚基类的分隔符。

    0xCCCCCCCC     // 后面的步骤会填充为A的虚表

    0xCCCCCCCC     // 后面的步骤会填充为A的数据成员m_nDataA

    结果会是我们推测的这样吗?这是构造完虚基类A的情况。

    果然和我们猜想的一样.到这里了。你肯定会问。为什么不在是对象的首地址开始填充偏移地址了。

    这里要搞清楚的是。现在派生类有了自己虚函数Fun2(). 并且和父类不同名。所以必须单独建立一张虚表了。于是编译器就这样安排内存结构了.

    继续往下剖析:

    由于重复的操作,省略......  我们直接来看第五步

    5、第五步(派生类B的构造函数)

    B::virtual

    B::base offset A

    0xCCCCCCCC

    0x00000000      //  虚基类和非虚基类的分隔符

    A::Virtual          

    A::m_nDataA

    这里红色标记的地方产生了派生类虚函数的覆盖,虚表中的结构如下:

    B::fun    // 覆盖掉了 A::fun

    A::fun1

    并且,产生一个B虚表,虚表中的结构如下:

    B::fun2

    6、第六步

    B::virtual

    B::base offset A

    B::m_nDataB     // B的成员数据

    0x00000000      

    A::Virtual          

    A::m_nDataA

    好了,现在有了前面的讲解,我们来剖析下较为复杂菱形继承的内存结构,源码如下:

    class A
    {
    public:
      A()
      {
        m_nDataA = 1;
      }
      int m_nDataA;
    };
    
    class B :virtual public A
    {
    public:
      B()
      {
        m_nDataB = 2;
      }
      int m_nDataB;
    };
    
    class C :virtual public A
    {
    public:
      C()
      {
        m_nDatac = 3;
      }
      int m_nDatac;
    };
    
    class BC :public B, public C
    {
    public:
      BC()
      {
        m_nDataBC = 4;
      }
      int m_nDataBC;
    };
    
    int main(int argc, char* argv[])
    {
      BC the;
      return 0;
    }

    由于是虚继承,所以虚基类只会产生一份拷贝.内存结构必然如下:

    B::base offset A

    B::m_nDataB

    C::base offset A

    C::m_nDataC

    BC::m_nDataBC

    A::m_nDataA 

    在变形下.源码如下:

    class A
    {
    public:
      A()
      {
        m_nDataA = 1;
      }
      int m_nDataA;
      virtual void fun(){}  // 新增加
    };
    
    class B :virtual public A
    {
    public:
      B()
      {
        m_nDataB = 2;
      }
      int m_nDataB;
      virtual void fun(){}  // 新增加
    };
    
    class C :virtual public A
    {
    public:
      C()
      {
        m_nDatac = 3;
      }
      int m_nDatac;
      virtual void fun(){}  // 新增加
    };
    
    class BC :public B, public C
    {
    public:
      BC()
      {
        m_nDataBC = 4;
      }
      int m_nDataBC;
      virtual void fun(){}  // 新增加
    };
    
    int main(int argc, char* argv[])
    {
      BC the;
      return 0;
    }

    加了虚函数后,the对象的内存结构如下:

    B::base offset A

    B::m_nDataB

    C::base offset A

    C::m_nDataC

    0x00000000

    BC::vtable

    A::m_nDataA 

    红色地方同理,派生类的fun多次覆盖父类的。最后为BC::vtable。

    我们继续变形如下:

    class A
    {
    public:
      A()
      {
        m_nDataA = 1;
      }
      virtual void fun(){}
      virtual void funA(){}
      int m_nDataA;
    };
    
    class B :virtual public A
    {
    public:
      B()
      {
        m_nDataB = 2;
      }
      virtual void fun(){}
      virtual void funB(){}
      int m_nDataB;
    };
    
    class C :virtual public A
    {
    public:
      C()
      {
        m_nDatac = 3;
      }
      virtual void fun(){}
      virtual void funC(){}
      int m_nDatac;
    };
    
    class BC :public B, public C
    {
    public:
      BC()
      {
        m_nDataBC = 4;
      }
      virtual void fun(){}
      virtual void funBC(){}
      int m_nDataBC;
    };
    
    int main(int argc, char* argv[])
    {
      BC the;
      return 0;
    }

    the对象的内存结构如下:

    B::vtable

    B::base offset A 

    B::m_nDataB

    C::vtable

    C::base offset A

    C::m_nDataC

    BC::m_nDataBC

    0x00000000

    A::vtable

    A::m_nDataA

    B::vtable中表末尾存放着BC::funBC, 而BC::fun则覆盖到A::vtable中.

    BC::funBC我们知道。即使不是虚继承。也会自动填充到按定义顺序首基类的虚表的末尾。

    而B是定义的首继承基类,而B::fun中又覆盖掉了虚基类A的虚表的A::fun,由于B和C虚继承于A。

    所以B和C不能同时都在虚基类A中虚表末尾加上各自的虚函数,所以只能自己建张表.

    然而BC又是以B定义顺序的基类.也不是虚继承。就把BC::funBC直接填充到B::vtable末尾.

    到此为止,我们分析了几乎大部分虚继承的内存结构。在看到内存的时候。大家是否能还原出代码呢?

    当然了。还有很多更复杂的结构。只要掌握了最基本的原理。无非就是组合使用了!

  • 相关阅读:
    GSM Arena 魅族mx四核评测个人翻译
    Oracle Exists用法|转|
    NC公有协同的实现原理|同13的QQ||更新总部往来协同|
    NC客商bd_custbank不可修改账号、名称但可修改默认银行并更新分子公司trigger
    试玩了plsql中test窗口declare声明变量|lpad函数||plsql sql command test window区别|
    使用windows live writer测试
    用友写insert on bd_custbank 触发器和自动更新单位名称2in1
    oracle触发器select into和cursor用法的区别
    |转|oracle中prior的用法,connect by prior,树形目录
    客商增加自动增加银行账户|搞定!||更新使用游标course写法|
  • 原文地址:https://www.cnblogs.com/ziolo/p/3066022.html
Copyright © 2011-2022 走看看