异常处理是我们日常编程会不时用到但是却很少深入了解的部分,也是硬件、操作系统、编译器与用户程序需要密切配合才能完成的一个复杂过程。我之前学习Win32汇编时了解过Win32系统用户层的SEH(结构化异常处理),但对于Visual C++中的异常处理机制在实际编程中应该如何应用,以及它是如何利用Windows系统的SEH构建自己的异常处理机制等方面依然存在疑问。所以在这一系列文章中,我会对Visual C++中的异常处理机制的应用进行介绍、归纳和总结,并试图揭示其实现原理。
终止型异常处理
Visual C++提供的终止型异常处理机制想要实现如下功能:无论被__try保护的代码块是否能正常执行完(即不管其中是否发生异常、是否有执行流程向__try代码块外转移),__finally块中的代码都能被执行。
这在编程中是非常实用的:可以将可能发生异常或者产生错误的代码用__try块保护起来,并将解锁或者清理的代码放在__finally块中,增强程序的健壮性。下面给出两种经典应用场景:
在__finally块中执行解锁操作
try { // 1.执行加锁、申请资源等操作,获取对某资源的使用权 // 2.对该资源进行操作,操作过程可能发生异常 } __finally { // 执行解锁、释放资源等操作,释放对该资源的使用权 }
这种情况下,可以避免在执行P操作获取对该资源的使用权后,在操纵该资源时发生异常或错误,使得V操作不能被执行,最终导致其他需要访问该资源的线程全部在P操作上死等,而永远无法获取到该资源的使用权。
在__finally块中执行清理操作
try { bool bRet1 = false; bool bRet2 = false; bool bRet3 = false; // 1. 执行第1步操作 if (/* 第1步执行不成功 */) { __leave; } bRet1 = true; // 表明第1步执行成功 // 2. 执行第2步操作 if (/* 第2步执行不成功 */) { __leave; } bRet2 = true; // 表明第2步执行成功 // 3. 执行第3步操作 if (/* 第3步执行不成功 */) { __leave; } bRet3 = true; // 表明第3步执行成功 // 4. 执行后续操作 } __finally { if (bRet3) { // 清理、释放第3步中打开的资源 } if (bRet2) { // 清理、释放第2步中打开的资源 } if (bRet1) { // 清理、释放第1步中打开的资源 } }
个人认为这种情况可能是终止型异常处理最典型的应用了。Windows编程中有很多步骤都有如下特点:
- 后一步依赖前一步的执行成功
- 如果后一步执行失败,需要把前一步占用的资源释放掉
如果不适用终止型异常处理来完成这些步骤,那么这部分代码将变的冗长而繁琐,在每一步操作后的判断语句中,都需要在判定执行失败后清理之前所有步骤占用的资源。
正常情况下的执行流程
首先分析执行流程正常转移的情况,此时__try块中被保护的代码不会提前退出,所以无需进行局部展开,是效率最高、开销最小的情况。
DWORD funcTest01()
{
DWORD dwTemp = 3;
__try
{
dwTemp = 4;
}
__finally
{
dwTemp = 6;
if (AbnormalTermination())
{
cout << "__try块中执行时提前退出了" << endl;
}
else
{
cout << "执行流程自然转到了__finally块中" << endl;
}
}
cout << dwTemp << endl;
return 0;
}
从执行流程上分析,__try块中代码执行完后,执行流程转到__finally块中,因此对于__try块中的代码来说,属于正常终止的情况。
原理分析
反汇编代码
对应的反汇编如下:
5: DWORD funcTest01()
6: {
001523F0 55 push ebp
001523F1 8B EC mov ebp,esp
001523F3 6A FE push 0FFFFFFFEh
001523F5 68 E8 9E 15 00 push 159EE8h
001523FA 68 50 29 15 00 push offset _except_handler4 (0152950h)
001523FF 64 A1 00 00 00 00 mov eax,dword ptr fs:[00000000h]
00152