Win32结构化异常处理(SEH)——终止处理程序(__try/__finally)

 

环境:VC++6.0, Windows XP SP3

        当我们想编写一个健壮的程序时,我们会用到异常处理,对各种异常进行考虑并进行处理。现在在各种语言都有自己的异常处理机制,比如C++的try, catch, throw,JAVA也一样。不过它们的实现都要基于OS。

        Microsoft为了使系统程序和应用程序更加健壮,把异常处理加入了Windows。这里的异常处理就是SEH, 结构化异常处理(Structured Exception Handler) 。SEH可以分为两个功能,一是 终止处理程序 ,二是 异常处理程序 

        这篇文章讲了OS级SEH和编译器级SEH实现终止处理程序,但是没有讲编译器级SEH实现异常处理程序。

        在这里,我先重点说一下终止处理程序。

 

        至于终止处理程序的语法,大致是下面的样子(详细的可以看Jeffery的"Windows核心编程"):

  __try

  {

    //Guarded body

  }

  __finally

  {

    //Termination handler

  }

        终止处理程序可以确保不论try块中的代码是如何退出的,finally块中的代码都可以得到执行,无论我们在try块中使用了return,goto还是longjmp, 除非我们使用了函数ExitProcess, ExitThread, TerminateProcess, TerminateThread这四个之中的一个,则finally块中的代码不会得到执行 。因为系统执行finally中的代码,靠的是在线程栈中的信息,而上面四个函数会破坏线程栈,所以finally块中的代码就执行不了了。这句有点儿难理解?线程栈中的信息是什么?没关系,我下面会说到的。

       

        下面举一个简单的终止处理程序的例子:

  char *p = NULL;

  __try

  {

    p = new char[256];

    return;

  }

  __finally

  {

    delete[] p;

    p = NULL;

  }

  在执行try块时,为p分配了内存,然后退出函数,如果没有终止处理程序,显然会发生内存泄露,而当我们使用终止处理程序时,程序会在函数退出前,执行finally块中的内容,这样就没有内存泄露了。

 

        以下,我们将要探讨一下,终止处理程序是如何实现的。因为SEH分为了终止处理程序和异常处理程序,所以系统在实现这两个功能时,不同之处仅仅在于终止处 理程序没有ExceptionFilter,系统也是通过这个来判断一个try块后面跟是finally还是except,这也是为什么一个try块后面 只能跟finally或except中的一个,所以我认为finally块也可以看作是一个比较特殊的异常处理程序。又因为SEH的实现可以分为OS级的 实现和编译器级的实现,所以我们先看一下OS级的实现,这个简单一些。然后再介绍编译器级(MSVC++)的实现。

        我们先想一下,当我们遇见异常时,如果我们要处理,我们必须给系统提供一个异常处理的回调函数,这个函数将会在异常发生时被调用,这个函数的原型如下:

 EXCEPTION_DISPOSITION
__cdecl _except_handler(
     struct _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     );   
至于这个比较奇怪的返回值,我们现在可以不用去管它,我们现在来看一下参数,其实对我们比较有意义的参数就第一个和第三个,剩下的没目前不用管。  
 typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;                               //这是OS产生的异常代码  
DWORD ExceptionFlags;                              //产生异常的一些标致  
struct _EXCEPTION_RECORD *ExceptionRecord;                           //用于深层的嵌套  
PVOID ExceptionAddress;                             //发生异常的指令地址,也就是eip 
  DWORD NumberParameters;                       //目前没用 
  DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];             //目前没用  
}  EXCEPTION_RECORD;   
至于结构体CONTEXT的内容,这里就不再往出列了,因为这个结构体内容太多了,如果想看,在文件winNT.h里。这个结构体保存了发生异常时,CPU各个寄存器的值,也就是上下文了,呵呵。   
有了这些参数,我们在异常处理函数里,就可以知道发生异常时的状况了,也就可以作出相应的动作了。  
        知道了这个回调函数,你可能会想系统怎么知道我有这么一个函数,应该在什么时候安装这个函数,并且把这个函数安装在哪儿呢?  
   来看一个下面这个结构体:  
 EXCEPTION_REGISTRATION struc
     prev    dd      ?
     handler dd      ?
_EXCEPTION_REGISTRATION ends   
这个结构体是用masm汇编定义的,这是一个链表的结点类型,成员prev指向链表的前一向,成员handler就是我们刚才提到的回调函数。所以说,只要把回调函数放在这样一个链表中,系统通过链表就可以找到我们的回调函数了。那么系统又是如何定位到这个链表呢?  
        在Windows程序运行时,段选择子fs始终指向一个数据结构,这个数据结构就是TEB(Thread Environment Block),而TEB中第一个成员又是一个数据结构,TIB(Thread Information Block),在TIB的0偏移量处,是一个指向EXCEPTION_REGISTRATION结构体的指针,没错,就是我们上面提到的那个链表(SEH链)的头指针,所以,我们如果想使用系统的SEH服务,我们就构造这样一个结构体,并且把它放到fs:[0]指向的链表中去就可以了。下面是一个例子:  
data dd ?    ;定义一个有效的数据   
push offset myhandler    ;安装异常处理函数
mov eax, fs:[0]       ;获得前一个EXCEPTION_REGISTRATION结构的地址
push eax         ;形成一个EXCEPTION_REGISTRATIONXF结构
mov fs:[0], esp        ;表头插入上面形成的结点   
mov eax, 0           ;
mov [eax], 1         ;向0地址处写,为了产生一个异常
ret   
myhandler:           ;异常处理函数
mov eax, [esp + 0Ch]                ;记得这个函数的参数表吧,得到第三个参数PCONTEXT的值
mov [eax + 40h], offset data     ;将data的地址赋给CONTEXT中EAX的成员,在函数退出时,系统会把CONTEXT中EAX的值赋给寄存器EAX,这样,EAX就代表了一个有效的地址了
mov eax,  EXCEPTION_CONTINUE_EXECUTION                  ;告诉系统,这个异常我已经处理,可以重新执行产生异常的指令
ret   
从上面这个例子中,我们可以看到,安装一个异常处理函数很简单,安装完后,如果系统产生了一个异常,系统会从fs:[0]指向的第一个元素开始查找异常处理函数,并执行这些函数,可能现在你会问,难道系统每产生一个异常,这个链表中的函数就都要执行一遍吗?当然不是,当一个异常处理函数可以解决这个异常的时候,系统就不会执行后面的函数了。那么系统如何知道某个函数是否解决了这个异常呢?看到上面这个异常处理函数的返回值了吗,是 EXCEPTION_CONTINUE_EXECUTION  ,它的意思就是,我已经把问题解决了,CPU你可以重新执行刚才产生异常的那个指令了,像这样的返回值还有两个分别是: EXCEPTION_EXECUTE_HANDLER   EXCEPTION_CONTINUE_SEARCH  ,前者的意思是,我把问题解决了,CPU你从产生异常的指令的下一个指令执行吧,后者的意思是,这个问题我没办法,CPU你找别人吧,这时系统会执行链表中下一个异常处理函数。如果某个异常链表中所有的异常处理函数都解决不了,那么系统会结束掉当前的线程或进程。  
   这样,我们就知道了,如何用汇编语言使用SEH,其实也不难。  
   虽然我们知道了SEH是怎么实现的,但是如果每次要用SEH时,都把程序写成这样儿,只会使我们的程序的可读性和健壮性变差,SEH的好处会被这些缺点盖掉。那么,应该怎样做才能使SEH的优点突显出来呢。我们来看看MSVC++6.0是怎样实现SEH的吧,这也就是编译器级的实现。VC6在实现SEH时没有像上面所说的,把每个异常处理函数都链到那个链表里,而是在链表里只加了一个函数__except_handler3,由这个函数调用注册的每个异常处理函数。下面介绍几个在编译器级实现中的结构体:  
在这里,SEH链中结点的类型不再是前面的那个EXCEPTION_REGISTRATION,而是:

struct _EXCEPTION_REGISTRATION
{
  struct _EXCEPTION_REGISTRATION *prev;          //维持链表的指针
    void (*handler)(PEXCEPTION_RECORD,PEXCEPTION_REGISTRATION,PCONTEXT,PEXCEPTION_RECORD);             //异常处理函数
    struct scopetable_entry *scopetable;
  int trylevel;
  int _ebp;                  //try块的ebp
  };    
trylevel是为了try块的嵌套而设计的,最外层的trylevel为-1,进一层try块,trylevel就加1。scopetable是一个编译完就产生好的结构体数组,每一项记录了每层try的trylevel, lpfnFilter, lpfnHandler,得到异常后,__except_handler3通过这个数组就可以定位到每个try块了。   
typedef struct _SCOPETABLE
{
  DWORD previousTryLevel;
  DWORD lpfnFilter;
  DWORD lpfnHandler;
}SCOPETABLE, *PSCOPETABLE;    
看到了吧,每个try块就可以用这个结构体的表示了,如果是finally块,则lpfnFilter == NULL。   
还需要提一下的是,SEH链表的每个结点都是在栈中的,所以不用释放,当线程退出时,资源被自动回收。   
有如下代码(我们现在只看终止处理程序):  
int main()
{//trylevel = -1;
  int a = 0;        //这只是一个标记,没什么实际意义  
  __try
  {//trylevel = 0;
    a = 1;
    __try
    {//trylevel = 1;
      a = 5;
      return 0;
    }
     __finally
    {
      a = 6;
    }
  }
  __finally
  {
    a = 2;
  }
  a = 3;
  return 0;
}  
反汇编后如下:  
6:    int main()
7:    { ;下面会产生一个EXCEPTION_REGISTRATION结点,以后把这个结点叫做pReg,一会儿看见了别说不知道啊  
0040D4B0   push        ebp                    ;保存上级栈帧,从这句开始,就是产生SEH链的结点,并链到SEH链上
   0040D4B1   mov         ebp,esp            ;保存本级栈帧
   0040D4B3   push        0FFh                ;pReg->trylevel = -1  
0040D4B5   push        offset string "stream != NULL"+14h (00422f80)       ;pReg->scopetable = 0042ff80  
0040D4BA   push        offset __except_handler3 (004011f0)             ;pReg->handler = __except_handler3  
0040D4BF   mov         eax,fs:[00000000]             ;
0040D4C5   push        eax                ;pReg->prev = fs:[0]
   0040D4C6   mov         dword ptr fs:[0],esp           ;改变链表首指什为pReg
   0040D4CD   add         esp,0B0h                    ;这个地方很恶的,从字面上看明明是加,其实加的是0FFFFFFB0h  
0040D4D0   push        ebx                     ;保护寄存器  
0040D4D1   push        esi
0040D4D2   push        edi
0040D4D3   lea         edi,[ebp-60h]
0040D4D6   mov         ecx,12h
0040D4DB   mov         eax,0CCCCCCCCh
0040D4E0   rep stos    dword ptr [edi]
8:        int a = 0;
0040D4E2   mov         dword ptr [ebp-1Ch],0                ;a = 0
   9:        __try
0040D4E9   mov         dword ptr [ebp-4],0                    ;pReg->trylevel = 0,进了一层try
   10:       {
11:           a = 1;
0040D4F0   mov         dword ptr [ebp-1Ch],1                ;a = 1
   12:           __try
0040D4F7   mov         dword ptr [ebp-4],1                     ;pReg->trylevel = 1,又进了一层try  
13:           {
14:               a = 5;
0040D4FE   mov         dword ptr [ebp-1Ch],5                  ;a = 5,因为编译器知道下面有一个return,所以要执行局部展开  
0040D505   push        0FFh                                            ;__local_unwind2的参数
   0040D507   mov         dword ptr [ebp-20h],0                   ;把return的值先保存在一个局部变量里
   0040D50E   lea         eax,[ebp-10h]                              ;
0040D511   push        eax                                                ;__local_unwind2的参数,也就是pReg  
0040D512   call        __local_unwind2 (0040113a)           ;进行局部展开,两个finally块的内容会在这个函数里被调用
   0040D517   add         esp,8                                             ;恢复栈
   15:               return 0;
0040D51A   mov         eax,dword ptr [ebp-20h]            ;return 0
   0040D51D   jmp         $L539+9 (0040d546)                ;
16:           }
17:           __finally
18:           {
19:               a = 6;
0040D51F   mov         dword ptr [ebp-1Ch],6
$L541:
0040D526   ret
20:           }
21:       }
0040D527   mov         dword ptr [ebp-4],0FFFFFFFFh     ;因为一会儿不会进行局部展开,所以直接把trylevel = -1,不过,这里不会被执行到的
   0040D52E   call        $L536 (0040d535)
0040D533   jmp         $L539 (0040d53d)
22:       __finally
23:       {
24:           a = 2;
0040D535   mov         dword ptr [ebp-1Ch],2
$L537:
0040D53C   ret
25:       }
26:       a = 3;
0040D53D   mov         dword ptr [ebp-1Ch],3
27:       return 0;
0040D544   xor         eax,eax
28:   }
0040D546   mov         ecx,dword ptr [ebp-10h]             ;程序在这里完了,要恢复链表的头结点, ecx = pReg->prev  
0040D549   mov         dword ptr fs:[0],ecx
0040D550   pop         edi
0040D551   pop         esi
0040D552   pop         ebx
0040D553   add         esp,60h
0040D556   cmp         ebp,esp
0040D558   call        __chkesp (004010c0)
0040D55D   mov         esp,ebp
0040D55F   pop         ebp
0040D560   ret
上面的程序相信大家除了一个地方以外都能明白,这个地方就是函数__local_unwind2(EXCEPTION_REGISTRATION*,int)是干什么的,下面我们就看一下这个函数是怎么实现的:  
__local_unwind2(_EXCEPTION_REGISTRATION pRegistration, int tryLevel):  
  ;这个函数有两个局部变量:EXCEPTION_REGISTRATION pReg;  dword pre_level;
   0040113A   push        ebx          ;保护寄存器
   0040113B   push        esi
0040113C   push        edi
0040113D   mov         eax,dword ptr [esp+10h]    ;eax = pRegistration,下面将会给SEH链上再加一个结点,这样做的原因是,如果在用户写的异常处理函数里又发生了异常,系统可以调用__global_unwind2进行处理
   00401141   push        eax
00401142   push        0FEh                                                  ;pre_level = -2
   00401144   push        offset __global_unwind2+20h (00401118)
00401149   push        dword ptr fs:[0]
00401150   mov         dword ptr fs:[0],esp
00401157   mov         eax,dword ptr [esp+20h]                      ;pReg = pRegistration  
0040115B   mov         ebx,dword ptr [eax+8]                        ;ebx = pReg->scopetable
   0040115E   mov         esi,dword ptr [eax+0Ch]                      ;esi = pReg->trylevel
   00401161   cmp         esi,0FFh                                             ;如果这个函数在最外层的try块的外面被调用,则什么也不做
   00401164   je          __NLG_Return2+2 (00401194)
00401166   cmp         esi,dword ptr [esp+24h]                      ;和上一个判断意思一样,只不过用于比较的值是第二个参数   
0040116A   je          __NLG_Return2+2 (00401194)
0040116C   lea         esi,[esi+esi*2]                                       ;esi = esi + esi * 2
   0040116F   mov         ecx,dword ptr [ebx+esi*4]                     ;ecx = pReg->scopetable->pre_trylevel
   00401172   mov         dword ptr [esp+8],ecx                          ;pre_level = ecx
   00401176   mov         dword ptr [eax+0Ch],ecx                       ;pReg->trylevel = ecx ,表示已经处理了一层try块,为下一次的循环做准备
   00401179   cmp         dword ptr [ebx+esi*4+4],0                     ;pReg->lpfnFilter是否为NULL
   0040117E   jne         __NLG_Return2 (00401192)                   ;如果是NULL,则这是个finally块,否则是except块,若是except块,则这个函数不做什么
   00401180   push        101h                                                    ;the parameter of function __NLG_Notify
   00401185   mov         eax,dword ptr [ebx+esi*4+8]                 ;the parameter of function __NLG_Notify
   00401189   call        __NLG_Notify (004011ce)                     ;这个是干嘛的我不清楚
   0040118E   call        dword ptr [ebx+esi*4+8]                         ;能走到这一句,则说明这是个finally块的处理函数,调用这个函数
   __NLG_Return2:
00401192   jmp         __local_unwind2+1Dh (00401157)         ;进行下一次循环
   00401194   pop         dword ptr fs:[0]                                    ;卸载__global_unwind2
   0040119B   add         esp,0Ch
0040119E   pop         edi
0040119F   pop         esi
004011A0   pop         ebx
004011A1   ret  
从这个函数中,我们可以看到终止处理函数是如何被调用的,现在就差一点我没给大家列出来了,就是scopetable的值: 
00422F80   FF FF FF FF 00 00 00    .......
00422F87   00 41 D5 40 00    00 00    .A誁...
00422F8E   00 00 00 00 00 00 1F    .......
00422F95   D5 40 00  00 00 00 00  誁.....
00422F9C  00 00 00 00 00 00 00  ....... 
这样,对于终止处理函数是如何执行的,大家应该清楚了吧。 
   总结一下,当你想通过非正常途径跳出try块时,系统会在这里进行一个局部展开,在SEH链中寻找这个try块外层的所有try块的终止处理函数并执行,如果没有终止处理函数而是异常处理函数,则不执行,如果你通过ExitProcess, ExitThread, TerminateProcess, TerminateThread这四个之一跳出,则终止处理函数不会执行,因为,我们已经看到,SEH的信息都存在栈中,这四个函数会破坏掉线程的栈,所以,终止处理函数不会执行。说了这么多,其实就这么几句话,关于终止处理函数的详细介绍在"Windows核心编程"里有。 
   不过,这里只说了终止处理程序的执行,对于编译器级SEH如何实现异常处理程序,则与__except_handler3有关,这个下次再说。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值