C++虚函数之二:虚函数表与虚函数调用

继续前一篇《C++ 虚函数之一:对象内存布局》,这次来分析一下虚函数表的结构和虚函数的调用过程。

虚函数表结构

如何查看虚函数表的结构?使用gdb直接查看内存固然可以,但是不够直观,那么有没有更好的方法呢?使用gcc的-fdump-class-hierarchy选项是个不错的选择,在gcc手册中对该选项的部分解释如下:

-fdump-class-hierarchy-options (C++ only)
Dump a representation of each class’s hierarchy and virtual function table layout to a file.

它能够生成类的继承层次结构和虚函数表的布局。
上篇文章已经贴过部分代码,现在我把整个源文件贴在下面:

#include <stdint.h>
#include <string.h>

class Base1
{
public:
    Base1() { memset(&Base1Data, 0x11, sizeof(Base1Data)); }
    virtual void A() {};
    virtual void B() {};
    uint64_t Base1Data;
};

class Base2
{
public:
    Base2() { memset(&Base2Data, 0x22, sizeof(Base2Data)); }
    virtual void C() {};
    virtual void D() {};
    uint64_t Base2Data;
};

class Derived : public Base1, public Base2
{
public:
    Derived() { memset(&DerivedData, 0x33, sizeof(DerivedData)); }
    virtual void A() {};
    virtual void C() {};
    uint64_t DerivedData;
};

int main()
{
    Base1 *x = new Derived;
    x->A();
    x->B();

    Base2 *y = new Derived;
    y->C();
    y->D();

    Derived *z = new Derived;
    z->A();
    z->B();
    z->C();
    z->D();

    return 0;
}

使用gcc的-fdump-class-hierarchy-options选项分析源文件:

$ g++ -c -fdump-class-hierarchy call_function.cpp

生成了call_function.cpp.002t.class文件(删掉了在外部头文件定义的类):

Vtable for Base1
Base1::_ZTV5Base1: 4 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5Base1)
16    (int (*)(...))Base1::A
24    (int (*)(...))Base1::B

Class Base1
   size=16 align=8
   base size=16 base align=8
Base1 (0x0x7ff358c9bc00) 0
    vptr=((& Base1::_ZTV5Base1) + 16)

Vtable for Base2
Base2::_ZTV5Base2: 4 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5Base2)
16    (int (*)(...))Base2::C
24    (int (*)(...))Base2::D

Class Base2
   size=16 align=8
   base size=16 base align=8
Base2 (0x0x7ff358c9bea0) 0
    vptr=((& Base2::_ZTV5Base2) + 16)

Vtable for Derived
Derived::_ZTV7Derived: 9 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Derived::A
24    (int (*)(...))Base1::B
32    (int (*)(...))Derived::C
40    (int (*)(...))-16
48    (int (*)(...))(& _ZTI7Derived)
56    (int (*)(...))Derived::_ZThn16_N7Derived1CEv
64    (int (*)(...))Base2::D

Class Derived
   size=40 align=8
   base size=40 base align=8
Derived (0x0x7ff358b424d0) 0
    vptr=((& Derived::_ZTV7Derived) + 16)
  Base1 (0x0x7ff358cfa180) 0
      primary-for Derived (0x0x7ff358b424d0)
  Base2 (0x0x7ff358cfa1e0) 16
      vptr=((& Derived::_ZTV7Derived) + 56)

我们得到了每个类的继承层次结构和虚函数表布局。可以看出来,这三个类每个类都有一个虚函数表。以Derived类为例,虚函数表共有9个条目,其布局为:

Vtable for Derived
Derived::_ZTV7Derived: 9 entries
0     (int (*)(...))0                           #1
8     (int (*)(...))(& _ZTI7Derived)            #2
16    (int (*)(...))Derived::A                  #3
24    (int (*)(...))Base1::B                    #4
32    (int (*)(...))Derived::C                  #5
40    (int (*)(...))-16                         #6
48    (int (*)(...))(& _ZTI7Derived)            #7
56    (int (*)(...))Derived::_ZThn16_N7Derived1CEv  #8
64    (int (*)(...))Base2::D                    #9

条目1和条目6是整形常量,条目2和条目7是typeinfo for Derived,条目3、4、5和9是指向类成员函数的指针,条目8demangle后的名字是non-virtual thunk to Derived::C()。我们用gdb分析在进行虚函数调用时,是如何使用这些条目的。

虚函数调用

使用gdb反汇编,分析每个函数调用的具体过程。

x->A():

   # x指针存在$rbp-0x28处,首先得到x指针,存放到rax
   0x000055555555497c <+34>:    mov    -0x28(%rbp),%rax
   # 取x指针指向的8个字节的数据存放到rax,还记得对象前8个字节存的是什么吗?是虚函数表指针
   # 不过虚表指针没有指向虚函数表首地址,对于Derived对象,对象起始位置虚表指针指向虚表的起始位置+16处,也就是条目3
   0x0000555555554980 <+38>:    mov    (%rax),%rax
   # 取条目3的内容存放到rax,而条目3是Derived::A函数指针,也就是rax现在存放的是 Derived::A函数地址
   0x0000555555554983 <+41>:    mov    (%rax),%rax
   # 将x存放到rdx
   0x0000555555554986 <+44>:    mov    -0x28(%rbp),%rdx
   # 将x存放到rdi,rdi一般作为接下来函数调用的第一个参数,对于Derived::A来说,第1个参数是this指针
   0x000055555555498a <+48>:    mov    %rdx,%rdi
   # 调用Derived::A
   0x000055555555498d <+51>:    callq  *%rax

x->B():

   # 同x->A()
   0x000055555555498f <+53>:    mov    -0x28(%rbp),%rax
   # 同x->A()
   0x0000555555554993 <+57>:    mov    (%rax),%rax
   # rax被调整为指向虚函数表第4个条目:Base1::B
   0x0000555555554996 <+60>:    add    $0x8,%rax
   # 将条目4存放到rax,Base1::B的地址
   0x000055555555499a <+64>:    mov    (%rax),%rax
   # 同x->A()
   0x000055555555499d <+67>:    mov    -0x28(%rbp),%rdx
   # 同x->A()
   0x00005555555549a1 <+71>:    mov    %rdx,%rdi
   # 调用Base1::B
   0x00005555555549a4 <+74>:    callq  *%rax

y->C()和y->D()的过程与x->A()和x->B()过程几乎完全相同,都是取Derived虚函数表条目内容,也就是函数地址,然后进行调用。你是否还记得条目8“non-virtual thunk to Derived::C()”,y->C()执行时调用了这个函数,我们看看这个函数到底是个什么:

(gdb) x/2g y #查看y指向内容,其首地址8个字节是Derived虚表指针
0x555555768eb0: 0x0000555555755d08  0x2222222222222222
(gdb) x/g 0x0000555555755d08 #得到条目8内容
0x555555755d08 <_ZTV7Derived+56>:   0x0000555555554b95
(gdb) disassemble 0x0000555555554b95 #反汇编
Dump of assembler code for function _ZThn16_N7Derived1CEv:
   0x0000555555554b95 <+0>: sub    $0x10,%rdi
   0x0000555555554b99 <+4>: jmp    0x555555554b8a <Derived::C()>
End of assembler dump.

non-virtual thunk to Derived::C()只做了两件事,首先将this指针向前调整16个字节,调整到Derived对象首地址,然后跳转到Derived::C()执行。这么做的原因也好理解,y指针指向了Derived对象的中间部分,而传给Derived::C()的this指针必然需要是一个指向Derived对象首地址的指针,否则访问数据成员计算偏移量时会出问题。
z->A()、z->B()和z->C()都和x->A()和x->B()调用类似,z->D()则稍有不同,我把不同的地方注释了一下:

   0x0000555555554a53 <+249>:   mov    -0x18(%rbp),%rax
   # 取$rax+16存放到rdx,rax是z指针,则$rax+16则指向了Derived对象Base2部分的首地址
   0x0000555555554a57 <+253>:   lea    0x10(%rax),%rdx
   # 其他部分都类似y->D()
   0x0000555555554a5b <+257>:   mov    -0x18(%rbp),%rax
   0x0000555555554a5f <+261>:   mov    0x10(%rax),%rax
   0x0000555555554a63 <+265>:   add    $0x8,%rax
   0x0000555555554a67 <+269>:   mov    (%rax),%rax
   0x0000555555554a6a <+272>:   mov    %rdx,%rdi
   0x0000555555554a6d <+275>:   callq  *%rax

可以看出来,这个过程相当于先把z指针转型为Base2类型,然后按照Base2类型的调用过程来进行函数调用。这么做的原因类似与y->C()调用non-virtual thunk to Derived::C(),都是因为我们调用成员函数时所用的对象指针与传给成员函数的this指针类型不同,需要进行调整,以免访问数据成员时访问到了意外的内容。

虚函数表的其他内容

可以看出来,Derived虚函数表有9个条目,但我们目前只提到了5个,剩下4个是什么呢?

0     (int (*)(...))0                           #1
8     (int (*)(...))(& _ZTI7Derived)            #2
40    (int (*)(...))-16                         #6
48    (int (*)(...))(& _ZTI7Derived)            #7

剩下这4个条目用于在运行时获得某个对象指针的类型信息,例如对于前面的y指针,其其首地址是虚表指针。我们把虚表指针向前调整8个字节,就指向了条目7:typeinfo for Derived,就可以得到这个指针实际指向对象的类型;把虚表指针向前调整16个字节,就指向了条目6:-16,这个-16的含义就是,把y指针向前调整16个字节,就是其真正指向对象的首地址,x指针也是同样的道理。C++dynamic_cast的实现就依赖于这几个额外的条目,有兴趣的可自己钻研一下gcc源码。

猜你喜欢

转载自blog.csdn.net/imred/article/details/80717756