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

前两天读了陈皓两篇关于虚函数表的博客, 正如他在博客中说的那样, 由于年代久远, 所有的测试代码都是在32位机上跑的, 按照作者的思路, 针对64位机, 我仿写了那些代码, 在移植到64位的过程中碰到了一些坑, 也学到了一些小工具, 现在记录在这里。 **1.`-fdump-class-hierarchy`选项结合`c++filt`可以得到`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 DeriveDerive::_ZTV6Derive: 16u entries0 (int (*)(...))08 (int (*)(...))(& _ZTI6Derive)16 (int (*)(...))Derive::f24 (int (*)(...))Base1::g32 (int (*)(...))Base1::h40 (int (*)(...))Derive::g148 (int (*)(...))-1656 (int (*)(...))(& _ZTI6Derive)64 (int (*)(...))Derive::_ZThn16_N6Derive1fEv72 (int (*)(...))Base2::g80 (int (*)(...))Base2::h88 (int (*)(...))-3296 (int (*)(...))(& _ZTI6Derive)104 (int (*)(...))Derive::_ZThn32_N6Derive1fEv112 (int (*)(...))Base3::g120 (int (*)(...))Base3::hClass Derive size=48 align=8 base size=48 base align=8Derive (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 DeriveDerive::vtable for Derive: 16u entries0 (int (*)(...))08 (int (*)(...))(& typeinfo for Derive)16 (int (*)(...))Derive::f24 (int (*)(...))Base1::g32 (int (*)(...))Base1::h40 (int (*)(...))Derive::g148 (int (*)(...))-1656 (int (*)(...))(& typeinfo for Derive)64 (int (*)(...))Derive::non-virtual thunk to Derive::f()72 (int (*)(...))Base2::g80 (int (*)(...))Base2::h88 (int (*)(...))-3296 (int (*)(...))(& typeinfo for Derive)104 (int (*)(...))Derive::non-virtual thunk to Derive::f()112 (int (*)(...))Base3::g120 (int (*)(...))Base3::hClass Derive size=48 align=8 base size=48 base align=8Derive (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`[这篇博客](http://web.archive.org/web/20131210001207/http://thomas-sanchez.net/computer-sciences/2011/08/15/what-every-c-programmer-should-know-the-hard-part/)写的比较清楚了, 其实它是用来实现多重继承的, 原理也不难,比如说在上面的继承关系中```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;```我们依次输出这三个指针:```0x22ad0100x22ad0100x22ad020```可以看到`pb2`的指针偏移了一个`Base1`的大小(`0x10`也就是十进制的`16`), 但是现在问题来了, 编译器实现类的成员函数的时候都会隐含的加一个形式参数, 指向要调用这个成员函数的对象, 如果我们通过`pb2`调用`f()`, 这时候的`this`指针指向的是`Base2`对象,这和`Derive::f`的定义是不相符的。这时候就用到了`thunk`,编译器再次调整`this`指针, 让他继续指向`Derive`对象, 这时候就可以确定调用的就是`Derive`对象里面实现的那个具体函数了。```400cf4: 48 83 ef 10 sub $0x10,%rdi400cf8: eb 00 jmp 400cfa ```到时候底层会执行类似上面的汇编代码代码, 这就实现了如何通过`Base2`指针调用`Derive`中实现的函数。 - 在原博客中作者声明了一个`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位, 所以在指针转换的过程中需要改变。 - [多重继承](https://github.com/cincat/vtable-model/blob/master/mem_model1.cc) - [重复继承](https://github.com/cincat/vtable-model/blob/master/mem_model2.cc) - [单一虚拟继承](https://github.com/cincat/vtable-model/blob/master/mem_model3.cc) - [钻石型虚拟多重继承](https://github.com/cincat/vtable-model/blob/master/mem_model4.cc)

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

1.-fdump-class-hierarchy选项结合c++filt可以得到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; }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

如果我们用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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

这样我们能够大概的看到虚函数表在内存中的布局信息, 美中不足的是这个文件中显示的名字已经是被编译器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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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

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

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

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

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

Derive *pd = new Derive();
Base1 *pb1 = pd;
Base2 *pb2 = pd;
  • 1
  • 2
  • 3

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

0x22ad010
0x22ad010
0x22ad020
  • 1
  • 2
  • 3

可以看到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 
  • 1
  • 2

到时候底层会执行类似上面的汇编代码代码, 这就实现了如何通过Base2指针调用Derive中实现的函数。

  • 在原博客中作者声明了一个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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

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

原文地址:https://www.cnblogs.com/techMe/p/8259156.html

时间: 2024-11-06 05:46:43

C++对象在64位机器上的内存布局的相关文章

关于LogStash运行在AIX 64位机器上的问题与临时解决方案

需求;logstash运行在SUSE,LINUX,PPC LINUX,AIX机器上,并监控文件发送日志到KAFKA中去, 问题:在AIX机器上,file插件总是报异常,无法完成数据的读取 NotImplementedError: stat.st_dev unsupported or native support failed to load 分析:环境 :AIX 64 OSLEVEL :6.1.0 7.1.0 JDK : IBM JAVA 71 64 报错显示:是在获取设备的主辅号时出了问题,显

32位程序在64位系统上获取系统安装时间(要使用KEY_WOW64_64KEY标记)

众所周知,取系统的安装时间可取注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion的子项InstallDate,此值是个DWORD类型的UnixStamp.  但是在64位系统上有所不同(仅测试了win7.win8),默认情况下32程序在64位机器上访问的是下面这个地址HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion

【转载】GetAdaptersInfo函数在64位系统上返回ERROR_NOACCESS的有关问题

From:http://www.educity.cn/wenda/351190.html GetAdaptersInfo函数在64位系统下返回ERROR_NOACCESS的问题 实际应用中一个程序在长时间运行后内存占用较高时发生崩溃,从dump信息中,发现GetAdaptersInfo函数返回了一个奇怪的错误码998(ERROR_NOACCESS),百度搜索不到相关的信息.MSDN上GetAdaptersInfo函数的错误码正常情况下只有5种.并且一共发生的两次崩溃都出现在一台Win7 64位机

windows Server 2008 64位机器装了一个10g客户端,Oracle数据库连接不上问题解决。

windows Server 2008 64位机器装了一个10g客户端,32和64位不清楚 用Netmanager里面的服务名连.Data Source=o10ga;user id=DJZD;password=cy2015;直接显示Tns12514错误,库连接不上... 若改用 Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=10.72.5.13) (PORT=1521))(ADDRESS=(PROTOCOL

【转】将 Linux 应用程序移植到 64 位系统上

原文网址:http://www.ibm.com/developerworks/cn/linux/l-port64.html 随着 64 位体系结构的普及,针对 64 位系统准备好您的 Linux® 软件已经变得比以前更为重要.在本文中,您将学习如何在进行语句声明.赋值.位移.类型转换.字符串格式化以及更多操作时,防止出现可移植性缺陷. 0 评论: Harsha S. Adiga, 软件工程师, IBM 2006 年 5 月 18 日 内容 在 IBM Bluemix 云平台上开发并部署您的下一个

在64位linux上编译32位程序

ld指令有一个选项:--oformat output_format,用于指定输出文件的格式.输入文件./kernel/kernel.o等是elf32格式,当前系统是64位,而ld默认生成的文件格式是elf64-x86-64:因此会出现"ld: warning: i386 architecture of input file `./kernel/kernel.o' is incompatible with i386:x86-64 output"这样的提示.之前,将系统从三墩转移到我自己的

[单选题]64位系统上,定义的变量int *a[2][3]占据的——字节

4 12 24 48 正确答案: 很遗憾,没答对,再接再厉! 答案解析 在64位系统上,一个指针占8个字节.在32位系统上,一个指针占4个字节.注意无论在32位还是在64位系统占,int均为4个字节.

64位ubuntu上安装 hadoop-2.4.0

完全参考:http://blog.csdn.net/cruise_h/article/details/18709969 这上面的安装教程 伪分布配置: http://my.oschina.net/mynote/blog/93735 64位ubuntu上安装 hadoop-2.4.0,布布扣,bubuko.com

PL/SQL跑在Oracle 64位数据库上初始化错误

安装完Oracle(64位).PL/SQL后运行PL/SQL出现如下的错误: 网上查资料说,我的PL/SQL与ORACLE不兼容,即PL/SQL不支持64位的ORACLE,因此得下一个32位的ORCALE客户端并配置相应的参数: 解决步骤小记: 一.下载ORACLE 32位客户端 下载地址:http://www.onlinedown.net/soft/102902.htm(Oracle 10g客户端精简绿色版) 二.解压到ORACLE 安装目录下一个叫product的目录下,并重命名一下(命名不