ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

vptr(虚表指针) 和 vtable(虚表)

vptr(虚表指针) 和 vtable(虚表) 1.1 什么是 vptr 和 vtableC 通过虚函数表vtable实现运行时多态。当一个类含有虚函数时编译器会为这个类生成一张虚表vtable本质是一个函数指针数组里面存放该类所有虚函数的地址。在每个对象的内存布局最前面插入一个隐藏指针vptr指向该类的 vtable。也就是说一个含虚函数的对象内存布局大致是------------------- | vptr | - 8 字节64 位系统指向 vtable ------------------- | 成员变量 1 | ------------------- | 成员变量 2 | ------------------- | ... | -------------------1.2 关键区别概念位置说明vptr在对象内存里每个对象一份指向所属类的 vtablevtable在只读数据段.rodata每个类一份被所有对象共享不同编译器如 MSVC的 vtable 布局可能不同但核心思想一致。64 位系统下指针是 8 字节vptr 占 8 字节。二、实验代码#include iostream class Base { public: Base() : a(0x11) {} virtual ~Base() {} virtual void func1() { std::cout Base::func1\n; } virtual void func2() { std::cout Base::func2\n; } virtual int func3(int x) { return x 1; } int a; }; class Derived : public Base { public: Derived() : b(0x22) {} ~Derived() override {} void func1() override { std::cout Derived::func1\n; } void func2() override { std::cout Derived::func2\n; } virtual void func4() { std::cout Derived::func4\n; } int b; }; int main() { Base base; Derived derived; Base* p1 base; Base* p2 derived; p1-func1(); p2-func1(); return 0; }编译时必须加-g保留调试信息并关闭优化避免布局被改变g -g -O0 -o vtable_demo vtable_demo.cpp //-g 生成调试信息供 gdb 等调试器使用 //-O0 关闭所有优化这是默认级别显式写出是为了强调优化级别对比级别含义编译速度运行性能调试友好度-O0不优化默认最快最差最好-O1基础优化快中等较好-O2中等偏强优化中等好一般-O3激进优化慢最好多数情况差-Os优化代码体积中等较好一般-Ofast超越-O3可能违反标准慢最高差三、用 GDB 观察 vptr3.1 启动 GDB 并设置断点gdb ./vtable_demo在 GDB 里执行(gdb) break main (gdb) run程序停在main开头。设置断点的基本语法break 位置 # 简写 b //当前文件的某一行 (gdb) b 42 //指定文件的某一行 (gdb) b vptr.cpp:42 //如果文件不在当前目录GDB 可能找不到需要用相对或绝对路径 (gdb) b /home/user/project/vptr.cpp:42 //普通函数 (gdb) b main (gdb) b func1 //类成员函数 (gdb) b Derived::func1 (gdb) b Base::func1 //带参数的函数名 (gdb) b Derived::func1(int) (gdb) b Derived::func1(const std::string) //指定文件里的函数 (gdb) b vptr.cpp:Derived::func13.2 单步到对象构造完成(gdb) next # 执行 Base base; (gdb) next # 执行 Derived derived;现在base和derived都已构造完成vptr 已经被设置。3.3 查看对象的内存布局查看 base 对象(gdb) print base输出类似$1 {_vptr.Base 0x555555557d30 vtable for Base16, a 17}(gdb) print derived输出类似$2 {Base {_vptr.Base 0x555555557d10 vtable for Derived16, a 17}, b 34}可以看到derived的 vptr 指向Derived的 vtable地址不同。16是什么意思这是 GDB 的友好提示vtable 数组前面还有 2 个 8 字节的元数据offset-to-top 和 typeinfo 指针所以 vptr 实际指向的是 vtable 的第 3 个槽位偏移 16 字节。后面会详细讲。3.4 直接查看对象内存的原始字节上面的print是 GDB 用调试信息“美化”后的结果。要看真实内存用x命令(gdb) x/4gx base输出类似0x7fffffffe2d0: 0x0000555555557d30 0x0000000000000011 0x7fffffffe2e0: 0x0000000000000000 0x0000000000000000解读第 1 个 8 字节0x0000555555557d30vptr指向 Base 的 vtable。第 2 个 8 字节0x0000000000000011成员a 0x11 17。(gdb) x/4gx derived输出类似0x7fffffffe2c0: 0x0000555555557d10 0x0000000000000011 0x7fffffffe2d0: 0x0000000000000022 0x0000000000000000解读第 1 个 8 字节0x0000555555557d10vptr指向 Derived 的 vtable。第 2 个 8 字节0x0000000000000011基类成员a 0x11。第 3 个 8 字节0x0000000000000022派生类成员b 0x22。注意Derived对象的成员a和b是连续存放的vptr 在最前面。四、深入观察 vtable 的内容4.1 找到 vtable 的起始位置前面看到base的 vptr 是0x555555557d30GDB 提示它等于vtable for Base16。这意味着 vtable 的真正起始地址是0x555555557d30 - 16 0x555555557d20用 GDB 查看这个地址开始的内存(gdb) x/8gx 0x555555557d20输出类似0x555555557d20: 0x0000000000000000 0x0000555555557c80 0x555555557d30: 0x0000555555555b0a 0x0000555555555b2c 0x555555557d40: 0x0000555555555b4e 0x0000555555555b70 0x555555557d50: 0x0000555555555b92 0x00000000000000004.2 解析 vtable 结构对于 GCC 的 Itanium C ABIvtable 的布局是--------------------------- | offset-to-top (8 字节) | - vtable 起始 --------------------------- | typeinfo 指针 (8 字节) | - 指向 RTTI 信息 --------------------------- | 虚函数 1 地址 (8 字节) | - vptr 指向这里 --------------------------- | 虚函数 2 地址 (8 字节) | --------------------------- | ... | ---------------------------所以上面的内存解读是地址值含义0x555555557d200x0offset-to-top 0单继承无偏移0x555555557d280x555555557c80typeinfo 指针指向typeinfo for Base0x555555557d300x555555555b0a虚函数 1 地址vptr 指向这里0x555555557d380x555555555b2c虚函数 2 地址0x555555557d400x555555555b4e虚函数 3 地址0x555555557d480x555555555b70虚函数 4 地址析构相关4.3 把地址翻译成函数名用info symbol把地址翻译成函数名(gdb) info symbol 0x555555555b0a输出类似Base::func1() in section .text依次查看(gdb) info symbol 0x555555555b2c (gdb) info symbol 0x555555555b4e (gdb) info symbol 0x555555555b70你会看到类似Base::func2() in section .text Base::func3(int) in section .text Base::~Base() in section .text这证明了vtable 里按声明顺序存放了虚函数的地址。4.4 对 Derived 的 vtable 做同样操作(gdb) x/8gx 0x555555557d00 (gdb) info symbol 0x555555555b0a # 应该看到 Derived::func1你会看到Derived的 vtable 里func1和func2指向的是Derived的版本地址不同这就是虚函数覆盖override在内存层面的体现。五、观察运行时多态的真实过程5.1 在多态调用处下断点(gdb) break vtable_demo.cpp:39 (gdb) continue或者直接在p2-func1()那行下断点。5.2 查看调用时的 vptr当程序停在p2-func1()时(gdb) print p2 (gdb) print *p2你会看到p2是Base*类型但*p2的 vptr 指向的是Derived的 vtable。这就是动态绑定的关键编译器生成的代码是(*p2-vptr[0])(p2)即通过 vptr 找到 vtable再取第 0 个函数指针调用。由于p2实际指向Derived对象vptr 指向Derived的 vtable所以调用的是Derived::func1。5.3 反汇编验证(gdb) disassemble /m main你会看到类似mov -0x18(%rbp), %rax ; 把 p2 加载到 rax mov (%rax), %rax ; 取 vptrrax 指向对象首地址 mov (%rax), %rax ; 取 vtable[0]func1 地址 mov -0x18(%rbp), %rdi ; 把 p2 作为 this 参数 call *%rax ; 间接调用这就是多态的底层实现两次解引用vptr → vtable → 函数。六、进阶实验多重继承的 vptr如果类有多重继承对象里会有多个 vptr。修改代码class A { public: virtual void fa() {} int a; }; class B { public: virtual void fb() {} int b; }; class C : public A, public B { public: void fa() override {} void fb() override {} int c; };用 GDB 查看C对象(gdb) x/8gx c你会看到两个 vptr分别指向C的两个 vtable主 vtable 和次 vtable。这是多重继承下对象布局的典型特征。七、常见坑与注意事项坑说明优化等级-O2会打乱布局必须用-O0调试信息必须加-g否则 GDB 无法识别类型vtable 地址不固定每次运行 ASLR 会改变地址用info symbol而非硬编码MSVC 布局不同Windows 下 MSVC 的 vtable 布局与 GCC 不同本教程不适用虚析构函数析构函数也是虚函数会占用 vtable 槽位纯虚函数纯虚函数在 vtable 里通常指向__cxa_pure_virtual调用会崩溃八、完整的 GDB 命令速查# 启动与断点 gdb ./vtable_demo break main run # 查看对象 print base print derived print *p2 # 查看内存 x/4gx base x/4gx derived # 查看 vtable x/8gx 0x555555557d20 # 地址翻译 info symbol 0x555555555b0a # 反汇编 disassemble /m main # 查看类型信息 ptype Base ptype Derived九、总结一张图看懂全貌对象内存 vtable.rodata ---------------- --------------------- | vptr ---------|----------- | offset-to-top 0 | ---------------- --------------------- | 成员 a 0x11 | | typeinfo ptr | ---------------- --------------------- | Base::func1 | --------------------- | Base::func2 | --------------------- | Base::func3 | --------------------- | Base::~Base | ---------------------多态调用p-func1()的底层路径p - vptr - vtable[0] - Base::func1 或 Derived::func1推荐一个零声教育学习教程个人觉得老师讲得不错分享给大家[LinuxNginx ZeroMQMySQLRedisfastdfsMongoDBZK流媒体CDNP2PK8SDockerTCP/IP协程DPDK等技术内容点击立即学习:链接
返回列表