013、异常处理
异常改变了什么
普通调用通过 CALL/RET 成对转移;异常可能从函数深处直接寻找匹配处理器,并在途中销毁已经构造的局部对象。
1 | throw |
逆向异常代码必须同时恢复“处理器选择”和“栈展开清理”两条逻辑。
C++ throw / catch 的线索
常见线索包括:
- 调用编译器运行库的抛出辅助函数。
- 异常对象构造、复制或移动。
- 与类型描述相关的数据。
- 正常路径之外的析构清理块。
- 函数元数据中记录的状态与处理器。
具体函数名和数据结构会随 MSVC、GCC/Clang、架构与运行库版本变化,不应只依赖单个符号名。
Windows SEH 与 C++ EH
Windows 的结构化异常处理负责传递访问冲突、除零、断点等系统异常;C++ 编译器通常在其上构建语言级异常语义。
需要区分:
- 系统异常:由 CPU 或操作系统报告。
- 语言异常:由 C++
throw产生并携带类型信息。 - 调试异常:断点、单步等也通过异常机制通知调试器。
x86 的常见形态
32 位 Windows 的传统 SEH 可通过线程环境中的异常注册链组织处理节点。编译器生成的函数可能:
- 在入口安装异常处理记录。
- 维护当前构造/清理状态。
- 在退出时恢复上一处理节点。
- 由运行库处理器解释编译器元数据。
现代编译器可能采用不同优化和安全机制,不能仅凭固定序言模板识别全部异常函数。
x64 表驱动展开
Windows x64 主要使用表驱动展开,不再依赖每个函数动态挂接传统栈链。PE 的异常目录可关联:
RUNTIME_FUNCTION:描述函数范围和展开信息位置。UNWIND_INFO:描述序言操作、保存寄存器和栈空间。UNWIND_CODE:记录如何逆转函数序言。- 语言处理器数据:支持 C++ catch 或 SEH 过滤等语义。
这些信息让系统在没有帧指针时仍能恢复调用现场,也是调试器回溯栈的重要依据。
展开信息描述的是如何恢复机器状态,不等于完整源码控制流。反编译器给出的 try/catch 边界仍需结合清理块和运行时行为验证。
析构与栈展开
假设函数已构造对象 A、B,构造 C 时抛出异常:
1 | C 未完成,不按完整对象析构 |
这会产生与正常返回路径不同的析构调用。看到重复资源清理函数时,要判断它们分别服务于正常退出还是异常展开。
first chance 与 second chance
调试器第一次收到异常时称 first chance,程序自己的处理器仍有机会处理;若无人处理,调试器再次收到 second chance,继续放行通常会终止进程。
调试恶意样本或带反调试逻辑的程序时,不要把所有 first-chance 异常都当成崩溃。
分析步骤
- 标出所有抛出点和可能触发系统异常的指令。
- 查找函数的异常与展开元数据。
- 识别清理基本块和析构调用。
- 对照对象构造状态,确认哪些资源已生效。
- 区分过滤器、处理器和 finally/清理语义。
- 动态观察异常代码、处理次数与恢复地址。
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能说明异常查找与栈展开的区别 | 待复盘 |
| 能解释展开过程中为什么需要调用析构函数 | 待复盘 |
| 能区分系统异常与 C++ 语言异常 | 待复盘 |
| 能说明 x64 RUNTIME_FUNCTION 与 UNWIND_INFO 的作用 | 待复盘 |
| 能区分 first-chance 与 second-chance 异常 | 待复盘 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ruiqy~!





















