012、继承和多重继承
基类子对象
派生类对象通常内含一个或多个完整的基类子对象。访问控制只影响源码编译检查,不会把基类的私有成员从派生对象内存中删除。
单继承的概念布局:
1 | Derived object |
把 Derived* 转为主基类 Base* 时,地址可能不变;转到位于非零偏移的其他基类时,需要调整指针。
构造与析构顺序
1 | 构造:基类 -> 派生类成员 -> 派生类函数体 |
多重继承时,普通基类通常按声明顺序构造,逆序析构。虚基类由最派生类负责初始化,顺序还需结合 ABI 与源码声明。
成员访问与基类偏移
如果某成员函数把 this + constant 传给另一组函数,这个常量可能是基类子对象偏移:
1 | lea rcx, [rdi+20h] |
连续出现相同的 +20h 调整,并且目标函数都按自己的零偏移访问字段,是恢复第二基类的重要证据。
多重继承
多重继承对象可能具有:
- 多个基类数据区域。
- 多个虚表指针。
- 多组虚函数表。
- 调整
this后再进入真正实现的跳板函数。
概念布局:
1 | Derived |
通过 BaseB* 调用派生类覆盖函数时,编译器可能先把 this 从 BaseB 子对象地址调回完整对象地址,再跳转到实际函数。这类短函数常称为 adjustor thunk。
菱形继承与虚继承
菱形继承中,两条继承路径可能带来两个共同祖先副本。虚继承用于共享一个虚基类子对象,但会增加运行时定位复杂度。
MSVC 常见实现可能使用虚基表指针和虚基表来计算虚基类偏移;其他 ABI 的实现方式不同。分析时应关注“从对象读取表项,再把表项作为偏移加到对象地址”的模式。
不能把某个编译器版本的多重继承布局当成 C++ 标准。先确定架构、编译器和 ABI,再解释每个隐藏字段。
覆盖、隐藏与重载
机器码层面应区分:
- 覆盖:派生类替换虚表同一槽位的实现。
- 隐藏:名称查找规则导致基类同名函数在源码中不可见,未必涉及虚表。
- 重载:同一作用域中参数列表不同的多个函数,通常生成不同符号和函数体。
逆向恢复时最可靠的是槽位关系、调用原型和 this 调整,而不是源码术语猜测。
继承关系恢复流程
- 收集候选构造函数中的虚表写入。
- 按对象偏移把虚表分组。
- 对比各虚表槽位,寻找同槽位覆盖关系。
- 查找固定
this调整和跳板函数。 - 汇总基类构造、析构调用顺序。
- 根据成员偏移画出候选对象布局。
- 使用动态对象实例验证各子对象地址。
草图模板
1 | 完整对象地址: |
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能解释派生对象为什么包含基类子对象 | 待复盘 |
| 能根据固定偏移识别非首基类 | 待复盘 |
| 能说明多重继承为何可能有多个虚表指针 | 待复盘 |
| 能识别用于调整 this 的短跳板函数 | 待复盘 |
| 能说明虚继承解决的问题与增加的复杂度 | 待复盘 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ruiqy~!





















