0%

从反汇编看编译器实现c++虚函数的原理

测试源码与对应的反汇编代码

考虑如下一段c++源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class Base {
public:
virtual void show() {
std::cout << "Base class show function" << std::endl;
}
};

class Derived : public Base {
public:
void show() override {
std::cout << "Derived class show function" << std::endl;
}
};

int main() {
Base* basePtr = new Base();
Base* derivedPtr = new Derived();

basePtr->show();
derivedPtr->show();

delete basePtr;
delete derivedPtr;

return 0;
}
在bebug模式下 对应如下反汇编
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Dump of assembler code for function main():
0x0000590334da51e9 <+0>: endbr64
0x0000590334da51ed <+4>: push rbp
0x0000590334da51ee <+5>: mov rbp,rsp
0x0000590334da51f1 <+8>: push rbx
0x0000590334da51f2 <+9>: sub rsp,0x18
0x0000590334da51f6 <+13>: mov edi,0x8
0x0000590334da51fb <+18>: call 0x590334da50c0 <_Znwm@plt>
0x0000590334da5200 <+23>: mov rbx,rax
0x0000590334da5203 <+26>: mov QWORD PTR [rbx],0x0
0x0000590334da520a <+33>: mov rdi,rbx
0x0000590334da520d <+36>: call 0x590334da5372 <_ZN4BaseC2Ev>
0x0000590334da5212 <+41>: mov QWORD PTR [rbp-0x20],rbx
0x0000590334da5216 <+45>: mov edi,0x8
0x0000590334da521b <+50>: call 0x590334da50c0 <_Znwm@plt>
0x0000590334da5220 <+55>: mov rbx,rax
0x0000590334da5223 <+58>: mov QWORD PTR [rbx],0x0
0x0000590334da522a <+65>: mov rdi,rbx
0x0000590334da522d <+68>: call 0x590334da5390 <_ZN7DerivedC2Ev>
0x0000590334da5232 <+73>: mov QWORD PTR [rbp-0x18],rbx
0x0000590334da5236 <+77>: mov rax,QWORD PTR [rbp-0x20]
0x0000590334da523a <+81>: mov rax,QWORD PTR [rax]
0x0000590334da523d <+84>: mov rdx,QWORD PTR [rax]
0x0000590334da5240 <+87>: mov rax,QWORD PTR [rbp-0x20]
0x0000590334da5244 <+91>: mov rdi,rax
0x0000590334da5247 <+94>: call rdx
0x0000590334da5249 <+96>: mov rax,QWORD PTR [rbp-0x18]
0x0000590334da524d <+100>: mov rax,QWORD PTR [rax]
0x0000590334da5250 <+103>: mov rdx,QWORD PTR [rax]
0x0000590334da5253 <+106>: mov rax,QWORD PTR [rbp-0x18]
0x0000590334da5257 <+110>: mov rdi,rax
0x0000590334da525a <+113>: call rdx
0x0000590334da525c <+115>: mov rax,QWORD PTR [rbp-0x20]
0x0000590334da5260 <+119>: test rax,rax
0x0000590334da5263 <+122>: je 0x590334da5272 <main()+137>
0x0000590334da5265 <+124>: mov esi,0x8
0x0000590334da526a <+129>: mov rdi,rax
0x0000590334da526d <+132>: call 0x590334da50d0 <_ZdlPvm@plt>
0x0000590334da5272 <+137>: mov rax,QWORD PTR [rbp-0x18]
0x0000590334da5276 <+141>: test rax,rax
0x0000590334da5279 <+144>: je 0x590334da5288 <main()+159>
0x0000590334da527b <+146>: mov esi,0x8
0x0000590334da5280 <+151>: mov rdi,rax
0x0000590334da5283 <+154>: call 0x590334da50d0 <_ZdlPvm@plt>
0x0000590334da5288 <+159>: mov eax,0x0
0x0000590334da528d <+164>: add rsp,0x18
0x0000590334da5291 <+168>: pop rbx
0x0000590334da5292 <+169>: pop rbp
0x0000590334da5293 <+170>: ret
End of assembler dump.

详细分析

  1. 函数栈帧初始化 更新与设置好当前函数栈空间
    1
    2
    3
    4
    5
    0x0000590334da51e9 <+0>:	endbr64 
    0x0000590334da51ed <+4>: push rbp
    0x0000590334da51ee <+5>: mov rbp,rsp
    0x0000590334da51f1 <+8>: push rbx
    0x0000590334da51f2 <+9>: sub rsp,0x18
  2. 构造basePtr
1
2
3
4
5
6
7
0x0000590334da51f6 <+13>:	mov    edi,0x8                      ;申请8字节空间
0x0000590334da51fb <+18>: call 0x590334da50c0 <_Znwm@plt> ;调用申请空间的函数 申请空间的地址随函数返回存在rax中
0x0000590334da5200 <+23>: mov rbx,rax ;rbx = rax = 申请到的堆空间的地址指针
0x0000590334da5203 <+26>: mov QWORD PTR [rbx],0x0 ;申请到的堆空间的地址指针的前8个字节内容清零
0x0000590334da520a <+33>: mov rdi,rbx ;rdi = 申请到的堆空间的地址指针 作为Base::Base()构造函数的this指针
0x0000590334da520d <+36>: call 0x590334da5372 <_ZN4BaseC2Ev>;调用Base::Base()构造函数
0x0000590334da5212 <+41>: mov QWORD PTR [rbp-0x20],rbx ;

Base类没有成员函数,但是由于包含了虚函数,所以类中需要储存一个虚函数表对应的指针,64为机上一个指针8个字节,所以需要向系统申请8字节大小的空间。 tips: c++flit,指令可以将编译器按特定规则重命名后的函数名翻译回原始字符串,比如 c++filt _ZN4BaseC2Ev,将返回Base::Base()

其中Base::Base()的构造函数是编译器生成的 其汇编代码为:

1
2
3
4
5
6
7
8
9
10
11
12
Dump of assembler code for function _ZN4BaseC2Ev:
0x00005db79e2ed372 <+0>: endbr64
0x00005db79e2ed376 <+4>: push rbp
0x00005db79e2ed377 <+5>: mov rbp,rsp
0x00005db79e2ed37a <+8>: mov QWORD PTR [rbp-0x8],rdi
0x00005db79e2ed37e <+12>: lea rdx,[rip+0x29cb] # 0x5db79e2efd50 <_ZTV4Base+16> ;取base的虚表指针
0x00005db79e2ed385 <+19>: mov rax,QWORD PTR [rbp-0x8]
0x00005db79e2ed389 <+23>: mov QWORD PTR [rax],rdx ;将虚表指针存在头部
0x00005db79e2ed38c <+26>: nop
0x00005db79e2ed38d <+27>: pop rbp
0x00005db79e2ed38e <+28>: ret
End of assembler dump.
3. 构造derivedPtr 原理一致,先在堆上申请空间,然后进行初始化,其中编译器生成的派生类的构造函数的反汇编代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dump of assembler code for function _ZN7DerivedC2Ev:
0x00005c695ab1f344 <+0>: endbr64
0x00005c695ab1f348 <+4>: push rbp
0x00005c695ab1f349 <+5>: mov rbp,rsp
0x00005c695ab1f34c <+8>: sub rsp,0x10
0x00005c695ab1f350 <+12>: mov QWORD PTR [rbp-0x8],rdi
0x00005c695ab1f354 <+16>: mov rax,QWORD PTR [rbp-0x8]
0x00005c695ab1f358 <+20>: mov rdi,rax
0x00005c695ab1f35b <+23>: call 0x5c695ab1f326 <_ZN4BaseC2Ev> ;调用基类的构造函数
0x00005c695ab1f360 <+28>: lea rdx,[rip+0x29d9] # 0x5c695ab21d40 <_ZTV7Derived+16> ;取Derived的虚表指针
0x00005c695ab1f367 <+35>: mov rax,QWORD PTR [rbp-0x8]
0x00005c695ab1f36b <+39>: mov QWORD PTR [rax],rdx ;将虚表指针存在头部
0x00005c695ab1f36e <+42>: nop
0x00005c695ab1f36f <+43>: leave
0x00005c695ab1f370 <+44>: ret
End of assembler dump.

可以看到,子类调用了基类的构造函数,同时覆盖掉了自己被基类构造函数构造好的虚函数表指针

虚函数调用过程

1
2
3
4
5
6
0x0000590334da5236 <+77>:	mov    rax,QWORD PTR [rbp-0x20] ;从栈上取基类的this指针
0x0000590334da523a <+81>: mov rax,QWORD PTR [rax] ;将this指针的起始位置即虚表指针转载rax寄存器
0x0000590334da523d <+84>: mov rdx,QWORD PTR [rax] ;将虚表指针的其实位置的地址转载到rdx寄存器 即为show()函数的地址 应为案例只有一个虚函数 所以这个直接就是rax+0 如果有两个的话第二个虚函数应该就是[rax + 8]
0x0000590334da5240 <+87>: mov rax,QWORD PTR [rbp-0x20] ;重新转载this指针到rax寄存器 准备函数调用的参数
0x0000590334da5244 <+91>: mov rdi,rax ;this指针装载rdi寄存器
0x0000590334da5247 <+94>: call rdx ;调用rdx函数

可以看到这里的虚函数调用不是直接通过call函数地址直接调用的,而是通过相对于虚表指针的便宜地址实现的函数调用,编译器只要保证对于子类和基类相同的函数名所对应的函数相对各自虚表的偏移地址是一致的,就可以实现运行时多态了。