深入理解C++对象模型(1)--虚拟继承
0 引言
最近在工作之余,重新拾起了深度探索C++对象模型 这本书,想要进一步复习理解 C++对象模型相关的原理。
以往看这本书,仅仅是浮于表面,并没有动手去实现和验证书中所说的内容。因此这次,便想既然再度学习,那么就深入去看一看目前流行的编译器对C++对象模型 是如何实现的。故有了本专题。
目前使用较广泛的编译器有gcc,clang, msvc,但作为C++后台开发相信使用gcc,clang较多,而我也是使用两者较多,因此 本专题中以gcc编译器为主,主要深入研究其对C++对象模型的实现。
如果你感觉深度探索C++对象模型 让你深化了内力,想要进一步探索现代编译器对其的实现,进一步加深相应的理解,那希望本专题会对你有所帮助。
学习本专题,需要你先学习深入理解计算机系统(原书第3版) 主要熟悉 ATT汇编,基本的函数调用规则等,
同时也需要你学习了深度探索C++对象模型 这本书,毕竟,本专题以此书为主。
此外,你需要熟悉GDB, 特别是如下几个命令, 详细的讲解 可参考后续链接部分
set print pretty onset print vtbl onset print object on
也希望你能经常使用如下两个在线工具:
C++ Insightscppinsights.iohttps://cppinsights.io/
Compiler Explorer - C++ (x86-64 gcc (trunk))https://link.zhihu.com/?target=https%3A//godbolt.org/clientstate/eyJzZXNzaW9ucyI6W3siaWQiOjEsImxhbmd1YWdlIjoiYysrIiwic291cmNlIjoiI2luY2x1ZGUgPGlvc3RyZWFtPlxuI2luY2x1ZGUgPHN0cmluZz5cblxuY2xhc3MgWCB7fTtcbmNsYXNzIFkgOiBwdWJsaWMgdmlydHVhbCBYIHt9O1xuY2xhc3MgWiA6IHB1YmxpYyB2aXJ0dWFsIFgge307XG5jbGFzcyBBIDogcHVibGljIFksIHB1YmxpYyBaIHt9O1xuXG5pbnQgbWFpbigpIHtcbiAgICBYIHg7XG4gICAgWSB5O1xuICAgIFogejtcbiAgICBBIGE7XG4gICAgc3RkOjpjb3V0IDw8IFwic2l6ZW9mKFgpOiBcIiA8PCBzaXplb2YoWCkgPDwgXCJcXG5cIjtcbiAgICBzdGQ6OmNvdXQgPDwgXCJzaXplb2YoWSk6IFwiIDw8IHNpemVvZihZKSA8PCBcIlxcblwiO1xuICAgIHN0ZDo6Y291dCA8PCBcInNpemVvZihaKTogXCIgPDwgc2l6ZW9mKFopIDw8IFwiXFxuXCI7XG4gICAgc3RkOjpjb3V0IDw8IFwic2l6ZW9mKEEpOiBcIiA8PCBzaXplb2YoQSkgPDwgXCJcXG5cIjtcbiAgICByZXR1cm4gMDtcbn0iLCJjb21waWxlcnMiOlt7ImlkIjoiZ3NuYXBzaG90Iiwib3B0aW9ucyI6Ii1zdGQ9YysrMTEifV19XX0%3D
本专题所使用的机器环境如下
uname -aLinux qls-VirtualBox 5.11.0-38-generic #42~20.04.1-Ubuntu SMP Tue Sep 28 20:41:07 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
gcc编译器版本为
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
最后,本人也只是因为兴趣而想进一步研究,中间可能会有些理解不对的地方,欢迎大家随时指正;同时由于工作比较忙,因此我可能会断断续续更新该专栏。
1 故事的开端
关于C++对象模型,我们以深度探索C++对象模型 第三章开篇所给的例子说起,其例子如下
#include #include class X {};class Y : public virtual X {};class Z : public virtual X {};class A : public Y, public Z {};int main() { std::cout << "sizeof(X): " << sizeof(X) << "\n"; std::cout << "sizeof(Y): " << sizeof(Y) << "\n"; std::cout << "sizeof(Z): " << sizeof(Z) << "\n"; std::cout << "sizeof(A): " << sizeof(A) << "\n"; return 0;}
上述例子很简单,通过我的实验,在我的环境下上述输出的结果为
sizeof(X): 1sizeof(Y): 8sizeof(Z): 8sizeof(A): 16
为什么如此呢?此原因深度探索C++对象模型 在该章开始部分已经给出说明,本文将其摘抄至下
sizeof(X) = 1: 这是为了使得同一class的不同objects在内存中有独一无二的地址,因此编译器会安插一个char在空类中。
关于sizeof(Y): 8 sizeof(Z): 8 sizeof(A): 16相关的原因,我们通过gdb来感知和确认。
2 GDB感知C++对象模型
为了使用gdb感知上述的对象模型,在示例中增加如下四条语句
X x; Y y; Z z; A a;
通过如下gcc命令构建
g++ -o main lambda1.cc -std=c++17 -g
并用gdb运行相应的main程序后,设置如下三项命令
(gdb) set print pretty on(gdb) set print vtbl on(gdb) set print object on
然后通过b main后,运行info locals 命令出现如下结果
x = {}y = warning: can't find linker symbol for virtual table for `Y' value{ = {}, members of Y: _vptr.Y = 0x7ffff7d98fc8 }z = warning: can't find linker symbol for virtual table for `Z' valuewarning: found `A::A()' instead{ = , members of Z: _vptr.Z = 0x555555555430 }a = { = { = , members of Y: _vptr.Y = 0x0--Type for more, q to quit, c to continue without paging-- }, = { members of Z: _vptr.Z = 0x5555555550e0 }, }
故通过gdb的结果可以发现:对象y编译器安插了一个_vptr.Y成员,对象z由编译器安插了一个_vptr.Z成员,对象a 由编译器安插了一个_vptr.Y , _vptr.Z成员
虽然y,z, a都有成员X但均在内存布局的开头,因此被gcc优化掉其1byte。这也符合深度探索C++对象模型 中所说
针对empty virtual base class,某些新晋编译器对其进行特殊处理。在这个策略下,一个empty virtual base class 被视为derived class object开头的一部分,也就是说他没有花费任何额外的空间,这就节省掉了1byte。
故可便输出了相应结果。
也许故事的开始到这里便可以了,但我们说过要什么到汇编层面去进一步看看现代编译器如何实现C++对象模型的,也许会发现 深度探索C++对象模型 中所画的对象模型示意图已经符不符合现代编译器实现呢?(至少GCC)
那么,带着如下疑问:上述的vptr指针 到底指向哪里呢?相应的vtbl中的内容又是什么呢?是不是深度探索C++对象模型 中的如下示意图需要纠正呢?
如下的示意图中virtual base class offsets在本文实例中到底对还是不对呢?里面的内容究竟是什么呢?
带着上述疑问,我们进入下一节,细观vtbl!
3 细观vtbl
往往表面看起来简单的事情,背地里却是异常的复杂,正如本文给出的示例一样。其背后的复杂性我们可以从汇编层面一探究竟!
针对本文示例,我们通过文章开时所介绍的工具Compiler Explorer - C++ (x86-64 gcc (trunk)) 来查看相应的汇编实现,并回答上小结末尾所提出的疑问。
通过Compiler Explorer - C++ (x86-64 gcc (trunk)) 可知,在汇编层面有如下关键符号
Y::Y() [base object constructor]
Y::Y() [complete object constructor]
Z::Z() [base object constructor]
Z::Z() [complete object constructor]
A::A() [complete object constructor]
vtable for A
VTT for A
construction vtable for Y-in-A
construction vtable for Z-in-A
vtable for Z
VTT for Z
vtable for Y
VTT for Y
以及各种typeinfo for相关符号
关于上述这些符号的含义,我会在后续分享中一一讲明。
下面来回答我们上小结的疑问!
- vptr到底指向哪里(vtbl中的内容是什么呢?)
在此,将两个疑问合并成一个,如果你学习过深度探索C++对象模型 那便明白vptr指向vtbl,只是具体指向vtbl的
那一个slots你可能没有正确答案而已,本文会告诉你在gcc编译器中 vptr具体指向vtbl的哪一个slots。
此处,我们仅针对类型Y的vtbl来说明相关结论,关于Y的vtbl中到底有哪些内容,通过相应的在线工具Compiler Explorer - C++ (x86-64 gcc (trunk)) 知,其内容如下
vtable for Y: .quad 0 .quad 0 .quad typeinfo for Y
通过Y的构造函数中如下实现
movq %rdi, -8(%rbp) movl $vtable for Y+24, %edx movq -8(%rbp), %rax movq %rdx, (%rax) // this->vptr = *rax
将上述两者结合,知在gcc编译器下,Y的内存模型如下
如此对象模型也回答了上述小节关于深度探索C++对象模型 中所给出的示意图
是否正确的问题。
通过分析汇编代码知,在现代gcc编译器中,vptr并不是指向typeinfo for point,而是指向其下一个位置的slot。
那么剩下最后一个问题,深度探索C++对象模型 如下的示意图中virtual base class offsets在本文实例中到底对还是不对呢?里面的内容究竟是什么呢?
答案是很明显的,在现代gcc编译器下,
上述示意图是不对的,
- vptr应该指向type info 所在slot的下一个slot
- 如果存在virtual base class,在vtbl中会多两个slot,针对对象y而言这两个slot中的值为0
- 多的这两个slot分别含义为:top_offset,vbase_offset
所谓top_offset,即vptr针对该对象起始位置的距离;所谓vbase_offset即为该对象中 virtual base类对象所离该对象起始位置的距离。
所以最终,在现代gcc编译器下,Y的对象模型结构如下所示
相应的对象Z的内存模型可类比Y,此处不在展开!
4 总结
本专题我已在知乎更新(此处同步到这里)
通过本文可以初步总结出如下结论:
- 如果一个类A虚拟继承某个类X,那么在gcc编译的场景下,类Y会生成相应的vtbl且会被编译器插入一个vptr
- 类vtbl的虚表默认有三个slot,分别为vbase_offset, top_offset, typeinfo for A
- 类A的vptr会指向type info for A slot的下一个slot
本文未解决的问题:
- 什么是VTT?为什么gcc编译器会针对棱形继承生成VTT?
- 什么是base object constructor]和 complete object constructor
- 类A的内存模型是怎样的?
- 类A的构造过程是怎样的?
深入理解C++对象模型的故事很长,这篇文章仅仅是一个初步的开端,后续会继续完善并回答上述未解决的问题。
参考:
Debugging with gdb - Examining Data
Using gdb with Different Languages