400-028-6601
建站资讯

网站建设资讯

为你提供网站建设行业资讯、网站优化知识、主机域名邮箱、网站开发常见问题等。

全面剖析逆向还原代码的内存结构

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

建网站原本是网站策划师、网络程序员、网页设计师等,应用各种网络程序开发技术和网页设计技术配合操作的协同工作。创新互联公司专业提供成都网站建设、成都网站制作,网页设计,网站制作(企业站、响应式网站、电商门户网站)等服务,从网站深度策划、搜索引擎友好度优化到用户体验的提升,我们力求做到极致!

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

首先,我们定义如下类:

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nData = 1; 
  7.   } 
  8.   virtual void fun() 
  9.   { 
  10.  
  11.   } 
  12.   int m_nData; 
  13. }; 
  14.  
  15. class B 
  16. { 
  17. public: 
  18.   B() 
  19.   { 
  20.     m_nData = 2; 
  21.   } 
  22.   virtual void fun() 
  23.   { 
  24.      
  25.   } 
  26.   int m_nData; 
  27. }; 
  28.  
  29. class AB :public A, public B 
  30. { 
  31. public: 
  32.   AB() 
  33.   { 
  34.     m_nData = 3; 
  35.   } 
  36.   virtual void fun() 
  37.   { 
  38.      
  39.   } 
  40.   int m_nData; 
  41. }; 
  42.  
  43.    
  44. int main(int argc, char* argv[]) 
  45. { 
  46.   AB the; 
  47.  
  48.   return 0; 
  49. } 

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

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

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

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

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

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

内存中结构如下图:

在做如下修改:

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nData = 1; 
  7.   } 
  8.   virtual void fun() 
  9.   { 
  10.  
  11.   } 
  12.   virtual void fun1()  // 新增加 
  13.   { 
  14.      
  15.   } 
  16.   int m_nData; 
  17. }; 
  18.  
  19. class B 
  20. { 
  21. public: 
  22.   B() 
  23.   { 
  24.     m_nData = 2; 
  25.   } 
  26.   virtual void fun() 
  27.   { 
  28.      
  29.   } 
  30.   virtual void fun2()  // 新增加 
  31.   { 
  32.      
  33.   } 
  34.   int m_nData; 
  35. }; 
  36.  
  37. class AB :public A, public B 
  38. { 
  39. public: 
  40.   AB() 
  41.   { 
  42.     m_nData = 3; 
  43.   } 
  44.   virtual void fun() 
  45.   { 
  46.      
  47.   } 
  48.   virtual void fun3()  // 新增加 
  49.   { 
  50.      
  51.   } 
  52.   int m_nData; 
  53. }; 
  54.  
  55.    
  56. int main(int argc, char* argv[]) 
  57. { 
  58.   AB the; 
  59.  
  60.   return 0; 
  61. } 

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

1、A::Vtable如下:

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

2、B::Vtable如下:

&AB::fun  &B::fun2

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

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

 

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nDataA = 1; 
  7.   } 
  8.   int m_nDataA; 
  9. }; 
  10.  
  11. class B :virtual public A 
  12. { 
  13. public: 
  14.   B() 
  15.   { 
  16.     m_nDataB = 2; 
  17.   } 
  18.   int m_nDataB; 
  19. }; 
  20.  
  21. int main(int argc, char* argv[]) 
  22. { 
  23.   B the; 
  24.   return 0; 
  25. } 

 

假设没有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个字节中。才是真正数据的偏移地址

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

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

继续看源码:

 

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nDataA = 1; 
  7.   } 
  8.   virtual void fun() 
  9.   { 
  10.   } 
  11.   int m_nDataA; 
  12. }; 
  13.  
  14. class B :virtual public A 
  15. { 
  16. public: 
  17.   B() 
  18.   { 
  19.     m_nDataB = 2; 
  20.   } 
  21.   virtual void fun() 
  22.   { 
  23.   } 
  24.   int m_nDataB; 
  25. }; 
  26.  
  27. int main(int argc, char* argv[]) 
  28. { 
  29.   B the; 
  30.   return 0; 
  31. } 

 

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

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

我们继续看源码:

 

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nDataA = 1; 
  7.   } 
  8.   virtual void fun() 
  9.   { 
  10.   } 
  11.   virtual void fun1() 
  12.   { 
  13.   } 
  14.   int m_nDataA; 
  15. }; 
  16.  
  17. class B :virtual public A 
  18. { 
  19. public: 
  20.   B() 
  21.   { 
  22.     m_nDataB = 2; 
  23.   } 
  24.   virtual void fun() 
  25.   { 
  26.   } 
  27.   virtual void fun2() 
  28.   { 
  29.   } 
  30.   int m_nDataB; 
  31. }; 
  32.  
  33. int main(int argc, char* argv[]) 
  34. { 
  35.   B the; 
  36.   return 0; 
  37. } 

 

加入了虚函数,构成多个虚表的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

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

 

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nDataA = 1; 
  7.   } 
  8.   int m_nDataA; 
  9. }; 
  10.  
  11. class B :virtual public A 
  12. { 
  13. public: 
  14.   B() 
  15.   { 
  16.     m_nDataB = 2; 
  17.   } 
  18.   int m_nDataB; 
  19. }; 
  20.  
  21. class C :virtual public A 
  22. { 
  23. public: 
  24.   C() 
  25.   { 
  26.     m_nDatac = 3; 
  27.   } 
  28.   int m_nDatac; 
  29. }; 
  30.  
  31. class BC :public B, public C 
  32. { 
  33. public: 
  34.   BC() 
  35.   { 
  36.     m_nDataBC = 4; 
  37.   } 
  38.   int m_nDataBC; 
  39. }; 
  40.  
  41. int main(int argc, char* argv[]) 
  42. { 
  43.   BC the; 
  44.   return 0; 
  45. } 

 

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

B::base offset A

B::m_nDataB

C::base offset A

C::m_nDataC

BC::m_nDataBC

A::m_nDataA 

在变形下.源码如下:

 

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nDataA = 1; 
  7.   } 
  8.   int m_nDataA; 
  9.   virtual void fun(){}  // 新增加 
  10. }; 
  11.  
  12. class B :virtual public A 
  13. { 
  14. public: 
  15.   B() 
  16.   { 
  17.     m_nDataB = 2; 
  18.   } 
  19.   int m_nDataB; 
  20.   virtual void fun(){}  // 新增加 
  21. }; 
  22.  
  23. class C :virtual public A 
  24. { 
  25. public: 
  26.   C() 
  27.   { 
  28.     m_nDatac = 3; 
  29.   } 
  30.   int m_nDatac; 
  31.   virtual void fun(){}  // 新增加 
  32. }; 
  33.  
  34. class BC :public B, public C 
  35. { 
  36. public: 
  37.   BC() 
  38.   { 
  39.     m_nDataBC = 4; 
  40.   } 
  41.   int m_nDataBC; 
  42.   virtual void fun(){}  // 新增加 
  43. }; 
  44.  
  45. int main(int argc, char* argv[]) 
  46. { 
  47.   BC the; 
  48.   return 0; 
  49. } 

 

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

B::base offset A

B::m_nDataB

C::base offset A

C::m_nDataC

0x00000000

BC::vtable

A::m_nDataA 

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

我们继续变形如下:

 

  
 
 
 
  1. class A 
  2. { 
  3. public: 
  4.   A() 
  5.   { 
  6.     m_nDataA = 1; 
  7.   } 
  8.   virtual void fun(){} 
  9.   virtual void funA(){} 
  10.   int m_nDataA; 
  11. }; 
  12.  
  13. class B :virtual public A 
  14. { 
  15. public: 
  16.   B() 
  17.   { 
  18.     m_nDataB = 2; 
  19.   } 
  20.   virtual void fun(){} 
  21.   virtual void funB(){} 
  22.   int m_nDataB; 
  23. }; 
  24.  
  25. class C :virtual public A 
  26. { 
  27. public: 
  28.   C() 
  29.   { 
  30.     m_nDatac = 3; 
  31.   } 
  32.   virtual void fun(){} 
  33.   virtual void funC(){} 
  34.   int m_nDatac; 
  35. }; 
  36.  
  37. class BC :public B, public C 
  38. { 
  39. public: 
  40.   BC() 
  41.   { 
  42.     m_nDataBC = 4; 
  43.   } 
  44.   virtual void fun(){} 
  45.   virtual void funBC(){} 
  46.   int m_nDataBC; 
  47. }; 
  48.  
  49. int main(int argc, char* argv[]) 
  50. { 
  51.   BC the; 
  52.   return 0; 
  53. } 

 

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末尾.

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

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


本文标题:全面剖析逆向还原代码的内存结构
文章来源:http://hnjierui.cn/article/cdieogc.html

其他资讯