使用gdb探索 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
class A {
public:
A() : ma{-1}, maa{-10} {}
virtual ~A() = default;
virtual void va1() {}
virtual void va2() {}
void fa1() {}
long ma;
long maa;
};

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

class C : public A, public B {
public:
C() : mc{-4} {}
virtual ~C() = default;
virtual void vc2() {}
virtual void vc1() {}
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
(gdb) checksymbol &a sizeof(a)
debug info, addr is: 0x7fffffffde20, len is: 24
0x7fffffffde20: 0x0000555555557d38 vtable for A + 16
0x7fffffffde28: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde30: 0xfffffffffffffff6 No symbol matches 0xfffffffffffffff6.

(gdb) checksymbol *(long*)&a-16 48
debug info, addr is: 93824992247080, len is: 48
0x555555557d28 <vtable for A>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d30 <vtable for A+8>: 0x0000555555557da0 typeinfo for A
0x555555557d38 <vtable for A+16>: 0x0000555555555250 A::~A()
0x555555557d40 <vtable for A+24>: 0x000055555555526a A::~A()
0x555555557d48 <vtable for A+32>: 0x00005555555551fa A::va1()
0x555555557d50 <vtable for A+40>: 0x0000555555555206 A::va2()

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) checksymbol &b sizeof(b)
debug info, addr is: 0x7fffffffde10, len is: 16
0x7fffffffde10: 0x0000555555557d08 vtable for B + 16
0x7fffffffde18: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.

(gdb) checksymbol *(long*)&b-16 48
debug info, addr is: 93824992247032, len is: 48
0x555555557cf8 <vtable for B>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d00 <vtable for B+8>: 0x0000555555557d90 typeinfo for B
0x555555557d08 <vtable for B+16>: 0x0000555555555296 B::~B()
0x555555557d10 <vtable for B+24>: 0x00005555555552b0 B::~B()
0x555555557d18 <vtable for B+32>: 0x0000555555555238 B::vb1()
0x555555557d20 <vtable for B+40>: 0x0000555555555244 B::vb2()

类A、B没有继承任何类,结构比较简单,下面看类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
(gdb) checksymbol &c sizeof(c)
debug info, addr is: 0x7fffffffde40, len is: 48
0x7fffffffde40: 0x0000555555557c98 vtable for C + 16
0x7fffffffde48: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde50: 0xfffffffffffffff6 No symbol matches 0xfffffffffffffff6.
0x7fffffffde58: 0x0000555555557cd8 vtable for C + 80
0x7fffffffde60: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.
0x7fffffffde68: 0xfffffffffffffffc No symbol matches 0xfffffffffffffffc.

(gdb) checksymbol *(long*)&c-16 112
debug info, addr is: 93824992246920, len is: 112
0x555555557c88 <vtable for C>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557c90 <vtable for C+8>: 0x0000555555557d58 typeinfo for C
0x555555557c98 <vtable for C+16>: 0x0000555555555348 C::~C()
0x555555557ca0 <vtable for C+24>: 0x0000555555555396 C::~C()
0x555555557ca8 <vtable for C+32>: 0x00005555555551fa A::va1()
0x555555557cb0 <vtable for C+40>: 0x0000555555555206 A::va2()
0x555555557cb8 <vtable for C+48>: 0x0000555555555330 C::vc2()
0x555555557cc0 <vtable for C+56>: 0x000055555555533c C::vc1()
0x555555557cc8 <vtable for C+64>: 0xffffffffffffffe8 No symbol matches 0xffffffffffffffe8.
0x555555557cd0 <vtable for C+72>: 0x0000555555557d58 typeinfo for C
0x555555557cd8 <vtable for C+80>: 0x0000555555555390 non-virtual thunk to C::~C()
0x555555557ce0 <vtable for C+88>: 0x00005555555553c1 non-virtual thunk to C::~C()
0x555555557ce8 <vtable for C+96>: 0x0000555555555238 B::vb1()
0x555555557cf0 <vtable for C+104>: 0x0000555555555244 B::vb2()

内存结构:

地址 内容 含义
0x7fffffffde40 0x0000555555557c98 vtable for C + 16 虚函数表指针
0x7fffffffde48 0xffffffffffffffff A::ma
0x7fffffffde50 0xfffffffffffffff6 A::maa
0x7fffffffde58 0x0000555555557cd8 vtable for C + 80 虚函数表指针
0x7fffffffde60 0xfffffffffffffffe B::mb
0x7fffffffde68 0xfffffffffffffffc C::mc

类成员还是按照A、B、C排列,在A、B类成员间多了一个虚函数表指针。vtable for C + 16用于类A和C,vtable for C + 80用于类B。

虚函数表结构:

地址 内容 含义
0x555555557c88 [vtable for C] 0x0000000000000000 top_offset
0x555555557c90 [vtable for C+8] 0x0000555555557d58 typeinfo for C
0x555555557c98 [vtable for C+16] 0x0000555555555348 C::~C()
0x555555557ca0 [vtable for C+24] 0x0000555555555396 C::~C()
0x555555557ca8 [vtable for C+32] 0x00005555555551fa A::va1()
0x555555557cb0 [vtable for C+40] 0x0000555555555206 A::va2()
0x555555557cb8 [vtable for C+48] 0x0000555555555330 C::vc2()
0x555555557cc0 [vtable for C+56] 0x000055555555533c C::vc1()
0x555555557cc8 [vtable for C+64] 0xffffffffffffffe8 top_offset 值为-24
0x555555557cd0 [vtable for C+72] 0x0000555555557d58 typeinfo for C
0x555555557cd8 [vtable for C+80] 0x0000555555555390 non-virtual thunk to C::~C()
0x555555557ce0 [vtable for C+88] 0x00005555555553c1 non-virtual thunk to C::~C()
0x555555557ce8 [vtable for C+96] 0x0000555555555238 B::vb1()
0x555555557cf0 [vtable for C+104] 0x0000555555555244 B::vb2()

这里能看到有两个类似单继承中的虚函数表结构:vtable for C 0~63和vtable for C 64~111

多继承有覆盖

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}, maa{-10} {}
virtual ~A() = default;
virtual void va1() {}
virtual void va2() {}
void fa1() {}
long ma;
long maa;
};

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

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

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

类C覆盖了类A的va2和类B的vb1方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) checksymbol &a sizeof(a)
debug info, addr is: 0x7fffffffde20, len is: 24
0x7fffffffde20: 0x0000555555557d38 vtable for A + 16
0x7fffffffde28: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde30: 0xfffffffffffffff6 No symbol matches 0xfffffffffffffff6.

(gdb) checksymbol *(long*)&a-16 48
debug info, addr is: 93824992247080, len is: 48
0x555555557d28 <vtable for A>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d30 <vtable for A+8>: 0x0000555555557da0 typeinfo for A
0x555555557d38 <vtable for A+16>: 0x0000555555555250 A::~A()
0x555555557d40 <vtable for A+24>: 0x000055555555526a A::~A()
0x555555557d48 <vtable for A+32>: 0x00005555555551fa A::va1()
0x555555557d50 <vtable for A+40>: 0x0000555555555206 A::va2()

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) checksymbol &b sizeof(b)
debug info, addr is: 0x7fffffffde10, len is: 16
0x7fffffffde10: 0x0000555555557d08 vtable for B + 16
0x7fffffffde18: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.

(gdb) checksymbol *(long*)&b-16 48
debug info, addr is: 93824992247032, len is: 48
0x555555557cf8 <vtable for B>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557d00 <vtable for B+8>: 0x0000555555557d90 typeinfo for B
0x555555557d08 <vtable for B+16>: 0x0000555555555296 B::~B()
0x555555557d10 <vtable for B+24>: 0x00005555555552b0 B::~B()
0x555555557d18 <vtable for B+32>: 0x0000555555555238 B::vb1()
0x555555557d20 <vtable for B+40>: 0x0000555555555244 B::vb2()

类A、B和之前一样没什么变化,下面看类C

1
2
3
4
5
6
7
8
(gdb) checksymbol &c sizeof(c)
debug info, addr is: 0x7fffffffde40, len is: 48
0x7fffffffde40: 0x0000555555557c90 vtable for C + 16
0x7fffffffde48: 0xffffffffffffffff No symbol matches 0xffffffffffffffff.
0x7fffffffde50: 0xfffffffffffffff6 No symbol matches 0xfffffffffffffff6.
0x7fffffffde58: 0x0000555555557cd8 vtable for C + 88
0x7fffffffde60: 0xfffffffffffffffe No symbol matches 0xfffffffffffffffe.
0x7fffffffde68: 0xfffffffffffffffc No symbol matches 0xfffffffffffffffc.

类C的内存布局和之前一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) checksymbol *(long*)&c-16 120
debug info, addr is: 93824992246912, len is: 120
0x555555557c80 <vtable for C>: 0x0000000000000000 No symbol matches 0x0000000000000000.
0x555555557c88 <vtable for C+8>: 0x0000555555557d58 typeinfo for C
0x555555557c90 <vtable for C+16>: 0x0000555555555366 C::~C()
0x555555557c98 <vtable for C+24>: 0x00005555555553b4 C::~C()
0x555555557ca0 <vtable for C+32>: 0x00005555555551fa A::va1()
0x555555557ca8 <vtable for C+40>: 0x000055555555535a C::va2()
0x555555557cb0 <vtable for C+48>: 0x0000555555555330 C::vc2()
0x555555557cb8 <vtable for C+56>: 0x000055555555533c C::vc1()
0x555555557cc0 <vtable for C+64>: 0x0000555555555348 C::vb1()
0x555555557cc8 <vtable for C+72>: 0xffffffffffffffe8 No symbol matches 0xffffffffffffffe8.
0x555555557cd0 <vtable for C+80>: 0x0000555555557d58 typeinfo for C
0x555555557cd8 <vtable for C+88>: 0x00005555555553ae non-virtual thunk to C::~C()
0x555555557ce0 <vtable for C+96>: 0x00005555555553df non-virtual thunk to C::~C()
0x555555557ce8 <vtable for C+104>: 0x0000555555555353 non-virtual thunk to C::vb1()
0x555555557cf0 <vtable for C+112>: 0x0000555555555244 B::vb2()

虚函数表结构:

地址 内容 含义
0x555555557c80 [vtable for C] 0x0000000000000000 top_offset
0x555555557c88 [vtable for C+8] 0x0000555555557d58 typeinfo for C
0x555555557c90 [vtable for C+16] 0x0000555555555366 C::~C()
0x555555557c98 [vtable for C+24] 0x00005555555553b4 C::~C()
0x555555557ca0 [vtable for C+32] 0x00005555555551fa A::va1()
0x555555557ca8 [vtable for C+40] 0x000055555555535a C::va2()
0x555555557cb0 [vtable for C+48] 0x0000555555555330 C::vc2()
0x555555557cb8 [vtable for C+56] 0x000055555555533c C::vc1()
0x555555557cc0 [vtable for C+64] 0x0000555555555348 C::vb1()
0x555555557cc8 [vtable for C+72] 0xffffffffffffffe8 top_offset,值为-24
0x555555557cd0 [vtable for C+80] 0x0000555555557d58 typeinfo for C
0x555555557cd8 [vtable for C+88] 0x00005555555553ae non-virtual thunk to C::~C()
0x555555557ce0 [vtable for C+96] 0x00005555555553df non-virtual thunk to C::~C()
0x555555557ce8 [vtable for C+104] 0x0000555555555353 non-virtual thunk to C::vb1()
0x555555557cf0 [vtable for C+112] 0x0000555555555244 B::vb2()

vtable for C 0~47和类A的虚函数表布局一致,当作类A操作也可以,所以被覆盖的va2指针直接替换为C::va2的指针。

但是,vtable for C 0~XX无法形成和类B的虚函数表相同的布局,所以编译器在vtable for C 72~119构造了与类B相同布局的虚函数表。
被覆盖的vb1函数指针被“non-virtual thunk to C::vb1()”函数指针替换,什么是“non-virtual thunk”?

先看下类型转换的结果

1
2
(gdb) p static_cast<B*>(&c)
$6 = (B *) 0x7fffffffde58

转换为B*后和&c的值并不相同,而是c的第二个虚函数表指针的地址“vtable for C + 88”

1
2
3
void foo(B* f) {
f->vb1();
}

函数void foo(B*)接受类B的指针,如果传入一个类c的实例,首先会进行类型转换,这时this指针指向c的第二个虚函数表指针“vtable for C + 88”。对vb1方法的调用应该是执行类C的vb1函数,但是此时的this指针对于类C的vb1函数是不对的。“non-virtual thunk to C::vb1()”就是用来修正this指针并调用C::vb1,它通过top_offset将this修改到正确的值“vtable for C + 16”。

1
2
3
4
5
(gdb) disassemble 0x0000555555555353
Dump of assembler code for function _ZThn24_N1C3vb1Ev:
0x0000555555555353 <+0>: sub $0x18,%rdi
0x0000555555555357 <+4>: jmp 0x555555555348 <C::vb1()>
End of assembler dump.

Ref