如何窥探C++对象内存布局

周末闲在家里无聊,拜读了一遍Lippman的《深度探索C++对象模型》。简单刷完前三章,有种似懂非懂的感觉,毕竟,纸上得来终觉浅,只是简单的灌输知识无法理解深刻。于是决定打开电脑,躬行此事,一探C++对象背后的秘密。

使用GDB探索对象内存布局

以该demo为例,使用GDB工具来探索C++对象在内存中的布局:

#include<iostream>

class father{
  public:
    father();
    virtual void vfunc1();
    virtual void vfunc2();
  public:
    long _f;
};

class child: public father{
  public:
    child();
    virtual void vfunc1();
  public:
    long _c;
};

father::father() {
    _f = 0x888;
    std::cout << "Call father()" << std::endl;
}

void father::vfunc1() {
    std::cout << "Call father::vfunc1()" << std::endl;
}

void father::vfunc2() {
    std::cout << "Call father::vfunc2()" << std::endl;
}
    
child::child() {
    _c = 0x666;
    std::cout << "Call child()" << std::endl;
}

void child::vfunc1() {
    std::cout << "Call child::vfunc1()" << std::endl;
}

int main(){
    father* pc = new child();
    father* pf = new father();
    std::cout << "sizeof father is " << sizeof(father) << std::endl;
    std::cout << "sizeof child is " << sizeof(child) << std::endl;
    return 0;
}

编译指令如下:

g++ ./object_model.cpp -o object -g -std=c++11

使用GDB开始调试可执行文件object:

(gdb) b main
Breakpoint 1 at 0x400c08: file ./object_model.cpp, line 43.
(gdb) r
Starting program: /home/workspace/practice/C++/object_model/object 

Breakpoint 1, main () at ./object_model.cpp:43
43        father* pc = new child();
(gdb) b 47
Breakpoint 2 at 0x400c8c: file ./object_model.cpp, line 47.
(gdb) c
Continuing.
Call father()
Call child()
Call father()
sizeof father is 16
sizeof child is 24

Breakpoint 2, main () at ./object_model.cpp:47
47        return 0;
(gdb) p pc
$1 = (father *) 0x614c20
(gdb) x/10xg pc
0x614c20:    0x0000000000400e38    0x0000000000000888
0x614c30:    0x0000000000000666    0x0000000000000411
0x614c40:    0x6320666f657a6973    0x20736920646c6968
0x614c50:    0x000000000a0a3432    0x0000000000000000
0x614c60:    0x0000000000000000    0x0000000000000000
(gdb) x/10xg pf
0x615050:    0x0000000000400e58    0x0000000000000888
0x615060:    0x0000000000000000    0x000000000001ffa1
0x615070:    0x0000000000000000    0x0000000000000000
0x615080:    0x0000000000000000    0x0000000000000000
0x615090:    0x0000000000000000    0x0000000000000000

既然要研究对象的内存布局,最简单直接的莫过于直接把内存布局打印出来。不过,内存中都是0和1的排序,用不同格式解读会带来不同结果,在本例中father和child的成员都定义为long类型,这是因为在64位环境下,指针(地址)是64bits,因此将成员也定义成64bits,方便在查看内存时统一用64bits为单位去解释内存。

查看指定内存的内容使用GDB的x命令,"x/10xg pf"的意思是**”以pf为起始地址,往后打印10个单位的内容,每个单位占64bits,内容以16进制格式输出“**,x命令具体用法请见https://visualgdb.com/gdbreference/commands/x 。

前面通过sizeof知道了child和father对象的大小分别为24和16Bytes,再结合gdb中打印的地址内容,可以知道,该例中child对象*pc在内存中内容排布为:0x0000000000400e38,0x0000000000000888和0x0000000000000666。后两个数据很明显,0x888和0x666明显是father和child的数据成员,第一个0x400e38又是什么呢?

首先猜一下,0x400e38看着大概率是个地址。可以使用GDB的info symbol address命令查看某个地址对应的符号名:

(gdb) info symbol 0x0000000000400e38
vtable for child + 16 in section .rodata of /home/workspace/practice/C++/object_model/object

GDB告诉我们0x400e38是child虚表往后偏移16个字节的地址,可以使用info vtbl object命令(查看对象的虚表)验证一下:

(gdb) info vtbl *pc
vtable for 'father' @ 0x400e38 (subobject @ 0x614c20):
[0]: 0x400bd2 <child::vfunc1()>
[1]: 0x400b56 <father::vfunc2()>
(gdb) info vtbl *pf
vtable for 'father' @ 0x400e58 (subobject @ 0x615050):
[0]: 0x400b2a <father::vfunc1()>
[1]: 0x400b56 <father::vfunc2()>

可以看到,0x400e38确实指向child虚表(为什么GDB中显示的是vtable for ‘father’ @ 0x400e38? 应该是vtable for ‘child’ @ 0x400e38才对,若有知情者还请不离赐教!!!)。

根据以上信息,可以画出child对象内存布局:
在这里插入图片描述从GDB中”info symbol 0x0000000000400e38“的结果来看,0x400e38只是child vtable的+16bytes偏移地址,那么child vtable的前16bytes是什么内容呢? 下面在GDB中继续探索:

(gdb) x/10xg 0x400e28
0x400e28 <_ZTV5child>:    0x0000000000000000    0x0000000000400e68
0x400e38 <_ZTV5child+16>:    0x0000000000400bd2    0x0000000000400b56
0x400e48 <_ZTV6father>:    0x0000000000000000    0x0000000000400e88
0x400e58 <_ZTV6father+16>:    0x0000000000400b2a    0x0000000000400b56
0x400e68 <_ZTI5child>:    0x0000000000602220    0x0000000000400e80

(gdb) info symbol 0x0000000000400e68
typeinfo for child in section .rodata of /home/workspace/practice/C++/object_model/object

可以看到,child vtable的前16bytes分别为8 Bytes的0和child的typeinfo地址,于是上图可进一步完善:
在这里插入图片描述
child对象的内存布局基本上清晰了。我们还可以再深入一些,前面GDB调试过程中可以看到,类的虚表也是个符号,既然是符号,我们就可以在ELF文件中看到:

harry@HP:~/workspace/practice/C++/object_model$ nm --demangle ./object 
... ... ... ...
0000000000400bd2 T child::vfunc1()
0000000000400b82 T child::child()
0000000000400b82 T child::child()
0000000000400b2a T father::vfunc1()
0000000000400b56 T father::vfunc2()
0000000000400ae6 T father::father()
0000000000400ae6 T father::father()
... ... ... ...
0000000000400e68 V typeinfo for child
0000000000400e88 V typeinfo for father
0000000000400e80 V typeinfo name for child
0000000000400e98 V typeinfo name for father
0000000000400e28 V vtable for child
0000000000400e48 V vtable for father

使用nm命令查看object文件的符号信息,可以看到确实有child和father类的虚表符号,且地址完全能对的上(需要注意的是,如果是在shared library中,需要将偏移加上shared library的base address才能与真实的虚拟地址对上),亦能佐证前面通过GDB推断出来的内存布局。

在代码中获得虚表

前面已经通过GDB了解了C++简单对象的内存布局,下面尝试在代码中通过对象内存布局间接拿到其虚函数地址并访问,以进一步验证前面的结论。

#include<iostream>

class father{
  public:
    father();
    virtual void vfunc1();
    virtual void vfunc2();
  public:
    long _f;
};

class child: public father{
  public:
    child();
    virtual void vfunc1();
  public:
    long _c;
};

father::father() {
    _f = 0x888;
    std::cout << "Call father()" << std::endl;
}

void father::vfunc1() {
    std::cout << "Call father::vfunc1()" << std::endl;
}

void father::vfunc2() {
    std::cout << "Call father::vfunc2()" << std::endl;
}
    
child::child() {
    _c = 0x666;
    std::cout << "Call child()" << std::endl;
}

void child::vfunc1() {
    std::cout << "Call child::vfunc1()" << std::endl;
}

int main(){
    typedef void(*FuncType)(void);
    

    father* pc = new child();
    FuncType** vtbl_ptr = (FuncType**)pc;
    std::cout << "Will call child object's virtual function ... ..." << std::endl;
    // 通过虚表地址间接访问虚表中的虚函数
    for(int i=0; vtbl_ptr[0][i] != nullptr; i++) {
        FuncType vfunc= vtbl_ptr[0][i];
        vfunc();
    }
    // 通过对象首地址获得其内部成员
    std::cout << "child._c = " << std::hex << ((long*)vtbl_ptr)[2] << std::endl;
    std::cout << "father._f = " << std::hex << ((long*)vtbl_ptr)[1] << std::endl;
    
    return 0;
}

结果如下,基本可验证内存布局的正确性:

harry@HP:~/workspace/practice/C++/object_model$ ./object 
Call father()
Call child()
Will call child object's virtual function ... ...
Call child::vfunc1()
Call father::vfunc2()
child._c = 666
father._f = 888

总结

读过《深度探索C++对象模型》一书会发现,本文中通过GDB探究的child对象内存布局与书中的描述略有差异———虚表开头是否有8Bytes的0填充。这一细节其实无所谓对错,因为未在语言层面规定,具体细节视编译器的实现而定。

本文想表达的观点在于:书中描述的结论并不重要,没必要死记硬背。重要的是结论背后的逻辑和依据,以及独立探究并验证结论的能力。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值