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

\