Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)

前一段时间再次拜读《Inside the C++ Object Model》 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记:

Program Transformation Semantics (程序转换语义学)

The Semantics of Copy Constructors(拷贝构造函数之编译背后的行为)

The Semantics of Constructors: The Default Constructor (默认构造函数什么时候会被创建出来)

The Semantics of Data: Data语义学 深入探索C++对象模型

     这些文章都获得了很大的浏览量,虽然类似的博文原来都有,可能不容易被现在仍活跃在CSDN Blog的各位同仁看到吧。因此萌生了接着将这这本书读完的同时,再接着谈一下我的理解,或者说读书笔记。

     关于C++虚函数,很多博文从各个角度来探究虚函数是如何实现的,或者说编译器是如何实现虚函数的。比较经典的文章有陈皓先生的《C++虚函数表解析》和《C++对象内存布局》。本文通过GDB来从另外一个角度来理解C++ object的内存布局,一来熟悉语言背后编译器为了实现语言特性为我们做了什么;二来熟悉使用GDB来调试程序。

      同时,本文也将对如何更好的理解C++语言提供了一个方法:使用GDB,可以很直观的理解编译器的实现,从根本上掌握C++!我们不单单只会开车,还应该知道车的内部的构造。

2、带有虚函数的单一继承


   
   
  1. class Parent
  2. {
  3. public:
  4.   Parent():numInParent( 1111)
  5.   {}
  6.   virtual void Foo(){
  7.   };
  8.   virtual void Boo(){
  9.   };
  10. private:
  11.   int numInParent;
  12. };
  13. class Child: public Parent
  14. {
  15. public:
  16.   Child():numInChild( 2222){}
  17.   virtual void Foo(){
  18.   }
  19.   int numInChild;
  20. };
编译时不要忘记-g,使得gdb可以把各个地址映射成函数名。


   
   
  1. (gdb) set p obj on
  2. (gdb) p * this
  3. $2 = (Child) {&lt;Parent&gt; = {_vptr.Parent = <span class="hljs-number">0x400a30</span>, numInParent = <span class="hljs-number">1111</span>}, numInChild = <span class="hljs-number">2222</span>}</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="4"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">(gdb) <span class="hljs-built_in">set</span> p pretty on</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="5"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">(gdb) p *<span class="hljs-keyword">this</span></div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="6"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">$ 3 = (Child) {
  4. <Parent> = {
  5. _vptr.Parent = 0x400a30,
  6. numInParent = 1111
  7. },
  8. members of Child:
  9. numInChild = 2222
  10. }
  11. (gdb) p /a (*( void ***) this)[ 0]@ 3
  12. $ 4 = { 0x4008ec <Child::Foo()>, 0x4008b4 <Parent::Boo()>, 0x6010b0 <_ZTVN10__cxxabiv120__si_class_type_infoE@@CXXABI_1 .3+ 16>}
解释一下gdb的命令:

set p obj <on/off>: 在C++中,如果一个对象指针指向其派生类,如果打开这个选项,GDB会自动按照虚方法调用的规则显示输出,如果关闭这个选项的话,GDB就不管虚函数表了。这个选项默认是off。 使用show print object查看对象选项的设置。

set p pertty <on/off>: 按照层次打印结构体。可以从设置前后看到这个区别。on的确更容易阅读。

p /a (*(void ***)this)[0]@3
   
   
就是打印虚函数表了。因为知道是两个,可以仅仅打印2个元素。为了知道下一个存储了什么信息,我们打印了3个值。实际上后几个元素存储了Parent 和Child的typeinfo name和typeinfo。

总结:

对于单一继承,

1. vptr存储到了object的开始。

2. 在vptr之后,从Parent开始的data member按照声明顺序依次存储。

3. 多重继承,包含有相同的父类

对应的C++codes:


   
   
  1. class Point2d{
  2. public:
  3. virtual void Foo(){}
  4. virtual void Boo(){}
  5. virtual void non_overwrite(){}
  6. protected:
  7. float _x, _y;
  8. };
  9. class Vertex: public Point2d{
  10. public:
  11. virtual void Foo(){}
  12. virtual void BooVer(){}
  13. protected:
  14. Vertex *next;
  15. };
  16. class Point3d: public Point2d{
  17. public:
  18. virtual void Boo3d(){}
  19. protected:
  20. float _z;
  21. };
  22. class Vertex3d: public Vertex, public Point3d{
  23. public:
  24. void test(){}
  25. protected:
  26. float mumble;
  27. };
使用GDB打印的对象内存布局:


   
   
  1. <Vertex> = {
  2. <Point2d> = {
  3. _vptr.Point2d = 0x400ab0,
  4. _x = 5.88090213e-39,
  5. _y = 0
  6. },
  7. members of Vertex:
  8. next = 0x0
  9. },
  10. <Point3d> = {
  11. <Point2d> = {
  12. _vptr.Point2d = 0x400ae0,
  13. _x = -nan( 0x7fe180),
  14. _y = 4.59163468e-41
  15. },
  16. members of Point3d:
  17. _z = 0
  18. },
  19. members of Vertex3d:
  20. mumble = 0
  21. }
可见v3d有两个vptr,指向不同的vtable。首先看一下第一个:



   
   
  1. (gdb) p /a (*( void ***) this)[ 0]@ 5
  2. $<span class="hljs-number">9</span> = {<span class="hljs-number">0x4008be</span> &lt;Vertex::Foo()&gt;,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="3"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> <span class="hljs-number">0x4008aa</span> &lt;Point2d::Boo()&gt;,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="4"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> <span class="hljs-number">0x4008b4</span> &lt;Point2d::non_overwrite()&gt;,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="5"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> <span class="hljs-number">0x4008c8</span> &lt;Vertex::BooVer()&gt;,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="6"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> <span class="hljs-number">0xffffffffffffffe8</span>}</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="7"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">(gdb) p /a (*(<span class="hljs-keyword">void</span> ***)<span class="hljs-keyword">this</span>)[<span class="hljs-number">0</span>]@<span class="hljs-number">6</span></div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="8"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">$ 10 = { 0x4008be <Vertex::Foo()>,
  3. 0x4008aa <Point2d::Boo()>,
  4. 0x4008b4 <Point2d::non_overwrite()>,
  5. 0x4008c8 <Vertex::BooVer()>,
  6. 0xffffffffffffffe8,
  7. 0x400b00 <_ZTI8Vertex3d>}
  8. (gdb) info addr _ZTI8Vertex3d
  9. Symbol "typeinfo for Vertex3d" is at 0x400b00 in a file compiled without debugging.

你可以注意到了,vtable打印分行了,可以使用 set p array on将打印的数组分行,以逗号结尾。

注意到该虚函数表以

0xffffffffffffffe8
   
   
结尾。在单一继承中是没有这个结束标识的。

接着看第二个vtable:


   
   
  1. (gdb) p /a (*( void ***) this)[ 1]@ 5
  2. $ 11 = { 0x4008b2 <Point2d::Boo()>,
  3. 0x4008bc <Point2d::non_overwrite()>,
  4. 0x4008d0 <Vertex::BooVer()>,
  5. 0xffffffffffffffe8,
  6. 0x400b00 <_ZTI8Vertex3d>}
  7. (gdb) info addr _ZTI8Vertex3d
  8. Symbol "typeinfo for Vertex3d" is at 0x400b00 in a file compiled without debugging.

当然这个只是为了举个例子。现实中很少有人这么干吧。比如访问Foo,下面的code将会导致歧义性错误:

v3d.Boo();
   
   
 error: request for member Boo is ambiguous
multiInheritance.cpp:8: error: candidates are: virtual void Point2d::Boo()
只能指定具体的subobject才能进行具体调用:

v3d.::Vertex::Boo();
   
   

4. 虚拟继承

C++ codes:


   
   
  1. class Point2d{
  2. public:
  3. virtual void Foo(){}
  4. virtual void Boo(){}
  5. virtual void non_overwrite(){}
  6. protected:
  7. float _x, _y;
  8. };
  9. class Vertex: public virtual Point2d{
  10. public:
  11. virtual void Foo(){}
  12. virtual void BooVer(){}
  13. protected:
  14. Vertex *next;
  15. };
  16. class Point3d: public virtual Point2d{
  17. public:
  18. virtual void Boo3d(){}
  19. protected:
  20. float _z;
  21. };
  22. class Vertex3d: public Vertex, public Point3d{
  23. public:
  24. void test(){}
  25. protected:
  26. float mumble;
  27. };
继承关系图:


使用gdb打印object的内存布局:


   
   
  1. (gdb) p * this
  2. $ 10 = (Vertex3d) {
  3. <Vertex> = {
  4. <Point2d> = {
  5. _vptr.Point2d = 0x400b70,
  6. _x = 0,
  7. _y = 0
  8. },
  9. members of Vertex:
  10. _vptr.Vertex = 0x400b18,
  11. next = 0x4009c0
  12. },
  13. <Point3d> = {
  14. members of Point3d:
  15. _vptr.Point3d = 0x400b40,
  16. _z = 5.87993804e-39
  17. },
  18. members of Vertex3d:
  19. mumble =   0
  20. }

gdb打印的vptr相关:


   
   
  1. (gdb) p /a (*( void ***) this)[ 0]@ 60
  2. $ 25 = { 0x400870 <Vertex::Foo()>,
  3. 0x40087a <Vertex::BooVer()>,
  4. 0x10,
  5. 0xfffffffffffffff0,
  6. 0x400c80 <_ZTI8Vertex3d>, # "typeinfo for Vertex3d"
  7. 0x400884 <Point3d::Boo3d()>,
  8. 0x0,
  9. 0x0,
  10. 0xffffffffffffffe0,
  11. 0xffffffffffffffe0,
  12. 0x400c80 <_ZTI8Vertex3d>, # "typeinfo for Vertex3d"
  13. 0x400866 <_ZTv0_n24_N6Vertex3FooEv>, # "virtual thunk to Vertex::Foo()"
  14. 0x400852 <Point2d::Boo()>,
  15. 0x40085c <Point2d::non_overwrite()>,
  16. 0x0,
  17. 0x0,
  18. 0x0,
  19. 0x20,
  20. 0x0,
  21. 0x400cc0 <_ZTI6Vertex>, # "typeinfo for Vertex"
  22. 0x400870 <Vertex::Foo()>,
  23. 0x40087a <Vertex::BooVer()>,
  24. 0x0,
  25. 0x0,
  26. 0xffffffffffffffe0,
  27. 0xffffffffffffffe0,
  28. 0x400cc0 <_ZTI6Vertex>, # "typeinfo for Vertex"
  29. 0x400866 <_ZTv0_n24_N6Vertex3FooEv>, # "virtual thunk to Vertex::Foo()"
  30. 0x400852 <Point2d::Boo()>,
  31. 0x40085c <Point2d::non_overwrite()>,
  32. 0x0,
  33. 0x0,
  34. 0x0,
  35. 0x10,
  36. 0x0,
  37. 0x400d00 <_ZTI7Point3d>, # "typeinfo for Point3d"
  38. 0x400884 <Point3d::Boo3d()>,
  39. 0x0,
  40. 0x0,
  41. 0x0,
  42. 0xfffffffffffffff0,
  43. 0x400d00 <_ZTI7Point3d>, # "typeinfo for Point3d"
  44. 0x400848 <Point2d::Foo()>,
  45. 0x400852 <Point2d::Boo()>,
  46. 0x40085c <Point2d::non_overwrite()>,
  47. 0x6020b0 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1 .3+ 16>,
  48. 0x400d28 <_ZTS8Vertex3d>,
  49. 0x200000002,
  50. 0x400cc0 <_ZTI6Vertex>, # "typeinfo for Vertex"
  51. 0x2,
  52. 0x400d00 <_ZTI7Point3d>, # "typeinfo for Point3d"
  53. 0x1002,
  54. 0x0,
  55. 0x6020b0 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1 .3+ 16>,
  56. 0x400d32 <_ZTS6Vertex>,
  57. 0x100000000,
  58. 0x400d40 <_ZTI7Point2d>,
  59. 0xffffffffffffe803,
  60. 0x0,
  61. 0x0}

有兴趣的话可以看一下反汇编的vtable的构成。

参考:

1. http://stackoverflow.com/questions/6191678/print-c-vtables-using-gdb

2. http://stackoverflow.com/questions/18363899/how-to-display-a-vtable-by-name-using-gdb


尊重原创,转载请注明出处: anzhsoft http://blog.csdn.net/anzhsoft/article/details/18600163

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
调试是在编程过程中解决错误和问题的重要步骤。GNU调试器(GDB)是一种功能强大的调试工具,它允许我们在源代码级别上进行调试。 首先,为了使用GDB进行源代码级别调试,我们需要安装GDB。我们可以通过访问GNU调试器项目的官方网站,从那里下载并安装GDB软件包。 一旦安装完成,我们可以在终端中使用gdb”命令来启动GDB调试器。然后,通过在命令行中指定可执行文件的路径,我们可以将程序加载到GDB中。 在源代码级别调试中,我们可以使用GDB的一些命令来执行不同的操作。例如,我们可以使用“break”命令设置断点,以便在程序执行时停止。我们还可以使用“run”命令开始执行程序,直到遇到断点为止。 一旦程序停止在断点处,我们可以使用GDB的其他命令来查看和修改程序状态。例如,我们可以使用“print”命令显示变量的值,以帮助我们理解程序的行为。我们还可以使用“step”命令逐行执行程序,并在每一步检查变量和代码的状态。 除了这些基本命令外,GDB还提供了许多其他功能,如条件断点、查看内存内容、查看函数调用栈等等。这些功能可以帮助我们更全面地理解程序的执行过程和错误的来源。 总之,GDB是一个强大的源代码级别调试工具,可以帮助我们解决编程过程中的错误和问题。通过使用GDB,我们可以在程序执行过程中查看和修改变量和代码的状态,以便更好地理解程序的行为和调试错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值