pollux's Dairy

C++虚表杂记

字数统计: 1.2k阅读时长: 7 min
2020/03/30 Share

vptr虚指针被定义在对象首地址的前4个字节处
指针的类型决定了普通函数的调用,指针指向的实际类型决定了虚函数的调用
构造函数不能是虚函数,析构函数最好设置为虚函数
对象的虚表指针初始化是通过编译器在构造函数内插入代码来完成的
对象首地址为this指针
在构造函数和析构函数中调用虚函数会使多态性失效

实例化一个对象时,优先调用父类的构造函数,并以子类对象的首地址作为this指针传递给父类构造函数。在父类的构造函数中,首先会初始化子类虚表指针为父类的虚表首地址(会将虚表指针修改为当前类的虚表指针),接着在子类的构造函数中,会重新写入子类虚表指针为子类的虚表首地址
接着是子类的析构函数,和父类的析构函数,同样会重写子类虚表指针(单一继承时,父类和子类的虚表指针位置一样,即对象首地址的前4字节处),重写虚表指针是为了防止在构造或析构函数中调用子类或父类的虚函数,比如在父类的构造函数中,调用虚函数,如果不更新虚表指针为父类虚表,那么调用了子类的虚函数,但是子类构造函数还没执行,没有初始化虚表指针,这样就会访问错误,在析构函数中原理一样。

总结下构造函数和析构函数的调用顺序,若存在以下继承关系:

1
2
class Bird : public Mammal
class Bat : public Bird : public Mammal

构造:Bird -> Mammal - > Bat
析构:Bat -> Mammal - >Bird

在单一继承中,其子类对象内存首先存放的是父类的数据成员

Q:为什么析构函数要定义为虚函数

因为可以父类指针保存子类对象,如 Mammal *a = new Bird();,如果析构函数没定义为虚函数,释放对象空间时就会直接调用父类的析构函数,子类和父类对象的内存大小不同,释放内存就会产生问题

虚表在内存中的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------+
|offset to top|
+-------------+
| RTTI pointer|
+--------------<-------+ vptr
| Destructor1 |
+-------------+
| Destructor2 |
+-------------+
| fun1 |
+-------------+
| fun2 |
+-------------|

offset_to_top

将对象从当前这个类型转换为该对象的实际类型的地址偏移量,是对象内存中的偏移量,不是虚表内存中的偏移量

1
2
3
4
5
继承关系:Bat : Bird, Mammal

Bat *bat = new Bat();
Mammal *m = bat;
m->walk();

用父类指针保存子类对象,这时在子类对象的内存布局,

1
2
3
4
5
6
7
 0 | class Bat
0 | class Bird (primary base)
0 | (Bird vtable pointer)
4 | int a
8 | class Mammal (base)
8 | (Mammal vtable pointer)
12 | int b

this指向0,但是Mammal对象在偏移8处,需要修正this的位置,查看汇编:

1
2
3
4
5
push    ebx             ; this
call _ZN3BatC2Ev ; Bat::Bat(void)
mov [ebp+var_20], ebx
mov eax, [ebp+var_20]
add eax, 8 ; this = this+8

此时this指向Mammal对象,调用方法:

1
2
3
4
5
6
7
8
mov     [ebp+var_1C], eax
mov eax, [ebp+var_1C]
mov eax, [eax] ; eax = Mammal vptr
add eax, 8 ; eax = vptr + 8
mov eax, [eax] ; walk func addr
sub esp, 0Ch
push [ebp+var_1C]
call eax

Bat虚表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.rodata:08048C68 ; `vtable for'Bat
.rodata:08048C68 _ZTV3Bat dd 0 ; offset to this
.rodata:08048C6C dd offset _ZTI3Bat ; `typeinfo for'Bat
.rodata:08048C70 off_8048C70 dd offset _ZN3BatD2Ev ; DATA XREF: Bat::Bat(void)+28↑o
.rodata:08048C70 ; Bat::~Bat()+6↑o
.rodata:08048C70 ; Bat::~Bat()
.rodata:08048C74 dd offset _ZN3BatD0Ev ; Bat::~Bat()
.rodata:08048C78 dd offset _ZN3Bat3flyEv ; Bat::fly(void)
.rodata:08048C7C dd offset _ZN3Bat4flyyEv ; Bat::flyy(void)
.rodata:08048C80 dd -8 ; offset to this
.rodata:08048C84 dd offset _ZTI3Bat ; `typeinfo for'Bat
.rodata:08048C88 off_8048C88 dd offset _ZThn8_N3BatD1Ev
.rodata:08048C88 ; DATA XREF: Bat::Bat(void)+32↑o
.rodata:08048C88 ; Bat::~Bat()+10↑o
.rodata:08048C88 ; `non-virtual thunk to'Bat::~Bat()
.rodata:08048C8C dd offset _ZThn8_N3BatD0Ev ; `non-virtual thunk to'Bat::~Bat()
.rodata:08048C90 dd offset _ZN6Mammal4walkEv ; Mammal::walk(void)

可以看到次表的offset to top值为-8,即为Bat对象到Mammal对象偏移的负值

在多继承中,由于不同的基类起点可能处于不同的位置,因此当需要将它们转化为实际类型时,this 指针的偏移量也不相同,且由于多态的特性,bat 的实际类型在编译时期是无法确定的;那必然需要一个东西帮助我们在运行时期确定 bat 的实际类型,这个东西就是offset_to_top。通过让this指针加上offset_to_top的偏移量,就可以让 this 指针指向实际类型的起始地址

https://www.cnblogs.com/xhb19960928/p/11720314.html

https://www.zhihu.com/question/23971699

https://www.freebuf.com/articles/system/123821.html

https://blog.iret.xyz/article.aspx/cpp_vfunc_reversing_2

https://www.anquanke.com/post/id/85585

https://alschwalm.com/blog/static/2017/01/24/reversing-c-virtual-functions-part-2-2/

CATALOG
  1. 1. offset_to_top