共计 4393 个字符,预计需要花费 11 分钟才能阅读完成。
0 引言
最近在工作之余,从新拾起了深度摸索 C ++ 对象模型 这本书,想要进一步温习了解 C++ 对象模型相干的原理。
以往看这本书,仅仅是浮于外表,并没有入手去实现和验证书中所说的内容。因而这次,便想既然再度学习,那么就深刻去看一看目前风行的编译器对 C ++ 对象模型 是如何实现的。故有了本专题。
目前应用较宽泛的编译器有 gcc,clang,msvc,但作为 C ++ 后盾开发置信应用 gcc,clang 较多,而我也是应用两者较多,因而 本专题中以 gcc 编译器为主,次要深入研究其对 C ++ 对象模型的实现。
如果你感觉深度摸索 C ++ 对象模型 让你深入了内力,想要进一步摸索古代编译器对其的实现,进一步加深相应的了解,那心愿本专题会对你有所帮忙。
学习本专题,须要你先学习 深刻了解计算机系统(原书第 3 版)次要相熟 ATT 汇编,根本的函数调用规定等,
同时也须要你学习了深度摸索 C ++ 对象模型 这本书,毕竟,本专题以此书为主。
此外,你须要相熟 GDB, 特地是如下几个命令, 具体的解说 可参考后续链接局部
set print pretty on
set print vtbl on
set print object on
也心愿你能常常应用如下两个在线工具:
https://cppinsights.io/
godblot
本专题所应用的机器环境如下
uname -a
Linux 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): 1
sizeof(Y): 8
sizeof(Z): 8
sizeof(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' value
warning: 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
\