【科普】详细讲讲 虚函数表 与 RTTI

Ambr0se 一级用户组 3天前 90

【科普】详细讲讲 虚函数表 与 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 指针进行解引用。如果 a1this 指针(即对象实例的地址),那么 [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数据指针)


CSGO插件分享-申明 1、本网站名称:CSGO插件分享-中文站  网址:https://bbs.csgocn.net
2、本站的宗旨在于为CSGO玩家提供一个插件分享的中文资源平台,多数插件来源于SourceMod论坛,并配以中文介绍和安装教程。
3、欢迎有能力的朋友共享有趣的CSGO插件资源。
4、本站资源大多为百度网盘,如发现链接失效,可以点: 这里进行反馈,我们会第一时间更新。
最新回复 (2)
返回