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、带有虚函数的单一继承

class Parent
{
public:
  Parent():numInParent(1111)
  {}
  virtual void Foo(){
  };
  virtual void Boo(){
  };
private:
  int numInParent;
};

class Child: public Parent
{
public:
  Child():numInChild(2222){}
  virtual void Foo(){
  }
  int numInChild;
};

编译时不要忘记-g,使得gdb可以把各个地址映射成函数名。

(gdb) set p obj on
(gdb) p *this
$2 = (Child) {<Parent> = {_vptr.Parent = 0x400a30, numInParent = 1111}, numInChild = 2222}
(gdb) set p pretty on
(gdb) p *this
$3 = (Child) {
  <Parent> = {
    _vptr.Parent = 0x400a30,
    numInParent = 1111
  },
  members of Child:
  numInChild = 2222
}
(gdb)  p /a (*(void ***)this)[0]@3
$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:

class Point2d{
public:
  virtual void Foo(){}
  virtual void Boo(){}
  virtual void non_overwrite(){}
protected:
  float _x, _y;
};

class Vertex: public  Point2d{
public:
  virtual void Foo(){}
  virtual void BooVer(){}
protected:
  Vertex *next;
};

class Point3d: public Point2d{
public:
  virtual void Boo3d(){}
protected:
  float _z;
};

class Vertex3d: public Vertex, public Point3d{
public:
  void test(){}
protected:
  float mumble;
};
使用GDB打印的对象内存布局:

 <Vertex> = {
    <Point2d> = {
      _vptr.Point2d = 0x400ab0,
      _x = 5.88090213e-39,
      _y = 0
    },
    members of Vertex:
    next = 0x0
  },
  <Point3d> = {
    <Point2d> = {
      _vptr.Point2d = 0x400ae0,
      _x = -nan(0x7fe180),
      _y = 4.59163468e-41
    },
    members of Point3d:
    _z = 0
  },
  members of Vertex3d:
  mumble = 0
}
可见v3d有两个vptr,指向不同的vtable。首先看一下第一个:


(gdb) p /a (*(void ***)this)[0]@5
$9 =   {0x4008be <Vertex::Foo()>,
  0x4008aa <Point2d::Boo()>,
  0x4008b4 <Point2d::non_overwrite()>,
  0x4008c8 <Vertex::BooVer()>,
  0xffffffffffffffe8}
(gdb) p /a (*(void ***)this)[0]@6
$10 =   {0x4008be <Vertex::Foo()>,
  0x4008aa <Point2d::Boo()>,
  0x4008b4 <Point2d::non_overwrite()>,
  0x4008c8 <Vertex::BooVer()>,
  0xffffffffffffffe8,
  0x400b00 <_ZTI8Vertex3d>}
(gdb) info addr _ZTI8Vertex3d
Symbol "typeinfo for Vertex3d" is at 0x400b00 in a file compiled without debugging.

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

注意到该虚函数表以

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

接着看第二个vtable:

(gdb) p /a (*(void ***)this)[1]@5
$11 =   {0x4008b2 <Point2d::Boo()>,
  0x4008bc <Point2d::non_overwrite()>,
  0x4008d0 <Vertex::BooVer()>,
  0xffffffffffffffe8,
  0x400b00 <_ZTI8Vertex3d>}
(gdb) info addr _ZTI8Vertex3d
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:

class Point2d{
public:
  virtual void Foo(){}
  virtual void Boo(){}
  virtual void non_overwrite(){}
protected:
  float _x, _y;
};

class Vertex: public virtual  Point2d{
public:
  virtual void Foo(){}
  virtual void BooVer(){}
protected:
  Vertex *next;
};

class Point3d: public virtual Point2d{
public:
  virtual void Boo3d(){}
protected:
  float _z;
};

class Vertex3d: public Vertex, public Point3d{
public:
  void test(){}
protected:
  float mumble;
};
继承关系图:


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

(gdb) p *this
$10 = (Vertex3d) {
  <Vertex> = {
    <Point2d> = {
      _vptr.Point2d = 0x400b70,
      _x = 0,
      _y = 0
    },
    members of Vertex:
    _vptr.Vertex = 0x400b18,
    next = 0x4009c0
  },
  <Point3d> = {
    members of Point3d:
    _vptr.Point3d = 0x400b40,
    _z = 5.87993804e-39
  },
  members of Vertex3d:
  mumble =  0
}

gdb打印的vptr相关:

(gdb) p /a (*(void ***)this)[0]@60
$25 =   {0x400870 <Vertex::Foo()>,
  0x40087a <Vertex::BooVer()>,
  0x10,
  0xfffffffffffffff0,
  0x400c80 <_ZTI8Vertex3d>, #"typeinfo for Vertex3d"
  0x400884 <Point3d::Boo3d()>,
  0x0,
  0x0,
  0xffffffffffffffe0,
  0xffffffffffffffe0, 
  0x400c80 <_ZTI8Vertex3d>, #"typeinfo for Vertex3d"
  0x400866 <_ZTv0_n24_N6Vertex3FooEv>, #"virtual thunk to Vertex::Foo()"
  0x400852 <Point2d::Boo()>,
  0x40085c <Point2d::non_overwrite()>,
  0x0,
  0x0,
  0x0,
  0x20,
  0x0,
  0x400cc0 <_ZTI6Vertex>, #"typeinfo for Vertex"
  0x400870 <Vertex::Foo()>,
  0x40087a <Vertex::BooVer()>,
  0x0,
  0x0,
  0xffffffffffffffe0,
  0xffffffffffffffe0,
  0x400cc0 <_ZTI6Vertex>, #"typeinfo for Vertex"
  0x400866 <_ZTv0_n24_N6Vertex3FooEv>, #"virtual thunk to Vertex::Foo()"
  0x400852 <Point2d::Boo()>,
  0x40085c <Point2d::non_overwrite()>,
  0x0,
  0x0,
  0x0,
  0x10,
  0x0,
  0x400d00 <_ZTI7Point3d>, #"typeinfo for Point3d"
  0x400884 <Point3d::Boo3d()>,
  0x0,
  0x0,
  0x0,
  0xfffffffffffffff0,
  0x400d00 <_ZTI7Point3d>, #"typeinfo for Point3d"
  0x400848 <Point2d::Foo()>,
  0x400852 <Point2d::Boo()>,
  0x40085c <Point2d::non_overwrite()>,
  0x6020b0 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1.3+16>,
  0x400d28 <_ZTS8Vertex3d>,
  0x200000002,
  0x400cc0 <_ZTI6Vertex>, #"typeinfo for Vertex"
  0x2,
  0x400d00 <_ZTI7Point3d>, #"typeinfo for Point3d"
  0x1002,
  0x0,
  0x6020b0 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1.3+16>,
  0x400d32 <_ZTS6Vertex>,
  0x100000000,
  0x400d40 <_ZTI7Point2d>,
  0xffffffffffffe803,
  0x0,
  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

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
8天前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
39 0
|
8天前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
30 1
Linux C/C++之IO多路复用(aio)
|
3天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
15 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
3天前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
8 2
|
4天前
|
Ubuntu Linux 编译器
Linux/Ubuntu下使用VS Code配置C/C++项目环境调用OpenCV
通过以上步骤,您已经成功在Ubuntu系统下的VS Code中配置了C/C++项目环境,并能够调用OpenCV库进行开发。请确保每一步都按照您的系统实际情况进行适当调整。
43 3
|
3天前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
13 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
8天前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
12 0
Linux C/C++之线程基础
|
8天前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
13 0
Linux C/C++之IO多路复用(poll,epoll)
|
8天前
|
网络协议 Linux 网络性能优化
Linux C/C++之TCP / UDP通信
这篇文章详细介绍了Linux下C/C++语言实现TCP和UDP通信的方法,包括网络基础、通信模型、编程示例以及TCP和UDP的优缺点比较。
17 0
Linux C/C++之TCP / UDP通信
|
8天前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
13 0
Linux c/c++之IPC进程间通信