【科普】详细讲讲 虚函数表 与 RTTI
我曾在讲解VMT Hook(https://bbs.csgocn.net/thread-946.htm)的帖子中讲解过虚表
但是那篇帖子的图片被图床删了,因此我重新写了这篇帖子
如果在阅读过程发现文章错误之处,或有疑问,欢迎留言
多态
#include <iostream>
// 万能遥控器接口
class IRemoteControl {
public:
// “开关”对于不同设备有不同实现
virtual void power() {
std::cout << "POWWWWERRRRR\n";
}
virtual ~IRemoteControl() {}
};
// 电视
class TV : public IRemoteControl {
public:
void power() override { // `override` 确保我们正确地重写了基类函数(实现了设备对应的功能)
std::cout << "电视\n";
}
};
// 空调
class AirConditioner : public IRemoteControl {
public:
void power() override {
std::cout << "空调\n";
}
};
void pressPowerButton(IRemoteControl* device) {
std::cout << "电源按钮按下...\n";
device->power(); // 多态发生在这里,运行时决定调用哪个(设备的)power函数
}
int main() {
TV myTV;
AirConditioner myAC;
pressPowerButton(&myTV); // 传递的是TV
pressPowerButton(&myAC); // 传递的是AC
return 0;
}
静态绑定(编译时):
上文代码段解释:如果 `power()` 不是虚函数,`device->power()` 将永远调用 `IRemoteControl::power()`,因为编译器只知道 `device` 是个 `IRemoteControl*` 类型的指针
类似情形的反汇编表现:
(a1为类指针)
动态绑定(运行时):
上文代码段解释:因为 `power()` 是虚函数,程序会在运行时检查 `device` 指针实际指向的对象类型(是 `TV` 还是 `AirConditioner`),然后调用该类型的 `power()` 方法
类似情形的反汇编表现:
a1为类实例的指针。在反汇编中,[a1]
表示对 a1
指针进行解引用。如果 a1
是 this
指针(即对象实例的地址),那么 [a1]
操作读取的就是对象内存布局最开始的那个数据,也就是 vptr
(虚表指针)
[对象: myTV (实例化TV)]
+---------------------------+
| vptr (指向TV的虚表) +-----------------> [TV的虚表]
+---------------------------+
| (其他TV成员...) |
+---------------------------+
然后1432LL就是我们目标虚函数位于虚表中的偏移(由于是x64平台的,所以我们/8,也就是179),所以这个虚函数为a1所对应虚表中的[179](*vptr[179])
虚表

我们可以看出来虚表中的虚函数储存于一个连续的内存中,因此我们将虚表视作指针数组(void**)(你大多数时候看到的虚表指针解指针可能为:*(void***)(...),因为这是针对指向指针数组(void**)的指针(void***)的解指针)
可以这样分层理解:我们拿到的对象实例指针(this
)可以看作 Class*
类型,它指向对象实例。对象实例的第一个成员是虚表指针(vptr
),它的类型是 void**
。所以,this
指针实际上指向一个void**
类型的地址。当我们想通过this
指针去操作虚表时,就需要一个能指向“指向虚表的指针”的指针,也就是 void***
因此我们只需要取到虚表的VA(xxx.dll + RVA),然后将VA + offset就可以得出虚函数指针(void*)
注意:
ida 8+ pro反编译windows的二进制文件时image base默认是0x180000000

C++ 提供了 RTTI (Run-Time Type Information) 机制,允许我们在运行时查询对象的类型,最常见的应用就是 `dynamic_cast` 和 `typeid`
`dynamic_cast<TV*>(device)` 能成功,就是RTTI在起作用
为了高效实现,编译器再次利用了现有的虚表机制,将 RTTI 信息放置到虚表结构中。我们可以把 RTTI 信息看作是虚表的户口本,记录了类的继承关系、类名等关键信息
这种存储方式,直接导致了不同编译器的虚表布局出现了分歧
不同ABI(MSVC 与 Itanium C++)的虚表RTTI结构差异
准确来说,ABI分为C ABI和C++ ABI
gcc/llvm编译windows平台的二进制文件时,他的C ABI会强制遵守MSVC ABI规定,但其C++ ABI依旧使用Itanium C++标准(所以虚表RTTI结构差距依旧存在)
勘误:截图中的VA是错误的,都应是RVA
遵守MSVC ABI的二进制文件

RTTI数据指针位于vtable - (void*)长度的内存地址
遵守Itanium C++ ABI的二进制文件

RTTI指针位于vtable + (void*)长度的内存地址
(因此gcc/clang编译的二进制文件,虚函数一般从虚表中的[2](*vptr[2])开始,[0]是offset_to_top(通常为0),[1]是RTTI数据指针)