011、虚函数
动态绑定
虚函数让“通过基类指针或引用调用接口”在运行时选择实际派生类实现。常见 ABI 使用:
- 对象中的 虚表指针(vfptr/vptr)。
- 只读数据区中的 虚函数表(vftable/vtable)。
- 表内按槽位排列的函数地址。
典型虚调用可抽象为:
1 | mov rax, qword ptr [rcx] ; 读取对象的虚表指针 |
RCX 在 Windows x64 中通常是 this,但仍应结合调用约定验证。
虚表与对象
通常同一动态类型的对象共享一张虚函数表,每个对象只保存指向该表的指针。虚表中的函数代码不复制到对象里。
1 | object |
多重继承时,一个完整对象可能包含多个基类子对象和多个虚表指针,不能假设永远只有对象首地址处一个 vfptr。
构造与析构中的虚表写入
构造过程中,编译器会让对象在不同阶段表现为当前正在构造的类型;析构时反向变化。因此可能观察到同一对象地址先后写入不同虚表地址。
这提供了两类线索:
- 写虚表地址的函数可能是构造或析构相关函数。
- 虚表写入顺序有助于恢复继承层次。
虚表布局不是 C++ 标准规定的固定二进制格式。槽位、析构函数变体、RTTI 位置与继承处理都依赖 ABI。
静态调用与虚调用
即使函数声明为 virtual,编译器也可能生成直接调用:
- 通过明确对象类型调用,目标已知。
- 在构造或析构阶段调用。
- 优化器证明动态类型唯一并完成去虚拟化。
- 函数被内联。
因此不能用“没有间接调用”证明源码中没有虚函数。
纯虚函数与抽象类
纯虚函数表示基类只定义接口,不提供可直接使用的完整实现。虚表槽位可能指向运行库的纯虚调用处理函数,也可能因 ABI 和链接结果呈现不同形式。
包含纯虚函数的类通常不能直接实例化,但其构造、析构、RTTI 和基类子对象仍会出现在程序中。
虚析构函数
通过基类指针删除派生对象时,基类需要虚析构函数,才能先执行派生类清理,再执行基类清理。反汇编中可能看到多个析构相关入口:
- 只完成对象析构。
- 析构后再释放内存。
- 处理对象数组的包装逻辑。
命名时不要把所有变体都粗略标成同一个 destructor,应记录它是否调用 operator delete。
RTTI 辅助信息
启用 RTTI 时,虚表附近可能关联类型描述、类层次和基类信息。IDA、Ghidra 等工具可以利用这些结构辅助恢复类名和继承关系。
但程序可能关闭 RTTI、剥离符号或采用不同 ABI,因此 RTTI 是证据,不是必要条件。
虚调用恢复步骤
1 | 1. 确认间接 CALL 的目标来自内存表 |
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能画出对象、vfptr、vtable 与虚函数的关系 | 待复盘 |
| 能识别典型虚调用的两次间接访问 | 待复盘 |
| 能说明构造析构阶段为什么可能改写 vfptr | 待复盘 |
| 能解释虚函数为何也可能被直接调用 | 待复盘 |
| 能区分对象析构与“析构后释放”包装函数 | 待复盘 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ruiqy~!





















