基于LINUX平台G++编译器从汇编层面深度剖析C++虚函数

5 篇文章 0 订阅
5 篇文章 0 订阅

虚函数是C++语言实现运行时多态的唯一手段,因此掌握C++虚函数也成为C++程序员是否合格的试金石。诚然,C++虚函数的结构会因编译器不同而异,但所使用的原理是一样的。为此,本文使用linux平台下的g++编译器,试图从汇编的层面上分析虚函数表的结构,以及如何利用它来实现运行时多态。

汇编语言是难读的,特别是对一些没有汇编基础的朋友,因此,本文将汇编翻译成相应的C语言,以方便读者分析问题。

1. 代码 
为了方便表述问题,本文选取只有虚函数的两个类,当然,还有它的构造函数,如下:

01.class Base       
02.{      
03. public:      
04.    virtual void f() { }      
05.    virtual void g() { }      
06.};      
07.class Derive : public Base      
08.{      
09.  public:   
10.    virtual void f() {}   
11.};      
12.int main()      
13.{      
14.  Derive d;      
15.  Base *pb;   
16.  pb = &d;    
17.  pb->f();   
18.  return 0;   
19.}   
class Base    
{   
public:   
    virtual void f() { }   
    virtual void g() { }   
};   
class Derive : public Base   
{   
  public:
    virtual void f() {}
};   
int main()   
{   
  Derive d;   
  Base *pb;
  pb = &d; 
  pb->f();
  return 0;

2. 两个类的虚函数表(vtable) 
使用g++ –Wall –S test.cpp命令,可以将上述的C++代码生成它相应的汇编代码。

01._ZTV4Base:   
02.    .long   0     
03.    .long   _ZTI4Base   
04.    .long   _ZN4Base1fEv   
05.    .long   _ZN4Base1gEv   
06.    .weak   _ZTS6Derive   
07.    .section    .rodata._ZTS6Derive,”aG”,@progbits,_ZTS6Derive,comdat   
08.    .type   _ZTS6Derive, @object   
09.    .size   _ZTS6Derive, 8   
_ZTV4Base:
    .long   0  
    .long   _ZTI4Base
    .long   _ZN4Base1fEv
    .long   _ZN4Base1gEv
    .weak   _ZTS6Derive
    .section    .rodata._ZTS6Derive,”aG”,@progbits,_ZTS6Derive,comdat
    .type   _ZTS6Derive, @object
    .size   _ZTS6Derive, 8 

_ZTV4Base是一个数据符号,它的命名规则是根据g++的内部规则来命名的,如果你想查看它真正表示C++的符号名,可使用c++filt命令来转换,例如:

[lyt@t468 ~]$ c++filt _ZTV4Base 
vtable for Base

_ZTV4Base符号(或者变量)可看作为一个数组,它的第一项是0,第二项_ZIT4Base是关于Base的类型信息,这与typeid有关。为方便讨论,我们略去此二项数据。 因此Base类的vtable的结构,翻译成相应的C语言定义如下:

01.unsigned long Base_vtable[] = {   
02.    &Base::f(),   
03.    &Base::g(),   
04.};  
unsigned long Base_vtable[] = {
    &Base::f(),
    &Base::g(),
}; 
而Derive的更是类似,只有稍为有点不同:

01._ZTV6Derive:   
02.    .long   0     
03.    .long   _ZTI6Derive   
04.    .long   _ZN6Derive1fEv   
05.    .long   _ZN4Base1gEv   
06.    .weak   _ZTV4Base   
07.    .section    .rodata._ZTV4Base,”aG”,@progbits,_ZTV4Base,comdat   
08.    .align 8   
09.    .type   _ZTV4Base, @object   
10.    .size   _ZTV4Base, 16   
_ZTV6Derive:
    .long   0  
    .long   _ZTI6Derive
    .long   _ZN6Derive1fEv
    .long   _ZN4Base1gEv
    .weak   _ZTV4Base
    .section    .rodata._ZTV4Base,”aG”,@progbits,_ZTV4Base,comdat
    .align 8
    .type   _ZTV4Base, @object
    .size   _ZTV4Base, 16 

相应的C语言定义如下:

01.unsigned long Derive_vtable[] = {   
02.    &Derive::f(),   
03.    &Base::g(),   
04.};  
unsigned long Derive_vtable[] = {
    &Derive::f(),
    &Base::g(),
};

从上面两个类的vtable可以看到,Derive的vtable中的第一项重写了Base类vtable的第一项。只要子类重写了基类的虚函数,那么子类vtable相应的项就会更改父类的vtable表项。 这一过程是编译器自动处理的,并且每个的类的vtable内容都放在数据段里面。

3. 谁让对象与vtable绑到一起

上述代码只是定义了每个类的vtable的内容,但我们知道,带有虚函数的对象在它内部都有一个vtable指针,指向这个vtable,那么是何时指定的呢? 只要看看构造函数的汇编代码,就一目了然了:

Base::Base()函数的编译代码如下:

01._ZN4BaseC1Ev:   
02..LFB6:   
03.    .cfi_startproc   
04.    .cfi_personality 0×0,__gxx_personality_v0   
05.    pushl   %ebp   
06.    .cfi_def_cfa_offset 8   
07.    movl    %esp, %ebp   
08.    .cfi_offset 5, -8   
09.    .cfi_def_cfa_register 5   
10.    movl    8(%ebp), %eax   
11.    movl    $_ZTV4Base+8, (%eax)   
12.    popl    %ebp   
13.    ret   
14.    .cfi_endproc   
_ZN4BaseC1Ev:
.LFB6:
    .cfi_startproc
    .cfi_personality 0×0,__gxx_personality_v0
    pushl   %ebp
    .cfi_def_cfa_offset 8
    movl    %esp, %ebp
    .cfi_offset 5, -8
    .cfi_def_cfa_register 5
    movl    8(%ebp), %eax
    movl    $_ZTV4Base+8, (%eax)
    popl    %ebp
    ret
    .cfi_endproc 

ZN4BaseC1Ev这个符号是C++函数Base::Base() 的内部符号名,可使用c++flit将它还原。C++里的class,可以定义数据成员,函数成员两种。但转化到汇编层面时,每个对象里面真正存放的是数据成员,以及虚函数表。

在上面的Base类中,由于没有数据成员,因此它只有一个vtable指针。故Base类的定义,可以写成如下相应的C代码:

01.struct Base {   
02.    unsigned long **vtable;   
03.}   
struct Base {
    unsigned long **vtable;
}  
构造函数中最关键的两句是:

movl    8(%ebp), %eax 
movl    $_ZTV4Base+8, (%eax)

$_ZTV4Base+8 就是Base类的虚函数表的开始位置,因此,构造函数对应的C代码如下:

01.void Base::Base(struct Base *this)   
02.{   
03.    this->vtable = &Base_vtable;   
04.}  
void Base::Base(struct Base *this)
{
    this->vtable = &Base_vtable;
}

同样地,Derive类的构造函数如下:

01.struct Derive {   
02.    unsigned long **vtable;   
03.};   
04.void Derive::Derive(struct Derive *this)   
05.{   
06.    this->vtable = &Derive_vtable;   
07.}  
struct Derive {
    unsigned long **vtable;
};
void Derive::Derive(struct Derive *this)
{
    this->vtable = &Derive_vtable;
}

4. 实现运行时多态的最关键一步

在构造函数里面设置好的vtable的值,显然,同一类型所有对象内的vtable值都是一样的,并且永远不会改变。下面是main函数生成的汇编代码,它展示了C++如何利用vtable来实现运行时多态。

01..globl main   
02.    .type   main, @function   
03.main:   
04..LFB3:   
05.    .cfi_startproc   
06.    .cfi_personality 0×0,__gxx_personality_v0   
07.    pushl   %ebp   
08.    .cfi_def_cfa_offset 8   
09.    movl    %esp, %ebp   
10.    .cfi_offset 5, -8   
11.    .cfi_def_cfa_register 5   
12.    andl    $-16, %esp   
13.    subl    $32, %esp   
14.    leal    24(%esp), %eax   
15.    movl    %eax, (%esp)   
16.    call    _ZN6DeriveC1Ev   
17.    leal    24(%esp), %eax   
18.    movl    %eax, 28(%esp)   
19.    movl    28(%esp), %eax   
20.    movl    (%eax), %eax   
21.    movl    (%eax), %edx   
22.    movl    28(%esp), %eax   
23.    movl    %eax, (%esp)   
24.    call    *%edx   
25.    movl    $0, %eax   
26.    leave   
27.    ret    
28.    .cfi_endproc  
.globl main
    .type   main, @function
main:
.LFB3:
    .cfi_startproc
    .cfi_personality 0×0,__gxx_personality_v0
    pushl   %ebp
    .cfi_def_cfa_offset 8
    movl    %esp, %ebp
    .cfi_offset 5, -8
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    leal    24(%esp), %eax
    movl    %eax, (%esp)
    call    _ZN6DeriveC1Ev
    leal    24(%esp), %eax
    movl    %eax, 28(%esp)
    movl    28(%esp), %eax
    movl    (%eax), %eax
    movl    (%eax), %edx
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    *%edx
    movl    $0, %eax
    leave
    ret 
    .cfi_endproc  

    andl    $-16, %esp 
    subl    $32, %esp

这两句是为局部变量d和bp在堆栈上分配空间,也即如下的语句: 
Derive d;   
Base *pb;

leal    24(%esp), %eax 
movl    %eax, (%esp) 
call    _ZN6DeriveC1Ev

esp+24是变量d的首地址,先将它压到堆栈上,然后调用d的构造函数,相应翻译成C语言则如下:

Derive::Dervice(&d);

leal    24(%esp), %eax 
movl    %eax, 28(%esp)

这里其实是将&d的值赋给pb,也即: 
pb = &d; 
最关键的代码是下面这一段:

01.movl    28(%esp), %eax   
02.movl    (%eax), %eax   
03.movl    (%eax), %edx   
04.movl    28(%esp), %eax   
05.movl    %eax, (%esp)   
06.call    *%edx  
movl    28(%esp), %eax
movl    (%eax), %eax
movl    (%eax), %edx
movl    28(%esp), %eax
movl    %eax, (%esp)
call    *%edx

翻译成C语言也就传神的那句: 
pb->vtable[0](bp);

编译器会记住f虚函数放在vtable的第0项,这是编译时信息。

5. 小结 
这里省略了很多关于编译器和C++的细枝未节,是出于讨论方便用的需要。从上面的编译代码可以看到以下信息: 
1.每个类都有各有的vtable结构,编译会正确填写它们的虚函数表 
2. 对象在构造函数时,设置vtable值为该类的虚函数表 
3.在指针或者引用时调用虚函数,是通过object->vtable加上虚函数的offset来实现的。 
当然这仅仅是g++的实现方式,它和VC++的略有不同,但原理是一样的。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值