乐趣区

关于c++:深入理解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

\

退出移动版