Visual C++异常处理机制原理与应用(一)—— C/C++结构化异常处理之try-finally终止处理的使用与原理(上)

时间:2022-12-15 13:40:07

异常处理是我们日常编程会不时用到但是却很少深入了解的部分,也是硬件、操作系统、编译器与用户程序需要密切配合才能完成的一个复杂过程。我之前学习Win32汇编时了解过Win32系统用户层的SEH(结构化异常处理),但对于Visual C++中的异常处理机制在实际编程中应该如何应用,以及它是如何利用Windows系统的SEH构建自己的异常处理机制等方面依然存在疑问。所以在这一系列文章中,我会对Visual C++中的异常处理机制的应用进行介绍、归纳和总结,并试图揭示其实现原理。

终止型异常处理

Visual C++提供的终止型异常处理机制想要实现如下功能:无论被__try保护的代码块是否能正常执行完(即不管其中是否发生异常、是否有执行流程向__try代码块外转移),__finally块中的代码都能被执行。

这在编程中是非常实用的:可以将可能发生异常或者产生错误的代码用__try块保护起来,并将解锁或者清理的代码放在__finally块中,增强程序的健壮性。下面给出两种经典应用场景:

  1. 在__finally块中执行解锁操作

    try
    {
    // 1.执行加锁、申请资源等操作,获取对某资源的使用权
    // 2.对该资源进行操作,操作过程可能发生异常
    }
    __finally
    {
    // 执行解锁、释放资源等操作,释放对该资源的使用权
    }

    这种情况下,可以避免在执行P操作获取对该资源的使用权后,在操纵该资源时发生异常或错误,使得V操作不能被执行,最终导致其他需要访问该资源的线程全部在P操作上死等,而永远无法获取到该资源的使用权。

  2. 在__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]
00152405 50 push eax
00152406 81 C4 20 FF FF FF add esp,0FFFFFF20h
0015240C 53 push ebx
0015240D 56 push esi
0015240E 57 push edi
0015240F 8D BD 10 FF FF FF lea edi,[ebp-0F0h]
00152415 B9 36 00 00 00 mov ecx,36h
0015241A B8 CC CC CC CC mov eax,0CCCCCCCCh
0015241F F3 AB rep stos dword ptr es:[edi]
00152421 A1 00 B0 15 00 mov eax,dword ptr [__security_cookie (015B000h)]
00152426 31 45 F8 xor dword ptr [ebp-8],eax
00152429 33 C5 xor eax,ebp
0015242B 50 push eax
0015242C 8D 45 F0 lea eax,[ebp-10h]
0015242F 64 A3 00 00 00 00 mov dword ptr fs:[00000000h],eax
7: DWORD dwTemp = 3;
00152435 C7 45 E0 03 00 00 00 mov dword ptr [dwTemp],3
8: __try
0015243C C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
00152443 C7 85 14 FF FF FF 01 00 00 00 mov dword ptr [ebp-0ECh],1
9: {
10: dwTemp = 4;
0015244D C7 45 E0 04 00 00 00 mov dword ptr [dwTemp],4
11: }
00152454 C7 45 FC FE FF FF FF mov dword ptr [ebp-4],0FFFFFFFEh
0015245B C7 85 14 FF FF FF 00 00 00 00 mov dword ptr [ebp-0ECh],0
00152465 E8 02 00 00 00 call funcTest01+7Ch (015246Ch)
0015246A EB 65 jmp $LN10 (01524D1h)
12: __finally
13: {
14: dwTemp = 6;
0015246C C7 45 E0 06 00 00 00 mov dword ptr [dwTemp],6
15: if (AbnormalTermination())
00152473 83 BD 14 FF FF FF 00 cmp dword ptr [ebp-0ECh],0
0015247A 74 2B je funcTest01+0B7h (01524A7h)
16: {
17: cout << "__try块中执行时提前退出了" << endl;
0015247C 8B F4 mov esi,esp
0015247E 68 96 10 15 00 push offset std::endl<char,std::char_traits<char> > (0151096h)
00152483 68 30 8B 15 00 push offset string "__try\xbf\xe9\xd6\xd0\xd6\xb4\xd0\xd0\xca\xb1\xcc\xe1\xc7\xb0\xcd\xcb\xb3\xf6\xc1\xcb" (0158B30h)
00152488 A1 D8 C0 15 00 mov eax,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (015C0D8h)]
0015248D 50 push eax
0015248E E8 FB EE FF FF call std::operator<<<std::char_traits<char> > (015138Eh)
00152493 83 C4 08 add esp,8
00152496 8B C8 mov ecx,eax
00152498 FF 15 A4 C0 15 00 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (015C0A4h)]
0015249E 3B F4 cmp esi,esp
001524A0 E8 BE EC FF FF call __RTC_CheckEsp (0151163h)
18: }
19: else
001524A5 EB 29 jmp funcTest01+0E0h (01524D0h)
20: {
21: cout << "执行流程自然转到了__finally块中" << endl;
001524A7 8B F4 mov esi,esp
001524A9 68 96 10 15 00 push offset std::endl<char,std::char_traits<char> > (0151096h)
001524AE 68 50 8B 15 00 push offset string "\xd6\xb4\xd0\xd0\xc1\xf7\xb3\xcc\xd7\xd4\xc8\xbb\xd7\xaa\xb5\xbd\xc1\xcb__finally\xbf\xe9\xd6\xd0" (0158B50h)
001524B3 A1 D8 C0 15 00 mov eax,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (015C0D8h)]
001524B8 50 push eax
001524B9 E8 D0 EE FF FF call std::operator<<<std::char_traits<char> > (015138Eh)
001524BE 83 C4 08 add esp,8
001524C1 8B C8 mov ecx,eax
001524C3 FF 15 A4 C0 15 00 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (015C0A4h)]
001524C9 3B F4 cmp esi,esp
001524CB E8 93 EC FF FF call __RTC_CheckEsp (0151163h)
$LN14:
001524D0 C3 ret
22: }
23: }
24:
25: cout << dwTemp << endl;
001524D1 8B F4 mov esi,esp
001524D3 68 96 10 15 00 push offset std::endl<char,std::char_traits<char> > (0151096h)
001524D8 8B FC mov edi,esp
001524DA 8B 45 E0 mov eax,dword ptr [dwTemp]
001524DD 50 push eax
001524DE 8B 0D D8 C0 15 00 mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (015C0D8h)]
001524E4 FF 15 A0 C0 15 00 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (015C0A0h)]
001524EA 3B FC cmp edi,esp
001524EC E8 72 EC FF FF call __RTC_CheckEsp (0151163h)
001524F1 8B C8 mov ecx,eax
001524F3 FF 15 A4 C0 15 00 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (015C0A4h)]
001524F9 3B F4 cmp esi,esp
001524FB E8 63 EC FF FF call __RTC_CheckEsp (0151163h)
26: return 0;
00152500 33 C0 xor eax,eax
27: }

部分栈帧示意图与异常状态标记变量

首先来画个函数部分栈帧图,我们只关心SEH那部分内容:

ebp-0x10,fs:[0]-> 原FS:[0]
ebp-C __except_handler4
ebp-8 0x159EE8 xor __security_cookie
ebp-4 -2
ebp 原ebp

另外,还需要关注其中的[ebp-0ECh]这个局部变量:

  • 在进入__try块后,其值被设为1,如果没能执行到__try块的末尾就进入__finally块,其值就会保持1。

  • 而在__try块正常退出后,其值被设为0,这时再进入__try块,其值就为0。

综上所述,这个局部变量就是VS判断__try是否提前退出的伪函数AbnormalTermination()的关键,因此不妨将该局部变量取名为异常状态标记。下面在__finally块的头部用内联汇编修改该标记加以实证:

_asm mov[ebp - 0xEC], 1

运行后,AbnormalTermination()函数返回结果果然就变成了真,结果如下图:
Visual C++异常处理机制原理与应用(一)—— C/C++结构化异常处理之try-finally终止处理的使用与原理(上)

正常情况下执行流的转移过程

另外,还有一点需要注意的是,__finally块中的代码实际上是一个函数。当__try块正常退出后,即call到__finally块的函数入口,__finally块执行完毕后ret到call指令下一行的jmp指令跳到__finally块后的语句。

总结

将这部分内容用一张图加以展示,作为总结吧:

Visual C++异常处理机制原理与应用(一)—— C/C++结构化异常处理之try-finally终止处理的使用与原理(上)

2017.12.03补充:分析完VS2010的except_handler4函数后,对于上图中2和5的描述其实是不准确的,特此更正:

  • 这里的-2是指当前执行的代码块不处于任何try块的保护范围内。
  • 0表示的是当前代码块处于最外层try块的保护范围内。
  • 如果是1则表示的是当前代码块处于次外层try块的保护范围内,以此类推。
  • 在本例中,由于只有一个try块,因此在进入try块时,将该值置为0。