虚函数
虚函数表
示例
// code of virtual function
// filename: test.cpp
#include <stdio.h>
class A
{
public:
virtual ~A() {}
void draw(){draw_imp();}
protected:
virtual void draw_imp(){}
};
class B : public A
{
public:
protected:
void draw_imp() override{}
};
int main()
{
A *a = new B();
a->draw();
delete a;
return 0;
}
g++ test.cpp --dump-lang-class
cat a-*
得到信息为
Vtable for A
A::_ZTV1A: 5 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A)
16 (int (*)(...))A::~A
24 (int (*)(...))A::~A
32 (int (*)(...))A::draw_imp
Class A
size=8 align=8
base size=8 base align=8
A (0x0x7f223519eb40) 0 nearly-empty
vptr=((& A::_ZTV1A) + 16)
Vtable for B
B::_ZTV1B: 5 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1B)
16 (int (*)(...))B::~B
24 (int (*)(...))B::~B
32 (int (*)(...))B::draw_imp
Class B
size=8 align=8
base size=8 base align=8
B (0x0x7f2235043270) 0 nearly-empty
vptr=((& B::_ZTV1B) + 16)
A (0x0x7f223519ef00) 0 nearly-empty
primary-for B (0x0x7f2235043270)
可以看到,两个存在继承关系的类,其虚函数表中的函数(析构函数和draw)分别为各自类的实现;
注:虚函数表中一些特殊的结构,比如16字节偏移、对齐、两个析构函数,这些都是ABI的要求,不必关注;
虚函数的原理
汇编分析
g++ test.cpp -S
cat test.s
摘录其中一部分分析虚函数的调用过程(移除了大部分特殊指示代码):
_ZN1A4drawEv: # A::draw()
pushq %rbp # 栈基址
movq %rsp, %rbp # 新的栈基址
subq $16, %rsp
subq $16, %rsp # 栈共增长32Byte
movq %rdi, -8(%rbp) # this --> 栈第一个8字节
movq -8(%rbp), %rax # this --> rax
movq (%rax), %rax # *rax --> rax :this的首部就是虚函数表的指针地址,8字节)
addq $16, %rax # rax偏移16字节,考虑到虚表本身偏移16,即总共偏移为32,指向draw_imp
movq (%rax), %rdx # *rax --> rax :获取函数地址(draw_imp)
movq -8(%rbp), %rax #
movq %rax, %rdi
call *%rdx # 调用(draw_imp)
nop
leave
ret
_ZN1AC2Ev: # A::A()
endbr64
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp) # this指针放到栈的第一个8字节
leaq 16+_ZTV1A(%rip), %rdx # rdx = 虚表首部地址 + 16
movq -8(%rbp), %rax # rax = this
movq %rdx, (%rax) # *this = rdx ,即类A的虚表指针放到对象的第一个8字节
nop
popq %rbp
ret
.LFE10:
.set _ZN1AC1Ev,_ZN1AC2Ev
_ZN1BC2Ev: # B::B()
endbr64
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq %rdi, -8(%rbp) # this指针放到栈的第一个8字节
movq -8(%rbp), %rax # rax = this
movq %rax, %rdi # rdi = this
call _ZN1AC2Ev # A::A()
leaq 16+_ZTV1B(%rip), %rdx # 类B的虚表指针存放到rdx
movq -8(%rbp), %rax # rax = this
movq %rdx, (%rax) # 类B的虚表指针存放到this对象的第一个8字节(覆盖了A产生的虚表指针)
nop
leave
ret
.LFE12:
.set _ZN1BC1Ev,_ZN1BC2Ev
main:
endbr64
pushq %rbp # 栈基址
movq %rsp, %rbp # 新的栈基址
pushq %rbx #
subq $24, %rsp # 栈增长24字节
movl $8, %edi # edi = 8
call _Znwm@PLT # call new (size = 8)
movq %rax, %rbx # rax --> rbx(new得到的指针)
movq $0, (%rbx) # *rbx = 0
movq %rbx, %rdi # rbx --> rdi
call _ZN1BC1Ev # call B::B()
movq %rbx, -24(%rbp) # rbx 放到栈的第一个8字节(new得到的指针)
movq -24(%rbp), %rax # rax = (new得到的指针)
movq %rax, %rdi # rdx = (new得到的指针)
call _ZN1A4drawEv # call A::draw()
movq -24(%rbp), %rax # rax = (new得到的指针)
testq %rax, %rax
movq (%rax), %rdx # rdx = 虚表指针(B的虚表),vtable + 16
addq $8, %rdx # rdx = vtable + 24
movq (%rdx), %rdx # rdx = *(vtable + 24), i.e. B::~B()
movq %rax, %rdi
call *%rdx # call B::~B()
movl $0, %eax
movq -8(%rbp), %rbx
leave
ret
一些结论:
- 虚表指针是在对象的构造函数中赋值的;
- 继承关系中,虚表指针会被多次赋值;
- 虚函数的调用增加了利用虚表指针取得虚函数地址的过程;