虚函数

发布时间 2023-07-11 16:33:38作者: amazzzzzing

虚函数

虚函数表

示例

// 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

一些结论:

  • 虚表指针是在对象的构造函数中赋值的;
  • 继承关系中,虚表指针会被多次赋值;
  • 虚函数的调用增加了利用虚表指针取得虚函数地址的过程;