zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    evernote100个做笔记的好方法
    平衡二叉树的调整模版
    晨间日记的奇迹
    hdu 2952 Counting Sheep
    hdu 1535 Invitation Cards
    poj 3259 Wormholes(spfa)
    poj 2263 Heavy Cargo(floyd)
    poj 3268 Silver Cow Party(SPFA)
    hdu 1690 Bus System
    hdu 3631 Shortest Path(Floyd)
  • 原文地址:https://www.cnblogs.com/anzhsoft/p/3602966.html
Copyright © 2011-2022 走看看