首页 > 其他分享 >再探虚函数

再探虚函数

时间:2024-05-21 15:09:06浏览次数:22  
标签:函数 rdx rbp call 探虚 rax movq

虚函数是一种成员函数,其行为可以在派生类中被覆盖,支持动态调用派发。

使用示例代码如下:

extern "C" {
// 避免 operator<< 多次调用,简化汇编代码
void println(const char *s) { std::cout << s << std::endl; }
}

void *operator new(size_t n) {
    void *p = malloc(n);
    std::cout << "new " << n << " ptr " << p << std::endl;
    return p;
}

void operator delete(void *ptr) noexcept {
    std::cout << "delete " << ptr << std::endl;
    free(ptr);
}

struct Fd {
    Fd() : fd_(12345) {}
    virtual ~Fd() { println("Fd::~Fd"); }
    virtual void close() { println("Fd::close"); }
    void setsockopt() { println("Fd::setsockopt"); }

    int fd_;
};

struct Conn : public Fd {
    virtual ~Conn() { println("Conn::~Conn"); }
    virtual void connect() { println("Conn::connect"); }
};

struct TCPConn : public Conn {
    ~TCPConn() { println("TCPConn::~TCPConn"); }
    void connect() override { println("TCPConn::connect"); }
    void local_addr() {}
};

struct UDPConn : public Conn {
    ~UDPConn() { println("UDPConn::~UDPConn"); }
    void connect() override { println("UDPConn::connect"); }
    void local_addr() {}
};

typedef void (*Fn)(void *);

template <typename T>
void call_by_vtable(T *v, size_t idx) {
    uintptr_t *vtable_ptr = reinterpret_cast<uintptr_t *>(*reinterpret_cast<uintptr_t *>(v));
    // std::cout << "vtable address " << vtable_ptr << std::endl;

    void *vtable_entry_ptr = reinterpret_cast<void *>(*(vtable_ptr + idx));
    // std::cout << "  vtable entry " << idx << "th address " << vtable_entry_ptr << std::endl;

    Fn f = (Fn)vtable_entry_ptr;
    f(v);
}

int main() {
    Fd *conn = new TCPConn;
    conn->close();
    conn->setsockopt();
    delete conn;

    println("");

    conn = new UDPConn;
    call_by_vtable(conn, 1);
}

虚表

虚表是在编译期进行生成,在构造函数内设置对象的虚表地址,先于成员对象的初始化。

通过反汇编 TCPConn 相关代码,得到虚表的定义如下(比实际的要大一些,对象的虚表起始地址一般有两个偏移)

	.weak	_ZTV7TCPConn
	.section	.data.rel.ro.local._ZTV7TCPConn,"awG",@progbits,_ZTV7TCPConn,comdat
	.align 8
	.type	_ZTV7TCPConn, @object
	.size	_ZTV7TCPConn, 48
_ZTV7TCPConn:
	.quad	0
	.quad	_ZTI7TCPConn
	.quad	_ZN7TCPConnD1Ev
	.quad	_ZN7TCPConnD0Ev
	.quad	_ZN2Fd5closeEv
	.quad	_ZN7TCPConn7connectEv

虚表大小为 48 (_.size ZTV7TCPConn, 48),分别是

  • 0 填充
  • _ZTI7TCPConn 构造函数
  • _ZN7TCPConnD1Ev 普通的析构函数函数
  • _ZN7TCPConnD0Ev 调用 delete 的析构函数
  • _ZN2Fd5closeEv 成员函数 close,继承自父类
  • _ZN7TCPConn7connectEv 成员函数 connect

构造函数的汇编代码如下

	.section	.text._ZN7TCPConnC2Ev,"axG",@progbits,_ZN7TCPConnC5Ev,comdat
	.align 2
	.weak	_ZN7TCPConnC2Ev
	.type	_ZN7TCPConnC2Ev, @function
_ZN7TCPConnC2Ev:
.LFB1796:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	_ZN4ConnC2Ev
	leaq	16+_ZTV7TCPConn(%rip), %rdx
	movq	-8(%rbp), %rax
	movq	%rdx, (%rax)
	nop
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1796:
	.size	_ZN7TCPConnC2Ev, .-_ZN7TCPConnC2Ev
	.weak	_ZN7TCPConnC1Ev
	.set	_ZN7TCPConnC1Ev,_ZN7TCPConnC2Ev
  • 调用父类构造函数
  • 设置虚表指针,将虚表第三个条目的地址(_ZTV7TCPConn+16)也就是 _ZN7TCPConnD1Ev 的地址存在 this 指针的内存中 movq %rdx, (%rax)
leaq	16+_ZTV7TCPConn(%rip), %rdx
movq	-8(%rbp), %rax
movq	%rdx, (%rax)

虚函数调用

虚函数的调用需要查询虚函数表,找到对应的函数地址进行调用。

使用 conn->close() 来说明,其部分反汇编代码如下:

movq	-24(%rbp), %rax  ; this 指针
movq	(%rax), %rax     ; 解地址引用,取到对象虚表地址,地址为 _ZN7TCPConnD1Ev
addq	$16, %rax        ; 取对象虚表的第三个条目
movq	(%rax), %rdx     ; 解地址引用得到 _ZN2Fd5closeEv
movq	-24(%rbp), %rax
movq	%rax, %rdi       ; 将 this 指针作为第一个参数
call	*%rdx            ; 调用 Fd::close

非虚函数调用

普通函数调用 conn->setsockopt(); 无额外动作,其汇编代码如下

movq	-24(%rbp), %rax
movq	%rax, %rdi
call	_ZN2Fd10setsockoptEv

析构函数

TCPConn 反汇编看到实现为两个函数,

  • 普通的析构函数
  • 额外调用 delete 的析构函数

为保持可读性,删除了部分 cfi 等内容,一下为汇编代码

	.align 2
	.weak	_ZN7TCPConnD2Ev
	.type	_ZN7TCPConnD2Ev, @function
_ZN7TCPConnD2Ev:
.LFB1779:
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)
	leaq	16+_ZTV7TCPConn(%rip), %rdx
	movq	-8(%rbp), %rax
	movq	%rdx, (%rax)
	leaq	.LC7(%rip), %rax
	movq	%rax, %rdi
	call	println
	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	_ZN4ConnD2Ev
	nop
	leave
	ret

	.weak	_ZN7TCPConnD1Ev
	.set	_ZN7TCPConnD1Ev,_ZN7TCPConnD2Ev
	.type	_ZN7TCPConnD0Ev, @function
_ZN7TCPConnD0Ev:
.LFB1781:
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	_ZN7TCPConnD1Ev
	movq	-8(%rbp), %rax
	movl	$16, %esi
	movq	%rax, %rdi
	call	_ZdlPvm@PLT
	leave
	ret

析构函数的实现和虚成员函数没有区别,都是偏移查表然后调用。

movq	(%rax), %rdx
addq	$8, %rdx
movq	(%rdx), %rdx
movq	%rax, %rdi
call	*%rdx

注意这里偏移为 8,也就是将调用 _ZN7TCPConnD0Ev

观察 _ZN7TCPConnD0Ev 的实现,先调用普通的析构函数 _ZN7TCPConnD1Ev ,然后再调用 _ZdlPvm@PLT 也就是 operator delete

在析构函数的调用链中,不再进行查表的动作,也就是说整个 delete 过程只需要查一次虚表,两次解指针引用

  • 获取虚表地址
  • 获取虚函数地址

手动通过 this 指针来调用虚函数

针对以上代码,使用 C++ 代码来手动调用虚函数.

typedef void (*Fn)(void *);

template <typename T>
void call_by_vtable(T *v, size_t idx) {
    uintptr_t *vtable_ptr = reinterpret_cast<uintptr_t *>(*reinterpret_cast<uintptr_t *>(v));
    // std::cout << "vtable address " << vtable_ptr << std::endl;

    void *vtable_entry_ptr = reinterpret_cast<void *>(*(vtable_ptr + idx));
    // std::cout << "  vtable entry " << idx << "th address " << vtable_entry_ptr << std::endl;

    Fn f = (Fn)vtable_entry_ptr;
    f(v);
}

T *v 是对象地址,也就是 this 指针。两次解指针引用获取到虚表中第 idx 个虚函数地址。由于示例中所有代码都是无参函数,所以直接传入 this 指针即可进行调用。

测试手动执行虚析构函数

int main() {
    Fd *conn = new TCPConn;
    conn->close();
    conn->setsockopt();
    delete conn;

    println("");

    conn = new UDPConn;
    call_by_vtable(conn, 1);
}

可以看到 delete 和 手动寻址调用 的输出相同

new 16 ptr 0x560d34c13eb0
Fd::close
TCPConn::~TCPConn
Conn::~Conn
Fd::~Fd
delete 0x560d34c13eb0

new 16 ptr 0x560d34c13eb0
UDPConn::~UDPConn
Conn::~Conn
Fd::~Fd
delete 0x560d34c13eb0

虚函数性能

使用简单的一个用例来测试两个函数的性能

static void BM_call_normal_fn(benchmark::State &state) {
    Fd *conn = new TCPConn;
    for (auto _ : state)
        conn->setsockopt();
    delete conn;
}

static void BM_call_virtual_fn(benchmark::State &state) {
    Fd *conn = new TCPConn;
    for (auto _ : state)
        conn->close();
    delete conn;
}

未开启优化的情况下,虚函数的性能大概比普通函数低 30% 左右

2024-05-15T09:34:23+08:00
Running ./benchmark
Run on (20 X 4800 MHz CPU s)
CPU Caches:
  L1 Data 48 KiB (x10)
  L1 Instruction 32 KiB (x10)
  L2 Unified 1280 KiB (x10)
  L3 Unified 25600 KiB (x1)
Load Average: 0.37, 0.19, 0.08
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
-------------------------------------------------------------
Benchmark                   Time             CPU   Iterations
-------------------------------------------------------------
BM_call_normal_fn        1.35 ns         1.35 ns    516029631
BM_call_virtual_fn       1.74 ns         1.74 ns    410454516

使用 O2 的优化选项后,因为是非常简单的函数,普通函数直接被优化了,虚函数相比没有开优化也有提升。

-------------------------------------------------------------
Benchmark                   Time             CPU   Iterations
-------------------------------------------------------------
BM_call_normal_fn       0.000 ns        0.000 ns   1000000000000
BM_call_virtual_fn      0.309 ns        0.309 ns   2213725270

优化的点在于直接把空函数消除,但是虚函数调用那一套绕不过去,只是消除了部分 call 指令。

截取部分 benchmark 的汇编代码,setsockopt 完全被消除,只剩下 new/delete (也就是 _Znwm@PLT 和 _ZdlPvm@PLT)

.L241:
	movq	%rbp, %rdi
	call	_Znwm@PLT
	movq	16(%rbx), %rdx
	movq	%rax, %rcx
	testq	%rdx, %rdx
	jne	.L251
.L242:
	movq	%rcx, 8(%rbx)
	addq	$32, %rbx
	movq	%rbp, -8(%rbx)
	cmpq	%rbx, %r12
	je	.L239
	addq	$8, %rsp
	.cfi_remember_state
	.cfi_def_cfa_offset 40
	movq	%r13, %rsi
	movq	%r12, %rdi
	popq	%rbx
	.cfi_def_cfa_offset 32
	popq	%rbp
	.cfi_def_cfa_offset 24
	popq	%r12
	.cfi_def_cfa_offset 16
	popq	%r13
	.cfi_def_cfa_offset 8
	jmp	_ZdlPvm@PLT

虚函数 close 的反汇编代码如下,可以看到虚函数也被优化了,但是查表的过程被保留。

	call	_Znwm@PLT
	movl	$12345, 8(%rax)
	movq	%rax, %rbx
	leaq	16+_ZTV7TCPConn(%rip), %rax
	movq	%rax, (%rbx)
	movl	28(%rbp), %eax
	testl	%eax, %eax
	je	.L253
	movq	%rbp, %rdi
	call	_ZN9benchmark5State16StartKeepRunningEv@PLT
.L254:
	movq	%rbp, %rdi
	call	_ZN9benchmark5State17FinishKeepRunningEv@PLT
	movq	(%rbx), %rax
	leaq	_ZN7TCPConnD0Ev(%rip), %rdx
	movq	8(%rax), %rax
	cmpq	%rdx, %rax
	jne	.L255
	movq	%rbx, %rdi
	movl	$16, %esi
	popq	%rbx
	.cfi_remember_state
	.cfi_def_cfa_offset 24
	popq	%rbp
	.cfi_def_cfa_offset 16
	popq	%r12
	.cfi_def_cfa_offset 8
	jmp	_ZdlPvm@PLT
	.p2align 4,,10
	.p2align 3
.L253:
	.cfi_restore_state
	movq	16(%rbp), %r12
	movq	%rbp, %rdi
	call	_ZN9benchmark5State16StartKeepRunningEv@PLT
	testq	%r12, %r12
	jns	.L254

所以借助编译器的优化,除非是在CPU密集型的地方,否则虚函数该用还是用,性能损失并不大。

标签:函数,rdx,rbp,call,探虚,rax,movq
From: https://www.cnblogs.com/shuqin/p/18204073

相关文章

  • c++ 结构体的构造函数
    结构体中构造函数1、不使用构造函数1#include<iostream>23structstudent{45intage;6std::stringgender;78}Liu;910intmain(){11Liu.age=20;12Liu.gender="man";1314std::cout<<Liu.age<......
  • Flink富函数
      富函数是DataStreamAPI提供的函数接口,Flink的函数都有它的Rich版本,它与其他函数不同的是,富函数可以获取到运行环境上下文,初始化参数,拥有生命周期方法等,可通过它进行自定义复杂功能。我们常见的如RichMapFunction、RichFilterFunction等。    富函数的生命周期主要通过......
  • 【代码】--库函数学习 temperature.c
    1. 封装的函数   用到了内核中的hwmon子系统,   hwmon子系统作为Linux内核中的一个子系统,用于监控硬件传感器的状态(设备的温度、电压和风扇转速)和提供对硬件传感器的访问接口。   在应用层,对传感器信息的读取,本质上是对驱动中hwmon子系统在注册传感器设备时所......
  • 方法:类似其它语言的函数
    方法:类似其它语言的函数方法的重载的规则:方法名称必须相同。参数列表必须不同(个数不同、或类型不同、参数排列顺序不同等)。​顺序不同是指类型的顺序不同,与你起什么变量名无关,比如inta,intb与intb,inta就是相同顺序方法的返回类型可以相同也可以不相同。......
  • 函数对象、装饰器、闭包函数
    函数对象Python中一切皆对象【1】可以直接被引用定义一个函数用一个新的变量名来存,用新的变量名来调用【2】可以作为元素被储存功能字典的函数地址【3】函数可以作为参数传递给另一个函数将函数的内存地址作为参数传递【4】函数的返回值可以是函数直接将函数的内存地址返......
  • Oracle ORA-06575: 程序包或函数WM_CONCAT处于无效状态
    ------OracleORA-06575:程序包或函数WM_CONCAT处于无效状态----失效原因:版本不支持,WM_CONCAT是oracle的非公开函数,并不鼓励使用,新版本oracle并没有带此函数,需要手工加上。--首先使用dba账号登录oracle数据库sqlplussys/sysassysdba--解锁wmsys用户(可以是你自己定义的......
  • lodash已死?radash库方法介绍及源码解析 —— 函数柯里化 + Number篇
    写在前面tips:点赞+收藏=学会!主页有更多其他篇章的方法,欢迎访问查看。本篇我们继续介绍radash中函数柯里化和Number相关的方法使用和源码解析。函数柯里化chain:创建一个函数链并依次执行使用说明功能描述:用于创建一个函数链,该链依次执行一系列函数,每个函数的输出......
  • 终于搞懂了!原来 Vue 3 的 generate 是这样生成 render 函数的
    前言在之前的面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?文章中讲了transform阶段处理完v-for、v-model等指令后,会生成一棵javascriptAST抽象语法树。这篇文章我们来接着讲generate阶段是如何根据这棵javascriptAST抽象语法树生成render函数字符串的,本文中使用的v......
  • 数据库中了解的知识点:视图、触发器、事务、存储过程、函数、流程控制、索引
    【视图】1什么是视图?2视图就是通过查询得到一张虚拟表,然后保存下来,下次可以直接用3其实视图也是表45为什么要用视图?6如果要频繁的操作一张虚拟表,就可以制作成视图,下次可以直接操作78如何操作9#固定语法10createview......
  • x64 环境下_findnext() 函数报错——0xC0000005: 写入位置 0xFFFFFFFFDF47C5A0 时发生
    CSDN搬家失败,手动导出markdown后再导入博客园最近在搞单目相机位姿估计,相机标定参考了【OpenCV3学习笔记】相机标定函数calibrateCamera()使用详解(附相机标定程序和数据)提供的代码。/*@paramFile_Directory为文件夹目录@paramFileType为需要查找的文件类型@param......