基础
SEH(Structured Exception Handling)结构化异常处理
SEH 实际包含两个主要功能: 结束处理 (termination handling) 和异常处理(exceptionhandling)
结束处理(__finally)
- __try
- {// todo
- }
- __finally
- {// todo
- }
__try 和__finally 关键字用来标出结束处理程序两段代码的轮廓. 在上面的代码段中, 操作系统和编译程序共同来确保结束处理程序中的__finally 代码块能够被执行, 不管保护体 (try 块) 是如何退出的. 不论你在保护体中使用 return, 还是 goto, 或者是 long jump, 结束处理程序 (__finally 块) 都将被调用
示例 1:(正常流程)
- DWORD SEHTest()
- {
- // 第一步
- DWORD dwTemp;
- HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
- __try
- {// 第二步
- WaitForSingleObject(hSem,INFINITE);
- dwTemp = 5;
- }
- __finally
- {// 第三步
- ReleaseSemaphore(hSem,1,NULL);
- }
- // 第四步
- return dwTemp;
- }
示例 2:(__try 代码块中 return, 返回值为 5)
- DWORD SEHTest()
- {
- // 第一步
- DWORD dwTemp;
- HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
- __try
- {// 第二步
- WaitForSingleObject(hSem,INFINITE);
- dwTemp = 5;
- // 在 return 之前会执行第三步
- return dwTemp;
- }
- __finally
- {// 第三步
- ReleaseSemaphore(hSem,1,NULL);
- }
- // 此步不会被执行到
- dwTemp = 9;
- return dwTemp;
- }
当编译程序检查源代码时, 它看到在 try 块中有 return 语句, 这样, 编译程序就生成代码将返回值 (本例中是 5) 保存在一个编译程序建立的临时变量中. 编译程序然后再生成代码来执行 finally 块中包含的指令, 这称为局部展开. 更特殊的情况是, 由于 try 块中存在过早退出的代码, 从而产生局部展开, 导致系统执行 finally 块中的内容. 在 finally 块中的指令执行之后, 编译程序临时变量的值被取出并从函数中返回.
可以用 IDA 看到:
call ds:WaitForSingleObject(x,x)
mov [ebp+dwTemp], 5 ; 5 保存到临时变量
mov ecx, [ebp+dwTemp]
示例 3:(__try 代码块中 goto, 同样会执行到__finaly)
- DWORD SEHTest()
- {
- // 第一步
- DWORD dwTemp;
- HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
- __try
- {// 第二步
- WaitForSingleObject(hSem,INFINITE);
- dwTemp = 5;
- goto Go;
- }
- __finally
- {// 第三步
- ReleaseSemaphore(hSem,1,NULL);
- }
- Go:
- // 第四步
- return dwTemp;
- }
当编译程序看到 try 块中的 goto 语句, 它首先生成一个局部展开来执行 finally 块中的内容. 这一次, 在 finally 块中的代码执行之后, 在 Go 标号之后的代码将执行, 因为在 try 块和 finally 块中都没有返回发生. 这里的代码使函数返回 5. 而且, 由于中断了从 try 块到 finally 块的自然流程, 可能要蒙受很大的性能损失(取决于运行程序的 CPU).
示例 4:(异常退出)
- DWORD Sub_SEHTest()
- {
- // 错误产生
- int i=0;
- return 10/i;
- }
- DWORD SEHTest()
- {
- // 第一步
- DWORD dwTemp;
- HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
- __try
- {// 第二步
- WaitForSingleObject(hSem,INFINITE);
- dwTemp = Sub_SEHTest();// 此处会产生错误, 但仍会调到第三步
- }
- __finally
- {// 第三步
- ReleaseSemaphore(hSem,1,NULL);
- }
- return dwTemp;
- }
再假想一下, try 块中的 Funcinator 函数调用包含一个错误, 会引起一个无效内存访问. 如果没有 SEH, 在这种情况下, 将会给用户显示一个很常见的 ApplicationError 对话框. 当用户忽略这个错误对话框, 该进程就结束了. 当这个进程结束(由于一个无效内存访问), 信标仍将被占用并且永远不会被释放, 这时候, 任何等待信标的其他进程中的线程将不会被分配 CPU 时间. 但若将对 ReleaseSemaphore 的调用放在 finally 块中, 就可以保证信标获得释放, 即使某些其他函数会引起内存访问错误.
示例 5:(跳转流程分析)
- DWORD SEHTest()
- {
- DWORD dwTemp = 0;
- while (dwTemp<10)
- {//0
- __try
- {
- if (2 == dwTemp)
- {
- continue;//1
- }
- if (3 == dwTemp)
- {
- break;//2
- }
- }
- __finally
- {
- dwTemp++;//3
- }
- dwTemp++;//4
- }
- dwTemp += 10;//5
- return dwTemp;
- }
1.dwTemp = 0; 执行正常的__finally 中的 dwTemp++, 以及随后的 dwTemp++, 此时 dwTemp=2(流程: 0->3->4->0)
2.continue 被执行, 因为要跳出__try 代码块, 所以执行__finally 的 dwTemp++,dwTemp=3(接着流程: 1->3->0)
3.dwTemp=3 时, 因为要跳出__try 代码块, 所以执行__finally 的 dwTemp++,dwTemp=4,dwTemp+10=14(接着流程: 2->3->5)
示例 6:(__try 代码块的 return 值可以被__finally 块的覆盖, 下例返回 103)
- DWORD SEHTest()
- {
- // 第一步
- DWORD dwTemp;
- HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
- __try
- {// 第二步
- WaitForSingleObject(hSem,INFINITE);
- dwTemp = 5;
- // 在 return 之前会执行第三步
- return dwTemp;
- }
- __finally
- {// 第三步
- ReleaseSemaphore(hSem,1,NULL);
- return 103;//103 会把 5 覆盖
- }
- // 此步不会被执行到
- dwTemp = 9;
- return dwTemp;
- }
示例 7:(__leave 可以减少局部展开的开销)
- DWORD SEHTest(int dwTemp)
- {
- bool bRet = false;
- __try
- {
- if (0 == dwTemp)
- {
- dwTemp++;
- __leave;
- }
- else if (1 == dwTemp)
- {
- dwTemp --;
- __leave;
- }
- bRet = true;// 用于区分是否为正常跳转到 finally
- }
- __finally
- {
- if (bRet)
- {
- dwTemp+=10;
- }
- }
- return dwTemp;
- }
在 try 块中使用__leave 关键字会引起跳转到 try 块的结尾. 可以认为是跳转到 try 块的右大括号. 由于控制流自然地从 try 块中退出并进入 finally 块, 所以不产生系统开销. 当然, 需要引入一个新的 Boolean 型变量 bRet, 用来指示函数是成功或失败. 这是比较小的代价.
关于 finally 块的说明
1. 从 try 块进入 finally 块的正常控制流.
2. 局部展开: 从 try 块的过早退出 (goto,longjump,continue,break,return 等) 强制控制转移到 finally 块.
3. 全局展开(globalunwind), 在发生的时候没有明显的标识, 我们在示例 4 已经见到. 在 SEHTest 的 try 块中, 有一个对 Sub_SEHTest 函数的调用. 如果 Sub_SEHTest 函数引起一个内存访问违规(memoryaccessviolation), 一个全局展开会使 SEHTest 的 finally 块执行.
为了确定是哪一种情况引起 finally 块执行, 可以调用内部函数
BOOL AbnormalTermination();
这个内部函数只在 finally 块中调用, 返回一个 Boolean 值. 指出与 finally 块相结合的 try 块是否过早退出. 换句话说, 如果控制流离开 try 块并自然进入 finally 块, AbnormalTermination 将返回 FALSE. 如果控制流非正常退出 try 块 - 通常由于 goto,return,break 或 continue 语句引起的局部展开, 或由于内存访问违规或其他异常引起的全局展开 - 对 AbnormalTermination 的调用将返回 TRUE. 没有办法区别 finally 块的执行是由于全局展开还是由于局部展开. 但这通常不会成为问题, 因为可以避免编写执行局部展开的代码.
异常处理(__except)
- __try
- {}
- __except(1)
- {}
示例 1:(不会执行的__except 代码块)
- DWORD SEHTest()
- {
- DWORD dwTemp;
- __try
- {
- dwTemp = 0;
- }
- __except(EXCEPTION_EXECUTE_HANDLER)
- {
- // 此处不会执行
- }
- return dwTemp;
- }
try 块中, 只是把一个 0 赋给 dwTemp 变量. 这个操作决不会造成异常的引发, 所以 except 块中的代码永远不会执行
示例 2:(正常引发__except 异常处理)
- DWORD SEHTest()
- {
- DWORD dwTemp = 0;
- __try
- {
- dwTemp = 5/dwTemp;// 除以 0!
- dwTemp += 10;
- }
- __except(EXCEPTION_EXECUTE_HANDLER)
- {
- MessageBeep(0);
- }
- dwTemp+= 20;
- return dwTemp;
- }
try 块中有一个指令试图以 0 来除 5.CPU 将捕捉这个事件, 并引发一个硬件异常. 当引发了这个异常时, 系统将定位到 except 块的开头, 并计算异常过滤器表达式的值, 过滤器表达式的结果值只能是下面三个标识符之一, 这些标识符定义在 Windows 的 Excpt.h 文件中
标识符 | 定义为 |
---|---|
E X C E P T I O N _ E X E C U T E _ H A N D L E R | 1 |
E X C E P T I O N _ C O N T I N U E _ S E A R C H | 0 |
E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N | -1 |
流程图如下:
- (注意: 最里层 try 块是指包含了这个异常代码的最里层的 try 块, 不包含的不算)
- DWORD dwTemp = 0;
- __try
- {
- dwTemp = 5/dwTemp;// 异常
- __try
- {
- }
- __except(1)// 这个未包含异常, 所以执行的是外面那个 except
- {
- int j= 0;
- }
- }
- __except(1)// 这个被执行
- {
- int k = 0;
- }
- EXCEPTION_EXECUTE_HANDLER
这个值的意思是要告诉系统:"我认出了这个异常. 即, 我感觉这个异常可能在某个时候发生, 我已编写了代码来处理这个问题, 现在我想执行这个代码." 在这个时候, 系统执行一个全局展开 (本章后面将讨论), 然后执行向 except 块中代码(异常处理程序代码) 的跳转. 在 except 块中代码执行完之后, 系统考虑这个要被处理的异常并允许应用程序继续执行. 这种机制使 Windows 应用程序可以抓住错误并处理错误, 再使程序继续运行, 不需要用户知道错误的发生.
但是, 当 except 块执行后, 代码将从何处恢复执行? 稍加思索, 我们就可以想到几种可能性
1. 从产生异常的 CPU 指令之后恢复执行, 即执行示例 2 中的 dwTemp+=10
2. 是从产生异常的指令恢复执行, 如果在 except 块中有这样的语句会怎么样呢, 对应
dwTemp = 2;
可以从产生异常的指令恢复执行. 这一次, 将用 2 来除 5, 执行将继续, 不会产生其他的异常, 对应 EXCEPTION_CONTINUE_EXECUTION
3. 从 except 块之后的第一条指令开始恢复执行, 即执行 dwTemp+=20; 对应 EXCEPTION_EXECUTE_HANDLER
当一个异常过滤器的值为 EXCEPTION_EXECUTE_HANDLER 时, 系统必须执行一个全局展开(globalunwind). 这个全局展开使某些 try-finally 块恢复执行, 某些 try-finally 块指在处理异常的 try_except 块之后开始执行但未完成的块
示例 3:
- void Sub_SEHTest()
- {
- DWORD dwTemp = 0;
- HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
- __try
- {//2
- WaitForSingleObject(hSem,INFINITE);
- dwTemp = 5/dwTemp;// 异常!!!
- }
- __finally
- {//3
- ReleaseSemaphore(hSem,1,NULL);
- }
- }
- void SEHTest()
- {
- __try
- {//1
- Sub_SEHTest();
- }
- __except(EXCEPTION_EXECUTE_HANDLER)
- {//4
- // TODO.
- int j=0;
- }
- }
1.SEHTest 开始执行, 进入它的__try 块并调用 Sub_SEHTest
2.Sub_SEHTest 开始执行, 等到信标, 然后除 0 产生异常
3. 系统因此取得控制, 开始搜索一个与 except 块相配的 try 块, 因为 Sub_SEHTest 的__try 对应的是__finally, 所以它向上查找
4. 系统在 SEHTest 中找到相配的_except
5. 系统现在计算与 SEHTest 中 except 块相联的异常过滤器的值, 并等待返回值. 当系统看到返回值是 EXCEPTION_EXECUTE_HANDLER 的, 系统就在 Sub_SEHTest 的 finally 块中开始一个全局展开
6. 对于一个全局展开, 系统回到所有未完成的 try 块的结尾, 查找与 finally 块相配的 try 块. 在这里, 系统发现的 finally 块是 Sub_SEHTest 中所包含的 finally 块. 从而执行 finally 块
7. 在 finally 块中包含的代码执行完之后, 系统继续上溯, 查找需要执行的未完成 finally 块. 在这个例子中已经没有这样的 finally 块了. 系统到达要处理异常的 try-except 块就停止上溯. 这时, 全局展开结束, 系统可以执行 except 块中所包含的代码.
为了更好地理解这个执行次序, 我们再从不同的角度来看发生的事情. 当一个过滤器返回 EXCEPTION_EXECUTE_HANDLER 时, 过滤器是在告诉系统, 线程的指令指针应该指向 except 块中的代码. 但这个指令指针在 Sub_SEHTest 的 try 块里. 回忆一下前面提到的, 每当一个线程要从一个 try-finally 块离开时, 必须保证执行 finally 块中的代码. 在发生异常时, 全局展开就是保证这条规则的机制.
流程如下:
暂停全局展开
通过在 finally 块里放入一个 return 语句, 可以阻止系统去完成一个全局展开
示例 4:
- void Sub_SEHTest()
- {
- DWORD dwTemp = 0;
- HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
- __try
- {
- WaitForSingleObject(hSem,INFINITE);
- dwTemp = 5/dwTemp;// 异常!!!
- }
- __finally
- {
- ReleaseSemaphore(hSem,1,NULL);
- return;/// 直接返回!!
- }
- }
- void SEHTest()
- {
- __try
- {
- Sub_SEHTest();
- }
- __except(EXCEPTION_EXECUTE_HANDLER)
- {
- // TODO.
- int j=0;// 这里永远也调不到了
- }
- }
当全局展开时, 先执行 Sub_SEHTest 的 finally 中的代码, 它的 return 使系统完全停止了展开, 从而无法执行到 except 块
EXCEPTION_CONTINUE_EXECUTION
前面为简单起见, 在过滤器里直接硬编码了标识符 EXCEPTION_EXECUTE_HANDLER, 但实际上可以让过滤器调用一个函数确定应该返回哪一个标识符
示例 5:
- char g_szBuf[100];
- LONG SEHFilter(char** ppBuf)
- {
- if (*ppBuf == NULL)
- {
- *ppBuf = g_szBuf;
- return (EXCEPTION_CONTINUE_EXECUTION);
- }
- return EXCEPTION_EXECUTE_HANDLER;
- }
- void SEHTest()
- {
- int x = 0;
- char* pBuf = NULL;
- __try
- {
- *pBuf = 'j';
- x = 5/x;
- }
- __except(SEHFilter(&pBuf))
- {
- // todo
- int j = 0;
- }
- }
理论上应该是这样的:
1.*pBuf='j'引发异常, 从而跳转到 SEHFilter 中, 从而让 pBuf 指向 g_szBuf, 并返回 EXCEPTION_CONTINUE_EXECUTION
2. 继续试着执行 * pBuf='j'成功, 即 g_szBuf[0]='j'
3. 继续向下执行 x = 5/x; 除数为 0, 所以异常, 从而跳转到 SEHFilter 中, 这次返回 EXCEPTION_EXECUTE_HANDLER
4. 所以执行到 except 块
但实际上 g_szBuf[0]='j'不一定能成立, 因为 EXCEPTION_CONTINUE_EXECUTION 是让 thread 回到发生 exception 的机器指令, 不是回到发生 exception 的 C/C++ 语句
如果 * pBuf = 'j'的机器指令如下:
- 00EC367D mov eax,dword ptr [ebp-2Ch]
- 00EC3680 mov byte ptr [eax],6Ah
那么, 第二条指令产生异常. 异常过滤器可以捕获这个异常, 修改 pBuf 的值, 并告诉系统重新执行第二条 CPU 指令. 但问题是, 寄存器的值可能不改变, 不能反映装入到 pBuf 的新值,
如果编译程序优化了代码, 继续执行可能顺利; 如果编译程序没有优化代码, 继续执行就可能失败.
GetExceptionCode(异常处理)
一个异常过滤器在确定要返回什么值之前, 必须分析具体情况. 例如, 异常处理程序可能知道发生了除以 0 引起的异常时该怎么做, 但是不知道该如何处理一个内存存取异常. 异常过滤器负责检查实际情况并返回适当的值.
- __try
- {
- x = 0;
- y = 4 / x;
- }
- __except((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?
- EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
- {
- // Handle divide by zero exception.
- }
内部函数 GetExceptionCode 返回一个值, 该值指出所发生异常的种类:
内部函数 GetExceptionCode 只能在一个过滤器中调用(--except 之后的括号里), 或在一个异常处理程序中被调用
- ___try
- {
- y = 0;
- x = 4 / y;
- }
- __except(
- ((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ||
- (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ?
- EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
- {
- switch(GetExceptionCode())
- {
- case EXCEPTION_ACCESS_VIOLATION:
- //Handle the access violation.
- break;
- case EXCEPTION_INT_DIVIDE_BY_ZERO:
- //Handle the integer divide by?.
- break;
- }
- }
但是, 不能在一个异常过滤器函数里面调用 GetExceptionCode. 编译程序会捕捉这样的错误. 当编译下面的代码时, 将产生编译错误
- __try
- {
- y = 0;
- x = 4 / y;
- }
- __except(CoffeeFilter())
- {
- // Handle the exception.
- }
- LONG CoffeeFilter(void)
- {
- //Compilation error: illegal call to GetExceptionCode.
- return((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ?
- EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
- }
可以按下面的形式改写代码:
- __try
- {
- y = 0;
- x = 4 / y;
- }
- __except(CoffeeFilter(GetExceptionCode()))
- {
- //Handle the exception.
- }
- LONG CoffeeFilter(DWORD dwExceptionCode)
- {
- return((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ?
- EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
- }
异常处理错误码
1. 与内存有关的异常
EXCEPTION_ACCESS_VIOLATION: 线程试图对一个虚地址进行读或写, 但没有做适当的存取. 这是最常见的异常.
EXCEPTION_DATATYPE_MISALIGNMENT: 线程试图读或写不支持对齐 (alignment) 的硬件上的未对齐的数据. 例如, 16 位数值必须对齐在 2 字节边界上, 32 位数值要对齐在 4 字节边界上.
EXCEPTION_ARRAY_BOUNDS_EXCEEDED: 线程试图存取一个越界的数组元素, 相应的硬件支持边界检查.
EXCEPTION_IN_PAGE_ERROR: 由于文件系统或一个设备启动程序返回一个读错误, 造成不能满足要求的页故障.
EXCEPTION_GUARD_PAGE: 一个线程试图存取一个带有 PAGE_GUARD 保护属性的内存页. 该页是可存取的, 并引起一个 EXCEPTION_GUARD_PAGE 异常.
EXCEPTION_STACK_OVERFLOW: 线程用完了分配给它的所有栈空间.
EXCEPTION_ILLEGAL_INSTRUCTION: 线程执行了一个无效的指令. 这个异常由特定的 CPU 结构来定义; 在不同的 CPU 上, 执行一个无效指令可引起一个陷井错误.
EXCEPTION_PRIV_INSTRUCTION: 线程执行一个指令, 其操作在当前机器模式中不允许.
2. 与异常相关的异常
EXCEPTION_INVALID_DISPOSITION: 一个异常过滤器返回一值, 这个值不是 EXCEPTION_EXECUTE_HANDLER,EXCEPTION_CONTINUE_SEARCH,EXCEPTION_CONTINUE_EXECUTION 三者之一.
EXCEPTION_NONCONTINUABLE_EXCEPTION: 一个异常过滤器对一个不能继续的异常返回 EXCEPTION_CONTINUE_EXECUTION.
3. 与调试有关的异常
EXCEPTION_BREAKPOINT: 遇到一个断点.
EXCEPTION_SINGLE_STEP: 一个跟踪陷井或其他单步指令机制告知一个指令已执行完毕.
EXCEPTION_INVALID_HANDLE: 向一个函数传递了一个无效句柄.
4. 与整数有关的异常
EXCEPTION_INT_DIVIDE_BY_ZERO: 线程试图用整数 0 来除一个整数.
EXCEPTION_INT_OVERFLOW: 一个整数操作的结果超过了整数值规定的范围.
5. 与浮点数有关的异常
EXCEPTION_FLT_DENORMAL_OPERAND: 浮点操作中的一个操作数不正常. 不正常的值是一个太小的值, 不能表示标准的浮点值.
EXCEPTION_FLT_DIVIDE_BY_ZERO: 线程试图用浮点数 0 来除一个浮点.
EXCEPTION_FLT_INEXACT_RESULT: 浮点操作的结果不能精确表示成十进制小数.
EXCEPTION_FLT_INVALID_OPERATION: 表示任何没有在此列出的其他浮点数异常.
EXCEPTION_FLT_OVERFLOW: 浮点操作的结果超过了允许的值.
EXCEPTION_FLT_STACK_CHECK: 由于浮点操作造成栈溢出或下溢.
EXCEPTION_FLT_UNDERFLOW: 浮点操作的结果小于允许的值.
来源: https://www.qcloud.com/developer/article/1598558