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
也心愿你能常常应用如下两个在线工具:
https://cppinsights.io/
godblot
本专题所应用的机器环境如下
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 <iostream>#include <string>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 = {<No data fields>}y = warning: can't find linker symbol for virtual table for `Y' value{ <X> = {<No data fields>}, members of Y: _vptr.Y = 0x7ffff7d98fc8 <__exit_funcs_lock>}z = warning: can't find linker symbol for virtual table for `Z' valuewarning: found `A::A()' instead{ <X> = <invalid address>, members of Z: _vptr.Z = 0x555555555430 <__libc_csu_init>}a = { <Y> = { <X> = <invalid address>, members of Y: _vptr.Y = 0x0--Type <RET> for more, q to quit, c to continue without paging-- }, <Z> = { members of Z: _vptr.Z = 0x5555555550e0 <_start> }, <No data fields>}
故通过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
\