深入理解C++对象模型(开始)--由virtual继承说起

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

\

以上是 深入理解C++对象模型(开始)--由virtual继承说起 的全部内容, 来源链接: utcz.com/z/267478.html

回到顶部