C++异常处理机制反汇编(三):更深入地了解32位下异常处理机制
前面两篇文章只是大致介绍了32位环境下异常处理的基本结构和基本流程,省略了很多内容。这篇文章就把各种结构以及详细的异常处理流程串起来讲一遍,加深对异常处理机制底层原理的理解,为学习64位环境下异常处理机制做好准备。
**异常处理是编译器和操作系统合作完成的。**下面我们以一个示例来分析:
cpp
#include <stdio.h>
int main()
{
try
{
printf("try block\r\n");
throw 1;
}
catch (int e)
{
printf("catch int exception\r\n");
}
catch (float e)
{
printf("catch float exception\r\n");
}
return 0;
}
从ExceptionList说起
ExceptionList是NT_TIB(TEB的一部分)中的一个字段,它存储了当前线程的结构化异常处理链(SEH链)的头部指针。
那么,SEH链是怎么形成的呢?这其实是编译器的功劳。在编译阶段,编译器每进入一个包含try-catch块的函数时,就会将一个EXCEPTION_REGISTRATION结构体压入SEH链头部。
asm
push offset __except_handler3 ; handler
push fs:[0] ; 保存上一个SEH节点
mov fs:[0], esp ; 将新节点设为链头
对于上面的示例,压入EXCEPTION_REGISTRATION结构体的代码如下:


编译器生成的其它结构

发生异常时
对于示例中的throw语句,实际上是调用了:

可以看到,该函数有两个参数。实际上,这个函数原型如下:
cpp
extern "C" void __stdcall _CxxThrowException(
void* pExceptionObject,
_ThrowInfo* pThrowInfo
);
这个函数用于:
Builds the exception record and calls the runtime environment to start processing the exception.
它有两个参数:
- 异常对象的地址
- ThrowInfo结构体
下图是栈中这两个参数存储的内容:

我们在内存窗口中查看这两个地址实际存储的内容:

关于ThrowInfo结构,可以看第一篇文章:

关于这个函数的更多内容可以参考官方文档。
在_CxxThrowException函数内部,又调用了RasieException函数:

在调用RasieException之后,控制权转到Windows内核,也就是执行了KiDispathException函数。
异常捕获
当异常发生时,操作系统会介入:Windows异常分发器(KiDispathException)会捕获异常,遍历线程的SEH链,链中的节点(在具有异常处理功能的函数序言部分压入SEH链)都有一个handler字段,在遍历该链的时候会调用handler函数,handler函数会调用_CxxFrameHandler。
handler函数如下:

其中eax是FuncInfo的地址。在讲解_CxxFrameHandler之前,我们先来了解一下FuncInfo结构体。该结构体由编译器生成,编译器为每个包含异常处理的函数自动生成FuncInfo数据,它是连接C++异常处理源代码与Windows SEH的桥梁。它的作用可以看第一篇文章。_CxxFrameHandler中就使用了这个结构体。
下面我们来看看_CxxFrameHandler函数:
cpp
EXCEPTION_DISPOSITION __CxxFrameHandler(
EHExceptionRecord *pExcept,
EHRegistrationNode *pRN,
void *pContext,
DispatcherContext *pDC
);
这个函数的作用如下:
Internal CRT function. Used by the CRT to handle structured exception frames.
关于这个函数的更多内容,可以查看官方文档。
异常处理
在_CxxFrameHandler获取了异常的相关信息,并调用了InternalCxxFrameHandler函数对异常进行分类处理:

总结调用链
下面对上面的调用链进行总结:
txt
throw语句
↓
_CxxThrowException(异常对象, ThrowInfo)
↓
RaiseException(0xE06D7363, 4个参数)
↓
Windows内核异常分发器
↓
读取NT_TIB.ExceptionList(SEH链头)
↓
遍历SEH链,调用每个节点的handler
↓
↓→ 到达函数注册的__CxxFrameHandler
↓ ↓
↓ 使用FuncInfo定位try块
↓ ↓
↓ 使用TryBlockMapEntry查找catch范围
↓ ↓
↓ 使用msRttiDscr匹配catch类型
↓ ↓
↓ 使用ThrowInfo和CatchTableTypeArray进行RTTI匹配
↓ ↓
↓ 使用PMD调整this指针(如果需要)
↓ ↓
↓ 使用UnwindMapEntry进行栈展开
↓ ↓
↓ 设置EIP跳转到CatchProc
↓ ↓
↓← ExceptionContinueExecution
↓
恢复执行到catch块