学习和使用C++,继承和多态是避免不了的话题,其中核心就是虚函数表。接下来几篇文章,分几种继承情况,利用gdb解析虚函数表的结构。
C++标准并没有规定编译器如何实现C++的各种特性,所以不同编译器实现方法一般不同。
在上世纪90年代,GCC使用“setjmp”和“longjmp”实现异常处理。当Intel制造Itanium处理器时,Intel想让Intel Compiler和GCC可以一定程度互操作,当然包括互抛异常。于是Intel和GCC组织了一个工作组来设计新的ABI,即Itanium ABI。随后GCC开发者将该ABI扩展到了其他的处理器和操作系统。由于该ABI设计良好,LLVM也选择使用它。微软的MSVC编译器使用自己的ABI实现。
以下内容均是在下列环境讨论的
- Ubuntu 19.04 64bit系统
- gcc 8.3.0,即Itanium ABI
- gdb 8.2.91
- Python 3.7.3
- 编译时使用-g选项
过程中使用了上篇文章实现的gdb扩展“checksymbol”。该扩展主要是结合了gdb的“x”和“info symbol”命令。
“checksymbol”接受两个参数,地址a和长度l,它会打印从地址a开始l字节的地址及其内容,每8字节一行
地址a <地址a的符号名(如果有)>: 地址a中的内容(8字节) 地址a中内容(8字节)的符号名(如果有)
单继承没有覆盖
1 | class A { |
在gdb调试上述代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 检查a的内存布局
(gdb) checksymbol &a sizeof(a)
debug info, addr is: 0x7fffffffde20, len is: 16
0x7fffffffde20: 0x0000555555557d50 vtable for A + 16
0x7fffffffde28: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
// 检查a的虚函数表
(gdb) checksymbol *(long*)&a-16 48
debug info, addr is: 93824992247104, len is: 48
0x555555557d40 <vtable for A>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d48 <vtable for A+8>: 0x0000555555557da0 typeinfo for A
0x555555557d50 <vtable for A+16>: 0x0000555555555206 A::~A()
0x555555557d58 <vtable for A+24>: 0x0000555555555220 A::~A()
0x555555557d60 <vtable for A+32>: 0x00005555555551ee A::va1()
0x555555557d68 <vtable for A+40>: 0x00005555555551fa A::va2()
只有虚函数才会出现在虚函数表中。
类实例的前8字节是指向虚函数表的指针,但是注意它并没有指向虚函数表的起始地址,后面是类成员。
虚函数表第一个8字节是top_offset,在单继承中没有什么作用,均是0。
第二个8字节是用于支持RTTI的type info指针,就算编译时禁用RTTI,虚函数表还是有这一项,只是内容没意义
第三和第四个8字节是虚析构函数的指针,至于为什么是两个,不在本篇讨论范围,参见文末Ref。
剩下的就是各个虚函数的指针
所以一个有虚函数的普通类的结构
内存结构:
地址 | 内容 | 含义 |
---|---|---|
0x7fffffffde20 | 0x0000555555557d50 | vtable for A + 16 虚函数表指针 |
0x7fffffffde28 | 0xffffffffffffffff | A::ma |
虚函数表结构:
地址 | 内容 | 含义 |
---|---|---|
0x555555557d40 | 0x0000000000000000 | top_offset |
0x555555557d48 | 0x0000555555557da0 | typeinfo for A |
0x555555557d50 | 0x0000555555555206 | A::~A() |
0x555555557d58 | 0x0000555555555220 | A::~A() |
0x555555557d60 | 0x00005555555551ee | A::va1() |
0x555555557d68 | 0x00005555555551fa | A::va2() |
1 | (gdb) checksymbol &b sizeof(b) |
类B的结构和类A很接近,只是多了继承自类A的虚函数
内存结构:
地址 | 内容 | 含义 |
---|---|---|
0x7fffffffde30 | 0x0000555555557d10 | vtable for B + 16 虚函数表指针 |
0x7fffffffde38 | 0xffffffffffffffff | A::ma |
0x7fffffffde40 | 0xfffffffffffffffe | B::mb |
虚函数表结构:
地址 | 内容 | 含义 |
---|---|---|
0x555555557d00 | 0x0000000000000000 | top_offset |
0x555555557d08 | 0x0000555555557d88 | typeinfo for B |
0x555555557d10 | 0x000055555555529a | B::~B() |
0x555555557d18 | 0x00005555555552c4 | B::~B() |
0x555555557d20 | 0x00005555555551ee | A::va1() |
0x555555557d28 | 0x00005555555551fa | A::va2() |
0x555555557d30 | 0x0000555555555282 | B::vb1() |
0x555555557d38 | 0x000055555555528e | B::vb2() |
从类A和类B的虚函数表地址和符号名可以看出来,这两个不是同一张表,也不存在公共的部分,只不过有一部分内容相同而已。
类B的虚函数表前面部分的布局和类A的虚函数表布局一致,这样只要虚函数表里虚函数地址的顺序确定了(编译期),运行时就可以根据offset来调用对应的函数。
1 | (gdb) checksymbol &c sizeof(c) |
类C的结构和上面是一致的,这里不分析了。
单继承有覆盖
1 | class A { |
在gdb里调试1
2
3
4
5
6
7
8
9
10
11
12
13(gdb) checksymbol &a sizeof(a)
debug info, addr is: 0x7fffffffde20, len is: 16
0x7fffffffde20: 0x0000555555557d50 vtable for A + 16
0x7fffffffde28: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
(gdb) checksymbol *(long*)&a-16 48
debug info, addr is: 93824992247104, len is: 48
0x555555557d40 <vtable for A>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d48 <vtable for A+8>: 0x0000555555557da0 typeinfo for A
0x555555557d50 <vtable for A+16>: 0x0000555555555206 A::~A()
0x555555557d58 <vtable for A+24>: 0x0000555555555220 A::~A()
0x555555557d60 <vtable for A+32>: 0x00005555555551ee A::va1()
0x555555557d68 <vtable for A+40>: 0x00005555555551fa A::va2()
类A和没有覆盖时一样
1 | (gdb) checksymbol &b sizeof(b) |
类B覆盖了类A的va2函数
内存布局和没有覆盖时比没有变化
虚函数表有变化
没有覆盖 | 覆盖va2函数 |
---|---|
offset to top | offset to top |
type info地址 | type info地址 |
B::~B | B::~B |
B::~B | B::~B |
A::va1 | A::va1 |
A::va2 | B::va2 |
B::vb1 | B::vb1 |
B::vb2 | B::vb2 |
整个虚函数表每个函数的顺序没有改变,只是被覆盖的函数的地址变成了派生类函数的地址。当我们通过基类指针调用va2时,根据offset选到的就是B::va2,也就实现了多态。
1 | (gdb) checksymbol &c sizeof(c) |
类C的情况和上面的结论一致,略过。