Last Updated: 2023-12-28 03:49:33 Thursday
-- TOC --
让我们通过对汇编的分析,来探寻一下C++虚函数表vtable的本质。
测试用C++代码如下:
#include <iostream>
struct tom{
int a;
int b;
void f0(){}
virtual void f1(){}
virtual void f2(){}
};
int main(){
tom t{};
t.f0();
t.f1();
t.f2();
std::cout << t.a << t.b << std::endl;
std::cout << sizeof(tom); // 16
return 0;
}
tom类型t,有1个普通成员函数f0,两个虚函数f1和f2。
含有虚函数的class,sizeof要大8bytes!
汇编如下:
0000000000401186 <main>:
401186: 55 push rbp
401187: 48 89 e5 mov rbp,rsp
# 这16bytes,都属于t对象
40118a: 48 83 ec 10 sub rsp,0x10
40118e: 48 c7 45 f0 00 00 00 mov QWORD PTR [rbp-0x10],0x0
401195: 00
401196: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0
40119d: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
# 用stack上的地址,创建tom对象t
4011a4: 48 8d 45 f0 lea rax,[rbp-0x10]
4011a8: 48 89 c7 mov rdi,rax
4011ab: e8 de 00 00 00 call 40128e <tom::tom()>
4011b0: 48 8d 45 f0 lea rax,[rbp-0x10]
4011b4: 48 89 c7 mov rdi,rax
4011b7: e8 ae 00 00 00 call 40126a <tom::f0()>
4011bc: 48 8d 45 f0 lea rax,[rbp-0x10]
4011c0: 48 89 c7 mov rdi,rax
4011c3: e8 ae 00 00 00 call 401276 <tom::f1()>
4011c8: 48 8d 45 f0 lea rax,[rbp-0x10]
4011cc: 48 89 c7 mov rdi,rax
4011cf: e8 ae 00 00 00 call 401282 <tom::f2()>
4011d4: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
4011d7: 89 c6 mov esi,eax
4011d9: bf 80 40 40 00 mov edi,0x404080
4011de: e8 9d fe ff ff call 401080 <std::ostream::operator<<(int)@plt>
4011e3: 48 89 c2 mov rdx,rax
4011e6: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
4011e9: 89 c6 mov esi,eax
4011eb: 48 89 d7 mov rdi,rdx
4011ee: e8 8d fe ff ff call 401080 <std::ostream::operator<<(int)@plt>
4011f3: be 40 10 40 00 mov esi,0x401040
4011f8: 48 89 c7 mov rdi,rax
4011fb: e8 60 fe ff ff call 401060 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))@plt>
401200: be 10 00 00 00 mov esi,0x10
401205: bf 80 40 40 00 mov edi,0x404080
40120a: e8 21 fe ff ff call 401030 <std::ostream::operator<<(unsigned long)@plt>
40120f: b8 00 00 00 00 mov eax,0x0
401214: c9 leave
401215: c3 ret
000000000040126a <tom::f0()>:
40126a: 55 push rbp
40126b: 48 89 e5 mov rbp,rsp
40126e: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi
401272: 90 nop
401273: 5d pop rbp
401274: c3 ret
401275: 90 nop
0000000000401276 <tom::f1()>:
401276: 55 push rbp
401277: 48 89 e5 mov rbp,rsp
40127a: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi
40127e: 90 nop
40127f: 5d pop rbp
401280: c3 ret
401281: 90 nop
0000000000401282 <tom::f2()>:
401282: 55 push rbp
401283: 48 89 e5 mov rbp,rsp
401286: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi
40128a: 90 nop
40128b: 5d pop rbp
40128c: c3 ret
40128d: 90 nop
000000000040128e <tom::tom()>:
40128e: 55 push rbp
40128f: 48 89 e5 mov rbp,rsp
401292: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi
# 0x402020是tom类型vtable地址+16bytes,
# 这个值,被放入了this指针指向的地址,这就是__vptr,
# 这就是有虚函数的class,sizeof要多8bytes的原因,
# 创建含有虚函数的对象,编译器就会将vtable存入this指针指向的位置
401296: ba 20 20 40 00 mov edx,0x402020
40129b: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
40129f: 48 89 10 mov QWORD PTR [rax],rdx
4012a2: 90 nop
4012a3: 5d pop rbp
4012a4: c3 ret
分析汇编,看到的第1个问题是,那个0x402020地址是什么?
这个地址位于ELF文件的.rodata区域:
$ readelf -x16 a.out
Hex dump of section '.rodata':
0x00402000 01000200 00000000 00000000 00000000 ................
0x00402010 00000000 00000000 30204000 00000000 ........0 @.....
0x00402020 76124000 00000000 82124000 00000000 v.@.......@.....
0x00402030 a03d4000 00000000 40204000 00000000 .=@.....@ @.....
0x00402040 33746f6d 00 3tom.
这里存放的,就是class tom的vtable。(vtable在编译时生成,存放在.rodata section)
用Compiler Explorer看直观一些(要在output中打开Demangle identifiers开关):
tom::tom() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov edx, OFFSET FLAT:vtable for tom+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
pop rbp
ret
vtable for tom:
.quad 0
.quad typeinfo for tom
.quad tom::f1()
.quad tom::f2()
typeinfo for tom:
.quad vtable for __cxxabiv1::__class_type_info+16
.quad typeinfo name for tom
typeinfo name for tom:
.string "3tom"
vtable+16,就是tom::f1(),0x401276。
这个case虽然给class定义了虚函数,但最后生成的汇编,并没有看到动态绑定的影子!应该是因为这个case没有inherit和override。
测试用C++代码:
#include <iostream>
struct tom{
int a;
int b;
void f0(){}
virtual void f1(){}
virtual void f2(){}
};
struct jack: tom{
void f1() override{}
};
int main(){
jack j{};
tom *t { &j };
t->f0();
t->f1();
t->f2();
return 0;
}
汇编如下:
tom::f0():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
tom::f1():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
tom::f2():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
jack::f1():
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
tom::tom() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov edx, OFFSET FLAT:vtable for tom+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
pop rbp
ret
jack::jack() [base object constructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call tom::tom() [base object constructor]
mov edx, OFFSET FLAT:vtable for jack+16
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx
nop
leave
ret
main:
push rbp
mov rbp, rsp
sub rsp, 32
mov QWORD PTR [rbp-32], 0
mov DWORD PTR [rbp-24], 0
mov DWORD PTR [rbp-20], 0
lea rax, [rbp-32]
mov rdi, rax
call jack::jack() [complete object constructor]
lea rax, [rbp-32]
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
# 直接调用f0
call tom::f0()
mov rax, QWORD PTR [rbp-8]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
# rdx是this指针的内容,f1
call rdx
mov rax, QWORD PTR [rbp-8]
mov rax, QWORD PTR [rax]
# this+8,指向f2
add rax, 8
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call rdx
mov eax, 0
leave
ret
vtable for jack:
.quad 0
.quad typeinfo for jack
.quad jack::f1()
.quad tom::f2()
vtable for tom:
.quad 0
.quad typeinfo for tom
.quad tom::f1()
.quad tom::f2()
typeinfo for jack:
.quad vtable for __cxxabiv1::__si_class_type_info+16
.quad typeinfo name for jack
.quad typeinfo for tom
typeinfo name for jack:
.string "4jack"
typeinfo for tom:
.quad vtable for __cxxabiv1::__class_type_info+16
.quad typeinfo name for tom
typeinfo name for tom:
.string "3tom"
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L11
cmp DWORD PTR [rbp-8], 65535
jne .L11
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
call __cxa_atexit
.L11:
nop
leave
ret
_GLOBAL__sub_I_main:
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
jack继承tom,重写f1,编译器为jack生成的vtable如下:
vtable for jack:
.quad 0
.quad typeinfo for jack
.quad jack::f1()
.quad tom::f2()
指向jack vtable的指针,存放在j对象this位置。(创建jack对象时,先调用tom的ctor,用到了tom的vtable,但调用结束后,jack的this指针位置,就被jack的vtable覆盖)
直接调用f0,通过vtable调用f1和f2,对象的vtable就存放在对象this指针的位置,vtable的内容由编译器生成。编译器为每个有虚函数的class生成一个vtable,实例化的相同class的对象,指向同一个vtable。
关于this指针的思考
this指针是个神奇的存在,我们一般在代码中,有如下两种使用方式:
// return this object's reference,
// in copy/move assignment interface.
return *this;
// access member
this->member = value;
在C++中,this是默认的,固定的。固定用this这个名称,固定作为成员函数接口的第一个入参,不用把它写出来,它一直都在那里,它的工作由编译器完成。当使用*this
时,基本都是将其cast为对象的引用,估计这样设计是有语义上的考虑,*this
表示对象,而this是对象的地址,对象的引用在语法上就是对象别名,而不是指针地址。恰巧,这里出现了一个空间,通过this访问member,都需要显示的将其member name写出来,如果将这些member的存储,做一点偏移,并不会有任何影响。而为vtable偏移出来的这8个字节,刚好用来存放vtable的地址。每个对象的vtable在编译之后,都存放.rodata区域。
所以,我们可以在代码中获取对象vtable的地址,即(void*)*this
!
vtable机制对运行时性能的影响很轻微:
This virtual call mechanism can be made almost as efficient as the “normal function call” mechanism (within 25%). Its space overhead is one pointer in each object of a class with virtual functions plus one vtbl for each such class.
normal function call的地址是直接的,编译器已经完成了地址分配。而virtual function call,需要在vtable的地址上,多计算一次偏移(偏移距离编译后固定)和一次dereference。空间上,每个含有virtual function定义的对象,多8字节用来存放vtable地址,在.rodata区域,每个对象的每个virtual function使用8个字节来保存其地址。
virtual destructor的设计原理
virtual interface class一般都要写上一个virtual destructor,而且还没有=0
,不纯虚!为什么?原因如下:
当一个interface class类型的指针被delete的时候,vtable的机制就保证了,这个指针指向的具体对象的destructor能够被调用。
- An abstract class typically doesn’t need a constructor. ---- 《A Tour of C++》
- A class with a virtual function should have a virtual destructor. ---- 《A Tour of C++》
本文链接:https://cs.pynote.net/hd/asm/202302231/
-- EOF --
-- MORE --