C++对象在64位机器上的内存布局

前两天读了陈皓两篇关于虚函数表的博客, 正如他在博客中说的那样, 由于年代久远, 所有的测试代码都是在32位机上跑的, 按照作者的思路, 针对64位机, 我仿写了那些代码, 在移植到64位的过程中碰到了一些坑, 也学到了一些小工具, 现在记录在这里。

1. 如何在GCC环境下得到类在内存中的布局

只要我们在编译的时候加上-fdump-class-hierarchy选项, 就可以在源文件件的同目录下得到一个以.class结尾的文件, 这个文件详细的记载了源文件中的类在内存中的布局, 比如说如果有以下多继承的源代码:

class Base1 {
public:
    int ibase1;
    Base1():ibase1(10) {}
    virtual void f() { cout << "Base1::f()" << endl; }
    virtual void g() { cout << "Base1::g()" << endl; }
    virtual void h() { cout << "Base1::h()" << endl; }

};

class Base2 {
public:
    int ibase2;
    Base2():ibase2(20) {}
    virtual void f() { cout << "Base2::f()" << endl; }
    virtual void g() { cout << "Base2::g()" << endl; }
    virtual void h() { cout << "Base2::h()" << endl; }
};

class Base3 {
public:
    int ibase3;
    Base3():ibase3(30) {}
    virtual void f() { cout << "Base3::f()" << endl; }
    virtual void g() { cout << "Base3::g()" << endl; }
    virtual void h() { cout << "Base3::h()" << endl; }
};

class Derive : public Base1, public Base2, public Base3 {
public:
    int iderive;
    //long iderive1 = 200;
    Derive():iderive(100) {}
    virtual void f() { cout << "Derive::f()" << endl; }
    virtual void g1() { cout << "Derive::g1()" << endl; }
};

如果我们用g++编译的时候加上-fdump-class-hierarchy选项, 然后在生成的.class文件中找到类Derive的虚函数表的信息是这样的:

Vtable for Derive
Derive::_ZTV6Derive: 16u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI6Derive)
16    (int (*)(...))Derive::f
24    (int (*)(...))Base1::g
32    (int (*)(...))Base1::h
40    (int (*)(...))Derive::g1
48    (int (*)(...))-16
56    (int (*)(...))(& _ZTI6Derive)
64    (int (*)(...))Derive::_ZThn16_N6Derive1fEv
72    (int (*)(...))Base2::g
80    (int (*)(...))Base2::h
88    (int (*)(...))-32
96    (int (*)(...))(& _ZTI6Derive)
104   (int (*)(...))Derive::_ZThn32_N6Derive1fEv
112   (int (*)(...))Base3::g
120   (int (*)(...))Base3::h

Class Derive
   size=48 align=8
   base size=48 base align=8
Derive (0x0x7f53708fa4b0) 0
    vptr=((& Derive::_ZTV6Derive) + 16u)
  Base1 (0x0x7f53708794e0) 0
      primary-for Derive (0x0x7f53708fa4b0)
  Base2 (0x0x7f5370879540) 16
      vptr=((& Derive::_ZTV6Derive) + 64u)
  Base3 (0x0x7f53708795a0) 32
      vptr=((& Derive::_ZTV6Derive) + 104u)

这样我们能够大概的看到虚函数表在内存中的布局信息, 美中不足的是这个文件中显示的名字已经是被编译器mangle过的, 我们需要用c++filt这个工具demangle之后显示的信息才会更清晰。
我们可以在命令行键入cat mem_model.cc.002t.class | c++filt, 现在显示的就是一些更加清晰的信息:(我的测试源文件名是mem_model.cc所以生成的.class文件名就是mem_model.cc.002t.class

Vtable for Derive
Derive::vtable for Derive: 16u entries
0     (int (*)(...))0
8     (int (*)(...))(& typeinfo for Derive)
16    (int (*)(...))Derive::f
24    (int (*)(...))Base1::g
32    (int (*)(...))Base1::h
40    (int (*)(...))Derive::g1
48    (int (*)(...))-16
56    (int (*)(...))(& typeinfo for Derive)
64    (int (*)(...))Derive::non-virtual thunk to Derive::f()
72    (int (*)(...))Base2::g
80    (int (*)(...))Base2::h
88    (int (*)(...))-32
96    (int (*)(...))(& typeinfo for Derive)
104   (int (*)(...))Derive::non-virtual thunk to Derive::f()
112   (int (*)(...))Base3::g
120   (int (*)(...))Base3::h

Class Derive
   size=48 align=8
   base size=48 base align=8
Derive (0x0x7f53708fa4b0) 0
    vptr=((& Derive::vtable for Derive) + 16u)
  Base1 (0x0x7f53708794e0) 0
      primary-for Derive (0x0x7f53708fa4b0)
  Base2 (0x0x7f5370879540) 16
      vptr=((& Derive::vtable for Derive) + 64u)
  Base3 (0x0x7f53708795a0) 32
      vptr=((& Derive::vtable for Derive) + 104u)

关于thunk这篇博客写的比较清楚了, 其实它是用来实现多重继承的, 原理也不难,比如说在上面的继承关系中

Base1 *p = new Derive();
p->f();

通过Base1指针调用Derive类的重载函数f(), 因为指针就指向的是Derive对象内存布局中的第一个字节, 所以很容易直接通过虚函数表获得f的地址, 但是如果我们有下面的调用:

Base2 *p = new Derive();
p->f();

这个例子和上面的例子不同的是我们通过继承列表中的第二个对象指针调用派生函数, 那么在第一行的赋值中编译器会自动调整this指针, 我们可以做以下的验证:

Derive *pd = new Derive();
Base1 *pb1 = pd;
Base2 *pb2 = pd;

我们依次输出这三个指针:

0x22ad010
0x22ad010
0x22ad020

可以看到pb2的指针偏移了一个Base1的大小(0x10也就是十进制的16), 但是现在问题来了, 编译器实现类的成员函数的时候都会隐含的加一个形式参数, 指向要调用这个成员函数的对象, 如果我们通过pb2调用f(), 这时候的this指针指向的是Base2对象,这和Derive::f的定义是不相符的。这时候就用到了thunk,编译器再次调整this指针, 让他继续指向Derive对象, 这时候就可以确定调用的就是Derive对象里面实现的那个具体函数了。

400cf4:       48 83 ef 10             sub    $0x10,%rdi
400cf8:       eb 00                   jmp    400cfa 

到时候底层会执行类似上面的汇编代码代码, 这就实现了如何通过Base2指针调用Derive中实现的函数。
2. typedef void(Fun*)(void)是一个通用调用类的成员变量的方法吗

在原博客中作者声明了一个Fun的类型别名(typedef void(Fun*)(void))然后在随后遍历虚函数表的时候使用这个类型强制转换虚函数表中的项, 起到调用具体函数的目的。但我在具体实践的过程中发现这个类型别名的声明不是很好, 在多继承的情况下会产生段错误, 比如说下面的这段代码:

class B
{
public:
    int ib;
    char cb;
    B():ib(0),cb('b') {}

    virtual void f() { cout << "B::f()" << endl;}
    virtual void Bf() { cout << "B::Bf()" << endl;}
};
class B1 : virtual public B
{
public:
    int ib1;
    char cb1;
    B1():ib1(11),cb1('1') {}

    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() {cout << "B1::f1()" << endl;}
    virtual void Bf1() { cout << "B1::Bf1()" << endl;}

};
int main() {
    typedef void(*Fun)(void);
    long** pVtab = NULL;
    Fun pFun = NULL;
    B1 bb1;
    pVtab = (long**)&bb1;
    pFun = (Fun)pVtab[2][0];
    pFun();
}

这段代码在我的编译器上产生了段错误, 其原因很可能就是因为函数指针Fun被声明为无参的, 但他指向的函数是B1::virtual thunk to B1::f()需要一个隐含的指针形参, 如果进入这个函数之后操作了不是按照惯例保存函数参数的寄存器就会产生段错误。针对这个问题可以重新声明Fun的类型为typedef void(Fun*)(void*), 然后每次调用函数指针的时候传入相应的this指针, 这样就不会产生段错误了。
3. 在移植到64位平台的时候最明显的变化就是指针从32位变成了64位, 所以在指针转换的过程中需要改变。
4. 具体的源代码如下

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值