使用gdb探索 C++ 虚函数表 —— 单继承

学习和使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class A {
public:
A() : ma{-1} {}
virtual ~A() = default;
virtual void va1() {}
virtual void va2() {}
void fa1() {}
long ma;
};

class B : public A {
public:
B() : mb{-2} {}
virtual ~B() = default;
virtual void vb1() {}
virtual void vb2() {}
void fb1() {}
long mb;
};

class C : public B {
public:
C() : mc{-3} {}
virtual ~C() = default;
virtual void vc1() {}
virtual void vc2() {}
void fc1() {}
long mc;
};

int main() {
A a;
B b;
C c;
return 0;
}

在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) checksymbol &b sizeof(b)
debug info, addr is: 0x7fffffffde30, len is: 24
0x7fffffffde30: 0x0000555555557d10 vtable for B + 16
0x7fffffffde38: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde40: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.

(gdb) checksymbol *(long*)&b-16 64
debug info, addr is: 93824992247040, len is: 64
0x555555557d00 <vtable for B>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d08 <vtable for B+8>: 0x0000555555557d88 typeinfo for B
0x555555557d10 <vtable for B+16>: 0x000055555555529a B::~B()
0x555555557d18 <vtable for B+24>: 0x00005555555552c4 B::~B()
0x555555557d20 <vtable for B+32>: 0x00005555555551ee A::va1()
0x555555557d28 <vtable for B+40>: 0x00005555555551fa A::va2()
0x555555557d30 <vtable for B+48>: 0x0000555555555282 B::vb1()
0x555555557d38 <vtable for B+56>: 0x000055555555528e B::vb2()

类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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(gdb) checksymbol &c sizeof(c)
debug info, addr is: 0x7fffffffde50, len is: 32
0x7fffffffde50: 0x0000555555557cc0 vtable for C + 16
0x7fffffffde58: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde60: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.
0x7fffffffde68: 0xfffffffffffffffd No symbol matches 0xfffffffffffffffd.

(gdb) checksymbol *(long*)&c-16 80
debug info, addr is: 93824992246960, len is: 80
0x555555557cb0 <vtable for C>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557cb8 <vtable for C+8>: 0x0000555555557d70 typeinfo for C
0x555555557cc0 <vtable for C+16>: 0x000055555555533e C::~C()
0x555555557cc8 <vtable for C+24>: 0x0000555555555368 C::~C()
0x555555557cd0 <vtable for C+32>: 0x00005555555551ee A::va1()
0x555555557cd8 <vtable for C+40>: 0x00005555555551fa A::va2()
0x555555557ce0 <vtable for C+48>: 0x0000555555555282 B::vb1()
0x555555557ce8 <vtable for C+56>: 0x000055555555528e B::vb2()
0x555555557cf0 <vtable for C+64>: 0x0000555555555326 C::vc1()
0x555555557cf8 <vtable for C+72>: 0x0000555555555332 C::vc2()

类C的结构和上面是一致的,这里不分析了。

单继承有覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class A {
public:
A() : ma{-1} {}
virtual ~A() = default;
virtual void va1() {}
virtual void va2() {}
void fa1() {}
long ma;
};

class B : public A {
public:
B() : mb{-2} {}
virtual ~B() = default;
virtual void vb1() {}
virtual void vb2() {}
virtual void va2() {}
void fb1() {}
long mb;
};

class C : public B {
public:
C() : mc{-3} {}
virtual ~C() = default;
virtual void vb1() {}
virtual void vc2() {}
virtual void vc1() {}
virtual void va2() {}
void fc1() {}
long mc;
};

int main() {
A a;
B b;
C c;
return 0;
}

在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) checksymbol &b sizeof(b)
debug info, addr is: 0x7fffffffde30, len is: 24
0x7fffffffde30: 0x0000555555557d10 vtable for B + 16
0x7fffffffde38: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde40: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.

(gdb) checksymbol *(long*)&b-16 64
debug info, addr is: 93824992247040, len is: 64
0x555555557d00 <vtable for B>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d08 <vtable for B+8>: 0x0000555555557d88 typeinfo for B
0x555555557d10 <vtable for B+16>: 0x00005555555552a6 B::~B()
0x555555557d18 <vtable for B+24>: 0x00005555555552d0 B::~B()
0x555555557d20 <vtable for B+32>: 0x00005555555551ee A::va1()
0x555555557d28 <vtable for B+40>: 0x000055555555529a B::va2()
0x555555557d30 <vtable for B+48>: 0x0000555555555282 B::vb1()
0x555555557d38 <vtable for B+56>: 0x000055555555528e B::vb2()

类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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(gdb) checksymbol &c sizeof(c)
debug info, addr is: 0x7fffffffde50, len is: 32
0x7fffffffde50: 0x0000555555557cc0 vtable for C + 16
0x7fffffffde58: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde60: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.
0x7fffffffde68: 0xfffffffffffffffd No symbol matches 0xfffffffffffffffd.

(gdb) checksymbol *(long*)&c-16 80
debug info, addr is: 93824992246960, len is: 80
0x555555557cb0 <vtable for C>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557cb8 <vtable for C+8>: 0x0000555555557d70 typeinfo for C
0x555555557cc0 <vtable for C+16>: 0x0000555555555362 C::~C()
0x555555557cc8 <vtable for C+24>: 0x000055555555538c C::~C()
0x555555557cd0 <vtable for C+32>: 0x00005555555551ee A::va1()
0x555555557cd8 <vtable for C+40>: 0x0000555555555356 C::va2()
0x555555557ce0 <vtable for C+48>: 0x0000555555555332 C::vb1()
0x555555557ce8 <vtable for C+56>: 0x000055555555528e B::vb2()
0x555555557cf0 <vtable for C+64>: 0x000055555555533e C::vc2()
0x555555557cf8 <vtable for C+72>: 0x000055555555534a C::vc1()

类C的情况和上面的结论一致,略过。

Ref