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

异常处理是我们日常编程会不时用到但是却很少深入了解的部分,也是硬件、操作系统、编译器与用户程序需要密切配合才能完成的一个复杂过程。我之前学习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,std::char_traits > (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<<::char_traits > (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,std::char_traits >::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,std::char_traits > (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<<::char_traits > (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,std::char_traits >::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,std::char_traits > (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,std::char_traits >::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,std::char_traits >::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()函数返回结果果然就变成了真,结果如下图:
这里写图片描述

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

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

总结

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

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

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

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

你可能感兴趣的:(调试与反汇编)