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

时间:2022-12-15 13:31:06

环境:VC++6.0, Windows XP SP3

        上一篇中,我们看到了如何实现一个终止处理程序,和系统级的异常处理程序,在这一篇中,我们要着重看一下VC++6.0的异常处理程序是如何实现的。

        这里要用的结构体有:

 

   typedef struct _SCOPETABLE
  {
    DWORD       previousTryLevel;
    DWORD       lpfnFilter
    DWORD       lpfnHandler
  } SCOPETABLE, *PSCOPETABLE;


  struct _EXCEPTION_REGISTRATION

  {
    struct _EXCEPTION_REGISTRATION *prev;
    void (*handler)(PEXCEPTION_RECORD,
          PEXCEPTION_REGISTRATION,
          PCONTEXT,
          PEXCEPTION_RECORD);
    struct scopetable_entry *scopetable;
    int trylevel;
    int _ebp;
    PEXCEPTION_POINTERS xpointers;
  };

 

        如果你不知道这两个结构体是干嘛的,建议你先看一下前一篇文章,再来看这里。

        我们知道在系统级的异常处理程序中,那个SEH链上串的EXCEPTION_REGISTRATION结构体只有两项,就是上面的结构体的前两项,而且系 统级的异常处理中没有我们现在所用的过滤函数和处理函数之分,只有一个handler,而现在我们在用C语言或都C++写程序时,用到的SEH时,都有过 滤函数(就是except后面的小括号里的内容)和处理函数(就是except后面的大括号里的内容),其实系统SEH并没有给我们提供这样的服务,我们 之所以可以这样用,是因为编译器的原因。至于系统是如何做SEH的,在上一篇文章我已经说了。现在我们来看一下,VC++6.0是怎样做的。

        在VC++6.0中,基本上说只有一个异常处理函数(为什么说是基本上,后面我会说的)——__except_handler3,这个函数的指针会在产生 一个EXCEPTION_REGISTRATION结构时,赋给其成员handler,以后如果产生异常,系统就会调用这个函数,再由这个函数调用用户写 的异常处理函数,也就是说,用户写的异常处理函数(就是你自己写的except后面大括号里的内容)是没有注册到 EXCEPTION_REGISTRATION中的。那么,用户自己的异常处理函数在哪里呢?其实,编译器把用户的异常处理函数放到了一个结构体数组中, 这个结构体类型就是SCOPETABLE,每个try块会对应这么一个结构体,结构体中保存了它到底是哪一个try块(previousTryLevel 表示它的上一层try块的编号,最外层的try块的trylevel为0,所以,可以想像最外层的try块的previousTryLevel为-1), 它的过滤函数以及异常处理函数。在一个函数中出现的try块,编译器会把这些try块对应的SCOPETABLE结构体放到一个数组里,并把这个数组的首 地址给这个函数的EXCEPTION_REGISTRATION结构的成员scopetable。这样,我想大家应该就可以基本上理解系统产生一个异常 时,编译器是如何处理的了。

        在上面一话段中,你可能会发现一个函数拥有一个EXCEPTION_REGISTRATION结构,通过这个结构中可以访问这个函数中所在的try块,其 实就是这样的,这就是VC++6.0对SEH所做的一些修改,如果你有多个函数中使用了SEH,那么你会发现fs:[0]上有多个 EXCEPTION_REGISTRATION结构。虽然这和我们看到的系统级SEH不太一样,但这里的SEH与系统的SEH是兼容的,你仔细看这里的 EXCEPTION_REGISTRATION结构,你会发现前两项与系统的一样,不一样的内容只是在后面,所以系统看到编译器产生的SEH结构以后,也 是可以认得的。

        加一句,后面我会为了方便把EXCEPTION_REGISTRATION结构称为frame,声明一下。

        到这里,我相信大家可以大致想像一下VC++6.0处理后的SEH是怎么运行的了吧。下面,我会仔细说一下这个过程的。

        我们先看一下结构体EXCEPTION_REGISTRATION,这个结构体和系统的不一样,但是,为什么要这样设计呢?前四项我就不说了,大家都能看懂,现在就剩下两个了,_ebp,xpointers。

        先看_ebp,这个成员保存的值是产生这个frame的函数的栈帧指针,为的是得到这个frame后,可以轻松访问其栈帧,还有一个作用就是在当前函数 里,你可以通过对ebp的负偏移来访问这个frame中的成员,比如trylevel是[ebp - 04h],scopetable是[ebp - 08h]等等。

        再来看看xpointers。我们知道,在异常处理时,有一个API叫LPEXCEPTION_POINTERS GetExceptionInformation(void),这个函数的做用是返回异常信息,下面是EXCEPTION_POINTERS的定义,看一 下就行了,目前对我们没什么用:

 

   typedef struct _EXCEPTION_POINTERS { 
    PEXCEPTION_RECORD ExceptionRecord; 
    PCONTEXT ContextRecord; 
  } EXCEPTION_POINTERS;

 

MSDN中对这个函数有一句话: The filter expression can invoke a filter function. The filter function cannot call GetExceptionInformation . However, the return value ofGetExceptionInformation can be passed as a parameter to a filter function. 是什么意思呢,就是说GetExceptionInformation这个函数只能在except后面的那个小括号里被调用,为什么有这么奇怪的一条规定呢,我们来看一下这个函数的实现,就一句:

 

mov eax, dword ptr [ebp - 14h]

 

这是什么意 思呢,我们再看看EXCEPTION_REGISTRATION的定义,会发现ebp - 14h就是成员xpointers,这时你可能会想,只要在产生EXCEPTION_REGISTRATION的函数里调用 GetExceptionInformation应该也可以啊,为什么只能在except后面的小括号里调用啊?你可以把一个用SEH的程序用 VC++6.0反汇编后看看,你会发现,在程序开始设置frame的值的时候,并没有设置xpointers的值,而这个值是在 __except_handler3里被设定的,所以 如果你在除了except后面的小括号里调用GetExceptionInformation的时候得到的值是无效的!!

        看到这儿,过程已经慢慢清楚了,但是还不是很清楚,因为整个过程的实现是通过函数__except_handler3来做的,下面让我们看看这个函数是什 么内容,因为找到一组伪代码,所以用伪代码来分析(有点儿长,大家忍着点儿,不过看汇编的更长,呵呵):

 

int __except_handler3(        //从参数来看,就是我们上一篇文章中提到的那个回调函数,这儿不多说了
     struct _EXCEPTION_RECORD * pExceptionRecord,
     struct EXCEPTION_REGISTRATION * pRegistrationFrame,
     struct _CONTEXT *pContextRecord,
     void * pDispatcherContext )
{
     LONG filterFuncRet                   
//没什么意思,往后看就知道这个变量是干什么的了
     LONG trylevel                             //搞一个局部变量来存pRegistrationFrame->trylevel
     EXCEPTION_POINTERS exceptPtrs                         //这个就是GetExceptionInformation的返回值
     PSCOPETABLE pScopeTable                          //搞一个局部变量来存pRegistrationFrame->scopetable
 
     CLD    
 // Clear the direction flag (make no assumptions!)
 
   
   // if neither the EXCEPTION_UNWINDING nor EXCEPTION_EXIT_UNWIND bit
     // is set...  This is true the first time through the handler (the
     // non-unwinding case)
 


     if ( ! (pExceptionRecord->ExceptionFlags
             & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
     {
         
// Build the EXCEPTION_POINTERS structure on the stack
         exceptPtrs.ExceptionRecord = pExceptionRecord;
         exceptPtrs.ContextRecord = pContextRecord;

         
// Put the pointer to the EXCEPTION_POINTERS 4 bytes below the
         // establisher frame.  See ASM code for GetExceptionInformation
         *(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;    //看到了吧,这三句就是设定pRegistration->xpointers,现在明白为什么只能在except的小括号里调用GetExceptionInformation了吧
 
         
// Get initial "trylevel" value 
         trylevel = pRegistrationFrame->trylevel 

         
// Get a pointer to the scopetable array 
         scopeTable = pRegistrationFrame->scopetable;

search_for_handler: 

         if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )
         {
             if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter )
             {
                 PUSH EBP                        
// Save this frame EBP

                 // !!!Very Important!!!  Switch to original EBP.  This is
                 // what allows all locals in the frame to have the same
                 // value as before the exception occurred.
 

                 EBP = &pRegistrationFrame->_ebp 

                 
// Call the filter function 
                 filterFuncRet = scopetable[trylevel].lpfnFilter();    
 //原来我们自己写的过滤函数是在这儿被调用的啊
 
                 POP EBP                     
     // Restore handler frame EBP

                 if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )
                 {
                     if ( filterFuncRet < 0 ) 
// EXCEPTION_CONTINUE_EXECUTION
                         return ExceptionContinueExecution;

                    
 // If we get here, EXCEPTION_EXECUTE_HANDLER was specified
                     scopetable == pRegistrationFrame->scopetable

                     // Does the actual OS cleanup of registration frames
                     // Causes this function to recurse
 

                     __global_unwind2( pRegistrationFrame );

                     // Once we get here, everything is all cleaned up, except
                     // for the last frame, where we'll continue execution
 

                     EBP = &pRegistrationFrame->_ebp
                     
                     __local_unwind2( pRegistrationFrame, trylevel );

                     // NLG == "non-local-goto" (setjmp/longjmp stuff) 
                     __NLG_Notify( 1 );  // EAX == scopetable->lpfnHandler

                     // Set the current trylevel to whatever SCOPETABLE entry
                     // was being used when a handler was found
 

                     pRegistrationFrame->trylevel = scopetable->previousTryLevel;

                     // Call the _except {} block.  Never returns. 
                     pRegistrationFrame->scopetable[trylevel].lpfnHandler();
                 }
             }

             scopeTable = pRegistrationFrame->scopetable;
             trylevel = scopeTable->previousTryLevel

             goto search_for_handler;
         }
         else   
 // trylevel == TRYLEVEL_NONE
         {
             retvalue == DISPOSITION_CONTINUE_SEARCH;
         }
     }
     else  
   // EXCEPTION_UNWINDING or EXCEPTION_EXIT_UNWIND flags are set 
     {
         PUSH EBP   
 // Save EBP
         EBP = pRegistrationFrame->_ebp  // Set EBP for __local_unwind2

         __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )

         POP EBP    
 // Restore EBP

         retvalue == DISPOSITION_CONTINUE_SEARCH;
     }
}

注: 这个函数的伪代码来自于Matt Pietrek 1997年的论文"A Crash Course on the Depths of Win32 Structured Exception Handling",英语的注释就是作者自己写的,如果大家觉得我说得不清楚的话,建议看下这篇文章。

 

看到了吧, 如果我们的过滤函数返回的是EXCEPTION_CONTINUE_SEARCH,__except_handler3会看这个frame中的下一个过滤 函数,依次类推。还有一点要注意的是,如果我们的过滤函数返回值为EXCEPTION_EXECUTE_HANDLER的话,我们的异常处理会被 __except_handler3调用,但是, __except_handler3把控制权交给我们的程序后,我们的程序不会再返回到__except_handler3,而是顺序执行下去 ,这时也许你会想,就这样执行下去,栈的结构不是被破坏了吗,其实是破坏了,但是,破坏了我们还可以修复嘛,我们来看一段代码:

int main()
{
  int a = 0;
  __try
  {
    a = 1;
  }
  __except(filter(GetExceptionInformation()))  
//看到了吧,GetExceptionInformation只能在这儿调用
  {
    a = 2;
  }
  a = 3;
  return 0;
}

变成汇编我们再看看:

12:   int main()
13:   {
0040D860   push        ebp
0040D861   mov         ebp,esp
0040D863   push        0FFh
0040D865   push        offset string "stream != NULL"+14h (00422f80)
0040D86A   push        offset __except_handler3 (004011f0)
0040D86F   mov         eax,fs:[00000000]
0040D875   push        eax
0040D876   mov         dword ptr fs:[0],esp
0040D87D   add         esp,0B4h
0040D880   push        ebx
0040D881   push        esi
0040D882   push        edi
0040D883   mov         dword ptr [ebp-18h],esp    
;保存当前的ESP,待会儿用于恢复栈
0040D886   lea         edi,[ebp-5Ch]
0040D889   mov         ecx,11h
0040D88E   mov         eax,0CCCCCCCCh
0040D893   rep stos    dword ptr [edi]
14:       int a = 0;
0040D895   mov         dword ptr [ebp-1Ch],0
15:       __try
0040D89C   mov         dword ptr [ebp-4],0
16:       {
17:           a = 1;
0040D8A3   mov         dword ptr [ebp-1Ch],1
18:       }
0040D8AA   mov         dword ptr [ebp-4],0FFFFFFFFh
0040D8B1   jmp         $L564+11h (0040d8d1)
19:       __except(filter(GetExceptionInformation()))
0040D8B3   mov         eax,dword ptr [ebp-14h]    
;看到了吧,GetExceptionInformation就这么一句
0040D8B6   push        eax
0040D8B7   call        @ILT+10(filter) (0040100f)
0040D8BC   add         esp,4
$L565:
0040D8BF   ret
$L564:
0040D8C0   mov         esp,dword ptr [ebp-18h]     
 ;当我们的filter返回EXCEPTION_EXECUTE_HANDLER时,__except_handler3会把控制权交到这一句上,这句就是恢复栈的,所以,即使不用回到__except_handler3,程序也可以正常运行 
20:       {
21:           a = 2;
0040D8C3   mov         dword ptr [ebp-1Ch],2
22:       }
0040D8CA   mov         dword ptr [ebp-4],0FFFFFFFFh
23:       a = 3;
0040D8D1   mov         dword ptr [ebp-1Ch],3
24:       return 0;
0040D8D8   xor         eax,eax
25:   }
0040D8DA   mov         ecx,dword ptr [ebp-10h]
0040D8DD   mov         dword ptr fs:[0],ecx
0040D8E4   pop         edi
0040D8E5   pop         esi
0040D8E6   pop         ebx
0040D8E7   add         esp,5Ch
0040D8EA   cmp         ebp,esp
0040D8EC   call        __chkesp (004010c0)
0040D8F1   mov         esp,ebp
0040D8F3   pop         ebp
0040D8F4   ret
        现在,我们来总结一下,在VC++6.0中,如果我们在一个函数中使用了SEH,编译器会为我们产生一个EXCEPTION_REGISTRATION结 构,设置相应的值,并把这个结构串在fs:[0]指向的SEH链上,如果这个函数中有多个try块,不管是嵌套的还是平行的,编译器都会分别把这些块的信 息放在一个叫SCOPETABLE的结构中,并形成一个数组,并把这个数组的首地址赋给EXCEPTION_REGISTRATION结构的成员 scopetable,如果有多个函数,那么编译器会为我们产生多个EXCEPTION_REGISTRATION结构,并且都串在SEH链上。当一个用 到SEH链的函数产生异常时,系统会调用编译器设定在EXCEPTION_REGISTRATION里的异常处理函数 __except_handler3,再由这个函数在产生异常的那个函数对应的EXCEPTION_REGISTRATION结构中搜索,看哪个 except块可以解决这个异常(如果过滤函数返回不是EXCEPTION_CONTINUE_SEARCH,就表示可以解决),如果没有可以解决的,则 会转到一个叫UnhandledExceptionFilter函数那里,在这里,一般会结束掉产生异常的线程或进程,如果可以解决的话,而且过滤函数的 返回值又是EXCEPTION_EXECUTE_HANDLERR的话,__except_handler3会把控制权交给我们的except块的代码并 恢复栈指针ESP,程序继续向下执行。