🎨 颜色标记说明

  • 粉色:特别重要、主线必掌握、优先复盘的内容。
  • 蓝色:核心概念、术语、分类名。
  • 橙色:需要关注的边界、易错点、注意事项。
  • 绿色:解释类内容、帮助理解知识点。

返回:全书总览 · 上一章:构造与析构 · 下一章:继承

动态绑定

虚函数让“通过基类指针或引用调用接口”在运行时选择实际派生类实现。常见 ABI 使用:

  • 对象中的 虚表指针(vfptr/vptr)
  • 只读数据区中的 虚函数表(vftable/vtable)
  • 表内按槽位排列的函数地址。

典型虚调用可抽象为:

1
2
mov rax, qword ptr [rcx]       ; 读取对象的虚表指针
call qword ptr [rax+slot] ; 调用某个虚表槽位

RCX 在 Windows x64 中通常是 this,但仍应结合调用约定验证。

虚表与对象

通常同一动态类型的对象共享一张虚函数表,每个对象只保存指向该表的指针。虚表中的函数代码不复制到对象里。

1
2
3
4
5
object
+0x00 vfptr ------> vtable
+0x08 field_a [0] virtual_func_A
+0x0C field_b [1] virtual_func_B
[2] virtual_destructor

多重继承时,一个完整对象可能包含多个基类子对象和多个虚表指针,不能假设永远只有对象首地址处一个 vfptr

构造与析构中的虚表写入

构造过程中,编译器会让对象在不同阶段表现为当前正在构造的类型;析构时反向变化。因此可能观察到同一对象地址先后写入不同虚表地址。

这提供了两类线索:

  • 写虚表地址的函数可能是构造或析构相关函数。
  • 虚表写入顺序有助于恢复继承层次。

虚表布局不是 C++ 标准规定的固定二进制格式。槽位、析构函数变体、RTTI 位置与继承处理都依赖 ABI。

静态调用与虚调用

即使函数声明为 virtual,编译器也可能生成直接调用:

  • 通过明确对象类型调用,目标已知。
  • 在构造或析构阶段调用。
  • 优化器证明动态类型唯一并完成去虚拟化。
  • 函数被内联。

因此不能用“没有间接调用”证明源码中没有虚函数。

纯虚函数与抽象类

纯虚函数表示基类只定义接口,不提供可直接使用的完整实现。虚表槽位可能指向运行库的纯虚调用处理函数,也可能因 ABI 和链接结果呈现不同形式。

包含纯虚函数的类通常不能直接实例化,但其构造、析构、RTTI 和基类子对象仍会出现在程序中。

虚析构函数

通过基类指针删除派生对象时,基类需要虚析构函数,才能先执行派生类清理,再执行基类清理。反汇编中可能看到多个析构相关入口:

  • 只完成对象析构。
  • 析构后再释放内存。
  • 处理对象数组的包装逻辑。

命名时不要把所有变体都粗略标成同一个 destructor,应记录它是否调用 operator delete

RTTI 辅助信息

启用 RTTI 时,虚表附近可能关联类型描述、类层次和基类信息。IDA、Ghidra 等工具可以利用这些结构辅助恢复类名和继承关系。

但程序可能关闭 RTTI、剥离符号或采用不同 ABI,因此 RTTI 是证据,不是必要条件。

虚调用恢复步骤

1
2
3
4
5
6
7
1. 确认间接 CALL 的目标来自内存表
2. 回溯表基址是否从对象字段读取
3. 找到写入该字段的构造相关函数
4. 枚举表内函数地址并建立槽位编号
5. 对比不同虚表的同槽位实现
6. 追踪 this 是否发生调整
7. 用动态断点验证实际目标

学习检查清单

检查项 状态
能画出对象、vfptr、vtable 与虚函数的关系 待复盘
能识别典型虚调用的两次间接访问 待复盘
能说明构造析构阶段为什么可能改写 vfptr 待复盘
能解释虚函数为何也可能被直接调用 待复盘
能区分对象析构与“析构后释放”包装函数 待复盘