不会开机的男孩

SEH学习笔记一

| Comments

SEH(structured exception handling),结构化异常处理。在windows本身开发中运用的非常广泛,而且MS并没有独享,并且通过vs为开发者提供了方便几个关键字来支持。try, exception,__finally。但是讲解的却非常少。本文希望能够给大家抛砖引玉一下。

http://www.microsoft.com/msj/0197/exception/exception.aspx,这篇是理解SEH必须的文章,虽然他的时间悠久,但是却真正的解释了SEH的编译器级实现,下面的一些示例代码也来自这里。

相关的不错的SEH文章,http://www.woodmann.com/crackz/Tutorials/Seh.htm。

http://blogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx 这里讲了一些.net 异常机制,之前讲一些SEH也很不错。

SEH中,在《windows 核心编程》中有一些讲解,但是我相信绝大多数,想我这样的初学者,并不能理解Jeffrey Richter的意思。其中最富有争议的就是“栈展开”(stack unwind),这个可以说是非常有想象力的一个词,伴随这个还有全局展开(global unwind),和局部展开(local unwind)。以下内容,主要围绕《windows 核心编程》中比较容易让我这样的初学者困惑的地方展开(unwind? :P)。

首先我们需要对SEH有一个大体的认识,

当异常出现的时候,我们可以有选择性的处理异常,将相同的异常处理函数集中一起,大大减少了代码的维护工作,这意味着处理异常的时候,将有类似非局部跳转的能力。 异常和返回值判断的最根本的不同是,异常真正的做到了健壮性,甚至连栈溢出的问题都可以恢复运行(当然,这个恢复没有任何意义,主要是能够保存错误信息)。所以异常是和操作系统结合的,所以必然导致了复杂性的大大提高,效率上的降低。 程序的执行,需要一些最基本的运行环境,而在windows 中则是contex,(上下文),其中保存了大量的寄存器的值,而通过这些可以保证程序的执行环境正确,而这是在进行非局部跳转必须做到的事情。所以,在遇到try block的时候,编译器会在栈空间上保存一些信息,做为一个结点并将这些信息用链表联系起来,这样,当异常发生的时候,操作系统找到链表的头结点,然后遍历list,执行我们的代码,并找到相对应的处理异常的代码。而这个头结点,就保存在FS:[0]。当windows 遍历list,并找到相对应的代码时,由于程序控制流程的改变,在发生异常,到找到执行代码的这部分之间的一些临时变量都没有被释放掉(这里面不仅有我们的,还有一些是编译器默默为我们做的,比如之前提到的try所加入的节点必须从之前的list删掉)。而这个做的释放过程就是unwind。处理多个try的为global unwind,处理当前的try 上的__try则是local unwind(这里不是很准确,后面会详细解释)。

结束处理程序(Termination Handlers),看起来简单也十分让人疑惑,为什么 return, goto,longjump,异常,控制流离开try block的时候,可以去执行finally block呢? 同样,为什么ExitProcess, ExitThread, TerminateProcess, or TerminateThread则不能被执行呢?为什么可以使用goto到try外面,而不能跳入一个try block?等等。

异常处理程序(Exception Handlers),则更让新手疑惑,特别是在结合了结束处理程序情况下,在程序的执行流程则变的诡异起来,而我们看到在vc中的SEH并不能够支持finally 和except结合一起使用,这又是为什么?使用SEH是否为我们程序增加了相当的负担?SEH是否安全?

为了清楚的认识这些问题,我们必须更进一步的去探究SEH的具体实现过程,由于不同厂商不同编译器的实现方式不同,所以以下的部分来自MS自己的vc。而其由于SEH涉及到了一些安全问题和硬件的部分,所以在不同的vc 版本,不同的操作系统不同的计算机下的情况也不同。当然,为了简单,我们先看最简单的vc6。在我们正式进入细节的时候,让我们先暂时忘记那些__try关键字。

  异常是操作系统传给我们写的程序,我们写好处理异常的代码,那么操作系统是如何调用我们写的函数呢?当然是通过回调函数做的,那么这个回调函数是什么样子的呢?

EXCEPTION_DISPOSITION
__cdecl _except_handler(
     struct _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     );

在EXCPT.H中,我们可以找到这个定义。

typedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
}  EXCEPTION_RECORD;

EXCEPTION_RECORD 定义异常,更多的可以参考msdn,http://msdn.microsoft.com/en-us/library/aa363082(VS.85).aspx

contex的定义则根据不同的硬件有不同的定义,这里面定义了线程运行的环境,上下文。找到了回调函数,和异常的样子,那么操作系统是如何调用呢?还记得之前提到的list么?fs:[0],那里,有我们需要的,我们需要知道另一个结构体。这是一个汇编上的定义。

_EXCEPTION_REGISTRATION struc
        prev    dd              ?
        handler dd            ?
_EXCEPTION_REGISTRATION ends

prev记录了上一个_EXCEPTION_REGISTRATION结构体的地址,而handler则是我们回调函数的地址,操作系统通过fs:[0],找到了一系列的我们写的回调函数。

让我们先试一下。

//==================================================
// MYSEH - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH.CPP
// To compile: CL MYSEH.CPP
//==================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

DWORD  scratch;

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
    struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    unsigned i;

    // Indicate that we made it to our exception handler
    printf( "Hello from an exception handler\n" );

    // Change EAX in the context record so that it points to someplace
    // where we can successfully write
    ContextRecord->Eax = (DWORD)&scratch;

    // Tell the OS to restart the faulting instruction
    return ExceptionContinueExecution;
}

int main(int argc, char* argv[])
{
    DWORD handler = (DWORD)_except_handler;

    __asm
    {                           // Build EXCEPTION_REGISTRATION record:
        push    handler         // Address of handler function
        push    FS:[0]          // Address of previous handler
        mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION
    }

    __asm
    {
        mov     eax,0           // Zero out EAX
        mov     [eax], 1        // Write to EAX to deliberately cause a fault
    }

    printf( "After writing!\n" );

    __asm
    {                           // Remove our EXECEPTION_REGISTRATION record
        mov     eax,[ESP]       // Get pointer to previous record
        mov     FS:[0], EAX     // Install previous record
        add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack
    }

    return 0;
}

typedef enum _EXCEPTION_DISPOSITION {
    ExceptionContinueExecution,
    ExceptionContinueSearch,
    ExceptionNestedException,
    ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

vc通过类似的代码生成,在我们的这段代码,

mov eax,0  mov [eax], 1,

在栈空间上分配了一个EXCEPTION_REGISTRATION结构体,并插入了fs:[0]链表的表头。 当然,在最后跳出这个代码块的时候,这个栈空间的EXCEPTION_REGISTRATION结构体也必须从fs:[0]中卸载掉。而在_except_handler返回的ExceptionContinueExecution,则意味着告诉OS,需要从发生异常的那个语句重新执行,一切都是那么的简单和自然。为了简单,我们在首节点就处理了这个异常,让我们再进一步,看一下异常是如何传递的。

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
    struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    printf( "Home Grown handler: Exception Code: %08X Exception Flags %X",
        ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags );

    if ( ExceptionRecord->ExceptionFlags & 1 )
        printf( " EH_NONCONTINUABLE" );
    if ( ExceptionRecord->ExceptionFlags & 2 )
        printf( " EH_UNWINDING" );
    if ( ExceptionRecord->ExceptionFlags & 4 )
        printf( " EH_EXIT_UNWIND" );
    if ( ExceptionRecord->ExceptionFlags & 8 )
        printf( " EH_STACK_INVALID" );
    if ( ExceptionRecord->ExceptionFlags & 0x10 )
        printf( " EH_NESTED_CALL" );

    printf( "\n" );

    // Punt... We don't want to handle this... Let somebody else handle it
    return ExceptionContinueSearch;
}

void HomeGrownFrame( void )
{
    DWORD handler = (DWORD)_except_handler;

    __asm
    {                           // Build EXCEPTION_REGISTRATION record:
        push    handler         // Address of handler function
        push    FS:[0]          // Address of previous handler
        mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION
    }

    *(PDWORD)0 = 0;             // Write to address 0 to cause a fault

    printf( "I should never get here!\n" );

    __asm
    {                           // Remove our EXECEPTION_REGISTRATION record
        mov     eax,[ESP]       // Get pointer to previous record
        mov     FS:[0], EAX     // Install previous record
        add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    _try
    {
        HomeGrownFrame(); 
    }
    _except( EXCEPTION_EXECUTE_HANDLER )
    {
        printf( "Caught the exception in main()\n" );
    }
    return 0;
}

我们在_except_handler中返回了ExceptionContinueSearch,这会告诉windows,我们这个回调函数不处理这个异常,你找其他去吧。我们看到了这个输出结果。

Home Grown handler: Exception Code: C0000005 Exception Flags 0
Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING
Caught the exception in main()

  第一个我们很好理解,但是第二次是什么情况呢?这个就是之前提到的unwind过程。windows依次调用fs:[0]上的exceptionlist的回调函数,并根据返回值判断该如何执行,如果是ExceptionContinueSearch,则通过EXCEPTION_REGISTRATION 的prev寻找下一个,直到找到处理异常的函数(windows在创建线程的时候,已经为我们准备好了处理异常的程序)。在找到处理异常的代码后,windows会再一次遍历list,直到处理异常的地方。这一次和第一次不同的是Exception Flags | = EH_UNWINDING,这一次,正是给那些拒绝处理这个异常的代码块一次清理自己的机会,包括一些编译器默默为我们生成的一些临时东东的移除,c++一些临时对象的析构函数调用,从fs:[0],list上删除EXCEPTION_REGISTRATION 等等,当然,我们的finally block也正好趁着这个机会把自己执行了一次。但是,在我们开心的找到回调函数地址的时候,我们却不能直接执行这个地址的代码,因为在之前,很可能运行的环境已经变化了,许多寄存器的数值已经变化了,而且更重要的是ebp esp,很可能根本和我们的这个程序不符合,程序根本不能正确执行(之前做了很多的非局部跳转),所以,必须也把函数运行的状态保存起来,这样我们才能真正的执行我们的回调函数。那么这些状态保存在哪里呢?EXCEPTION_REGISTRATION结构体的地址,在windows fs:[0]可以找到, 那么我们只需要在原有的EXCEPTION_REGISTRATION成员下增加数据就可以找到这些状态。从而正确的恢复执行。

在进一步了解之前,让我们先回顾一下文法。

__try 
{
   //Guarded body
}
__except(exception filter) 
{
   // Exception handler
}

void FuncOStimpy1()
{
   //1. Do any processing here.

   ...

   __try
   {
      //2. Call another function.
      FuncORen1();

      // Code here never executes.
   }

   __except( /* 6. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) 
   {
      //8. After the unwind, the exception handler executes.
      MessageBox(…);
   }

   //9. Exception handled--continue execution.



}
void FuncORen1() 
{
   DWORD dwTemp = 0;

   //3. Do any processing here.



   __try
   {
      //4. Request permission to access protected data.
      WaitForSingleObject(g_hSem, INFINITE);

      //5. Modify the data.
      //    An exception is generated here.
      g_dwProtectedData = 5 / dwTemp;
   }
   __finally
   {
      //7. Global unwind occurs because filter evaluated
      //    to EXCEPTION_EXECUTE_HANDLER.

      // Allow others to use protected data.
      ReleaseSemaphore(g_hSem, 1, NULL);
   }
   // Continue processing--never executes.

   ...  

}

  有了现在的基础,在看上面的代码,在执行代码顺序上,已经没有疑惑了。我们所指的回调函数,其实就是exception filter,当异常在5处发生的时候,系统首先要遍历fs:[0],找到处理这个异常的代码,执行流程跑到了6,返回了EXCEPTION_EXECUTE_HANDLER,这告诉系统我认出了这个异常,然后,系统再次遍历fs:[0],这个就是unwind,然后,我们在7处的finally代码才执行,最后执行Exception handler的代码,然后程序从9处恢复执行。Jeffrey Richter中描述的global unwind,local unwind,又是什么意思呢?书写什么样的代码可以最大的提高效率?以及异常处理的效率为什么要慢呢?这背后还有许许多多的小问题,比如为什么goto 只能跳出try block,而不能跳入try block?GetExceptionCode为什么能够在filter expression 和exception-handler block,为什么不能在filter function中调用?而如果想弄清楚这一系列问题,我们需要更深入的了解SEH。当然,这才是学习的重点。由于这部分和系统相关,在异常的转发过程中,需要编译器和操作系统的支持,所以,我们需要找一个稍微简单一点的编译器和os,如果是第一次接触这个,那么最好是 vc6 + xp sp1或2000。如果对vc6有极大的抵触情绪(比如本人),使用08的时候需要在编译器中加入/GS-,否则编译器会在栈中生成其他代码(检测是否有溢出)越高的系统还可能会加入safeSEH,SEHOP,而且,具体的实现可能也会稍有不同,一上来全部接触,可能难度稍微有些大(对本人来说),所以,我们从最简单的开始。

让我们看下vc(vc6 vs2008),下的结构体。

struct _EXCEPTION_REGISTRATION {
     struct _EXCEPTION_REGISTRATION *prev;          //上一个结构体
     void (*handler)(PEXCEPTION_RECORD,             //我们的回调函数 
                     PEXCEPTION_REGISTRATION,
                     PCONTEXT,
                     PEXCEPTION_RECORD);
     struct scopetable_entry *scopetable;             
     int trylevel;
};

typedef struct _SCOPETABLE
{
    DWORD       previousTryLevel;
    DWORD       lpfnFilter        //我们的filter code address
    DWORD       lpfnHandler       //我们的exception handler block 或是 finally handler bloack address
} SCOPETABLE, *PSCOPETABLE;

这个trylevel有是什么呢?为什么要有SCOPETABLE?

我们考虑这样的一个问题。

{
...
    __try
    {
        __try { } __except() { }
    }
    __except()
    {

    }
...
    __try
    {

    }
    __except()
    {

    } 
...
}

当一个函数中,有非常多的try block时,如果我们每遇到一个try,就生成一个EXCEPTION_REGISTRATION ,加入fs:[0]然后离开之后,在从fs:[0]中卸载掉,这个的确是一个浪费时间,浪费空间的做法。vc 在做的时候,每个函数只是生成一个EXCEPTION_REGISTRATION 结构体,而在一个函数内,可能有嵌套的try block,也可能又并列的try block(以下把try 简写成try,这个的确不是一个好的书写,但是这个_是在是太麻烦了,try block 是c++的异常,和SEH很像,但也是有些不同的),那么如何才能分辨出到底是哪一个try block?trylevel 和SCOPETABLE,则是为了满足这个要求而实现的。在进入函数的时候,vc会把trylevel初始化为-1,这个表示目前的代码在当前的EXCEPTION_REGISTRATION 下,不属于try block保护下,遇到第一个try block的时候,vc把trylevel改为0,进入下一个并列的try block则为1….。struct scopetable_entry *则,保存了一个数组,previousTryLevel,告诉我们这个嵌套try block 的上一层block的index….。

可见,vc通过这些手段,在我们的代码之中,维护了一个树的结构,来标示每一个try block,并提供从内层到外层的遍历方法。handler,按理来所,应该跑我们的lpfnFilter ,这里会不会重复? 当然不会,vc实现_EXCEPTION_REGISTRATION 中,handler指向了同一个代码,vc 的运行时库函数 __except_handler ,根据vc版本后面3啊4啊什么的。原因也很简单,整个东东都有了嵌套,必然需要遍历,为了减少重复代码,和代码的安全,当然会都从一个函数入口开始,然后再去调用我们的代码。所以代码的地址,也需要保存。lpfnFilter 我们的except filter代码入口,lpfnHandler,则是我们的except block 入口。 那么,我们的finally在那里呢?由于,finally 并没有filter的概念,所以,当lpfnFilter == null的时候,vc会认为我们跑的是finally block,那么lpfnHandler则是我们的finally 的terminal handle。这也就告诉我们,为什么SEH中,不能同时存在finally 和except block了。

整个事情越来越有趣了,但是一大堆的论述,的确没有任何意思。还是让我们看看代码。我在原有的代码上加上了查看trylevel的代码。

#ifndef _MSC_VER
#error Visual C++ Required (Visual C++ specific information is displayed)
#endif

//----------------------------------------------------------------------------
// Structure Definitions
//----------------------------------------------------------------------------

// The basic, OS defined exception frame

struct EXCEPTION_REGISTRATION
{
    EXCEPTION_REGISTRATION* prev;
    FARPROC                 handler;
};


// Data structure(s) pointed to by Visual C++ extended exception frame

struct scopetable_entry
{
    DWORD       previousTryLevel;
    FARPROC     lpfnFilter;
    FARPROC     lpfnHandler;
};

// The extended exception frame used by Visual C++

struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
    scopetable_entry *  scopetable;
    int                 trylevel;
    int                 _ebp;
};

//----------------------------------------------------------------------------
// Prototypes
//----------------------------------------------------------------------------

// __except_handler3 is a Visual C++ RTL function.  We want to refer to
// it in order to print it's address.  However, we need to prototype it since
// it doesn't appear in any header file.

extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *,
                                PCONTEXT, PEXCEPTION_RECORD);


//----------------------------------------------------------------------------
// Code
//----------------------------------------------------------------------------

//
// Display the information in one exception frame, along with its scopetable
//

void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec )
{
    printf( "Frame: %08X  Handler: %08X  Prev: %08X  Scopetable: %08X\n",
            pVCExcRec, pVCExcRec->handler, pVCExcRec->prev,
            pVCExcRec->scopetable );

    scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable;

    for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ )
    {
        printf( "    scopetable[%u] PrevTryLevel: %08X  "
                "filter: %08X  __except: %08X\n", i,
                pScopeTableEntry->previousTryLevel,
                pScopeTableEntry->lpfnFilter,
                pScopeTableEntry->lpfnHandler );

        pScopeTableEntry++;
    }

    printf( "\n" );
}   

//
// Walk the linked list of frames, displaying each in turn
//

void WalkSEHFrames( void )
{
    VC_EXCEPTION_REGISTRATION * pVCExcRec;

    // Print out the location of the __except_handler3 function
    printf( "_except_handler3 is at address: %08X\n", _except_handler3 );
    printf( "\n" );

    // Get a pointer to the head of the chain at FS:[0]
    __asm   mov eax, FS:[0]
    __asm   mov [pVCExcRec], EAX

    // Walk the linked list of frames.  0xFFFFFFFF indicates the end of list
    while (  0xFFFFFFFF != (unsigned)pVCExcRec )
    {
        ShowSEHFrame( pVCExcRec );
        pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
    }       
}

void Function1( void )
{
    int tl=0;
    __try
    {
        __asm mov eax, [ebp-4]
        __asm mov tl, eax
        printf("try leval = %d\n", tl);
    }
    __except(EXCEPTION_CONTINUE_SEARCH)
    {

    }

    // Set up 3 nested _try levels (thereby forcing 3 scopetable entries)
    __try
    {
        __asm mov eax, [ebp-4]
        __asm mov tl, eax
        printf("try leval = %d\n", tl);
        __try
        {
            __asm mov eax, [ebp-4]
            __asm mov tl, eax
            printf("try leval = %d\n", tl);
            __try
            {
    __asm mov eax, [ebp-4]
    __asm mov tl, eax
    printf("try leval = %d\n", tl);
                WalkSEHFrames();    // Now show all the exception frames
            }
            __except( EXCEPTION_CONTINUE_SEARCH )
            {
            }
        }
        __except( EXCEPTION_CONTINUE_SEARCH )
        {
        }
    }
    __except( EXCEPTION_CONTINUE_SEARCH )
    {
    }
}

int main(int argc, char* argv[])
{
    int i;
    int tl=0;
    __asm mov eax, [ebp-4]
    __asm mov tl, eax
    printf("try leval = %d\n", tl);
    __try
    {
        __asm mov eax, [ebp-4]
  __asm mov tl, eax
  printf("try leval = %d\n", tl);

        Function1();    // Call a function that sets up more exception frames
    }
    __except( EXCEPTION_EXECUTE_HANDLER )
    {
        i = 0x4321;     // Do nothing (in reverse)
    }

    __try
    {
  __asm mov eax, [ebp-4]
  __asm mov tl, eax
  printf("try leval = %d\n", tl);

        Function1();    // Call a function that sets up more exception frames   
    }
    __except( EXCEPTION_EXECUTE_HANDLER )
    {
        // Should never get here, since we aren't expecting an exception
        printf( "Caught Exception in main\n" );
    }
    return 0; 
}

这里我们可以看到如下情况,当然,这个是在2003下的,win7,会有不同的结果。最好还是先不用win7。win7的问题,我也不清楚。这个只能先放下了。

try leval = -1

try leval = 0
try leval = 0
try leval = 1
try leval = 2
try leval = 3
_except_handler3 is at address: 004014C0

Frame: 0012FEFC  Handler: 004014C0  Prev: 0012FF70  Scopetable: 004210B8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401203  __except: 00401206
    scopetable[1] PrevTryLevel: FFFFFFFF  filter: 004012A4  __except: 004012A7
    scopetable[2] PrevTryLevel: 00000001  filter: 0040128E  __except: 00401291
    scopetable[3] PrevTryLevel: 00000002  filter: 00401278  __except: 0040127B

Frame: 0012FF70  Handler: 004014C0  Prev: 0012FFB0  Scopetable: 00420150
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 0040135F  __except: 00401365

Frame: 0012FFB0  Handler: 004014C0  Prev: 0012FFE0  Scopetable: 00420278
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401788  __except: 004017A3

Frame: 0012FFE0  Handler: 7C82B798  Prev: FFFFFFFF  Scopetable: 7C8123D8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 7C8571C8  __except: 7C8571DE

try leval = 1
try leval = 0
try leval = 1
try leval = 2
try leval = 3
_except_handler3 is at address: 004014C0

Frame: 0012FEFC  Handler: 004014C0  Prev: 0012FF70  Scopetable: 004210B8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401203  __except: 00401206
    scopetable[1] PrevTryLevel: FFFFFFFF  filter: 004012A4  __except: 004012A7
    scopetable[2] PrevTryLevel: 00000001  filter: 0040128E  __except: 00401291
    scopetable[3] PrevTryLevel: 00000002  filter: 00401278  __except: 0040127B

Frame: 0012FF70  Handler: 004014C0  Prev: 0012FFB0  Scopetable: 00420150
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 0040135F  __except: 00401365
    scopetable[1] PrevTryLevel: FFFFFFFF  filter: 004013A2  __except: 004013A8

Frame: 0012FFB0  Handler: 004014C0  Prev: 0012FFE0  Scopetable: 00420278
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 00401788  __except: 004017A3

Frame: 0012FFE0  Handler: 7C82B798  Prev: FFFFFFFF  Scopetable: 7C8123D8
    scopetable[0] PrevTryLevel: FFFFFFFF  filter: 7C8571C8  __except: 7C8571DE

有了实践,这部分比较好懂了。明白了vc如何维护try block 之后,想要更清楚一点,只能从汇编的角度来看了。

EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable pointer
EBP-0C handler function address
EBP-10 previous EXCEPTION_REGISTRATION
EBP-14 LPEXCEPTION_POINTERS
EBP-18 Standard ESP in frame

这是try except block生成时的堆栈。[ebp –10],这里保存了vc 的EXCEPTION_REGISTRATION结构体,就和之前一样,对windows来说,他只是知道最基本的EXCEPTION_REGISTRATION,也就是只是关注prev 和handler,而其他的则是vc 编译器为了生成高效代码为我们加上去的。对windows当然是透明的。从一开始的例子也可以看出,我们只是使用最基本的EXCEPTION_REGISTRATION,依然能够执行SEH。

同样,EBP-14 GetExceptionPointers, EBP-18 Standard ESP in frame也是vc帮我们加入的。[EBP-14 ]这个就是函数当调用GetExceptionInformation会返回[EBP-14], 所以,这个函数其实是一个vc相关的函数。同样的还有GetExceptioncode这个地方还有一点不同的是,vc通过on the flay的方式处理这个数据,也就是说,当异常真的发生的时候,这个数据才会添入数据(这个真是一个废话,没有发生异常,那里来的异常信息?)EBP-18 Standard ESP in frame就不用说了,想要非局部跳转,光搞定ebp是不行的,没有esp的修正,并不能将控制流转到那里。

为了正确理解整个过程,我们需要理解except_handler 的代码,可惜,Matt Pietrek的有一些细节问题,可能会给我们这样的初学者疑惑,所以可以先看下http://bbs.pediy.com/showthread.php?t=53778,也是一位大牛的文章中,有vc6的except_handler code。当然,他多了一个ValidateEH3RN,这个和SEH的安全机制有关,我们目前先跳过去。__except_handler 的代码去了ValidateEH3RN,比较容易理解,当然,细扣细节的话,可能不同。在下一篇文章中,我们会着重关注这些细节。

知道了这么多后,我们在看看我们现在可以解决什么样的问题了。Jeffrey Richter 告诉了我们很多有关于展开的,并且告诉了我们很多可能导致额外负担的代码,那么下面我们就看看,为什么会有额外代码。

DWORD Funcenstein1()

{
    DWORD dwTemp;

    //1. Do any processing here.

        __try
    {
        //2. Request permission to access
        //    protected data, and then use it.
        WaitForSingleObject(g_hSem, INFINITE);
        g_dwProtectedData = 5;
        dwTemp = g_dwProtectedData;

        // Return the new value.
        return(dwTemp);


    }
    __finally
    {
        //3. Allow others to use protected data.
        ReleaseSemaphore(g_hSem, 1, NULL);
    }

    //4. Continue processing.
    return(dwTemp);
}


.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 push    0FFFFFFFFh
.text:00401005                 push    offset stru_4021F8
.text:0040100A                 push    offset __except_handler3
.text:0040100F                 mov     eax, large fs:0
.text:00401015                 push    eax
.text:00401016                 mov     large fs:0, esp
.text:0040101D                 sub     esp, 0Ch
.text:00401020                 push    ebx
.text:00401021                 push    esi
.text:00401022                 push    edi
.text:00401023                 mov     [ebp+var_4], 0
.text:0040102A                 push    0FFFFFFFFh      ; dwMilliseconds
.text:0040102C                 mov     eax, ?g_hSem@@3PAXA ; void * g_hSem
.text:00401031                 push    eax             ; hHandle
.text:00401032                 call    ds:__imp__WaitForSingleObject@8 ; WaitForSingleObject(x,x)
.text:00401038                 mov     esi, 5
.text:0040103D                 mov     ?g_dwProtectedData@@3KA, esi ; ulong g_dwProtectedData
.text:00401043                 mov     [ebp+dwTemp], esi
.text:00401046                 push    0FFFFFFFFh
.text:00401048                 lea     ecx, [ebp+var_10]
.text:0040104B                 push    ecx
.text:0040104C                 call    __local_unwind2 ;这里应该就是Jeffrey Richter 告诉我们的局部展开。
.text:00401051                 add     esp, 8
.text:00401054                 mov     eax, esi
.text:00401056                 mov     ecx, [ebp+var_10]
.text:00401059                 mov     large fs:0, ecx
.text:00401060                 pop     edi
.text:00401061                 pop     esi
.text:00401062                 pop     ebx
.text:00401063                 mov     esp, ebp
.text:00401065                 pop     ebp
.text:00401066                 retn

那local unwind到底做了什么呢?当然是将本EXCEPTION_REGISTRATION内嵌套的那些try block遍历,并展开了。这里贴出local unwind伪代码。这个和我们想象的一样。当然,我这里掩去了一个很重要很重要的部分,是有关于异常嵌套的问题。这个问题会在下一篇中在描述。

void _local_unwind2(EXCEPTION_REGISTRATION*pEh3Exce, int targetLevel)
{
    scopetable_entry *scopetable = peh3Exce->scopetable;
    int trylevel = peh3Exce->trylevel;

    while (trylevel != -1)
    {
        if (targetLevel == -1 || trylevel > targetLevel)
            break;

        if (scopetable[trylevel]->lpfnFilter == NULL)//__finally block
        {
            eax = scopetable[trylevel]->lpfnHandler;
            _NLG_Notify(101);
            eax = scopetable[trylevel]->lpfnHandler;
            __NLG_Call();// call eax
        }
        trylevel = scopetable[targetLevel].previousTryLevel;
        peh3Exce->trylevel = trylevel;
    }
    return;
}

那当我们把return 换成leave时,又是什么样子呢?leave我们并没有看到local unwind,我们需要明白return 和leave的区别。从return发生local unwind,我们可以看出多少端倪,local unwind 的作用在于遍历本地的except frame,那么return和leave的区别就在于,leave不会跳出多个try block 而 return 是有可能的。 所以return 必须要产生额外的负担去执行local unwind,leave则相当于,goto到try block 的结束并正常跳出try block。所以,如果我们只是想跳出本次try要注意不要直接return。

写给自己。

这一篇其实没有写完,虽然历时1个多月,最近实在是太忙了。这篇文章有2点遗憾。

1、最后应该写上global unwind,但是的确是不想去重复大牛们的内容了,global unwind 其实是系统RtlUnwind的封装,上边的链接中有讲这个的,也很详细。只是由于时间悠久和我们现在的编译器和操作系统距离很远了。如果对这些感兴趣,可以看看wince的代码,http://www.2beanet.com/wince/src/COREOS/NK/KERNEL/EXDSPTCH.C.html,

http://www.2beanet.com/wince/src/COREOS/NK/KERNEL/X86/MDX86.C.html。这个和我们的xp2比较像。

2、本来想尽可能的在这一篇中没有或是少有汇编,但是这个的确对我来说,是一个比较复杂的问题,而且越到最后,其实汇编也是不可避免的,因为真实的代码也很有可能就是汇编写的,我们实在是没有必要去把他翻译成c。

这篇文章里面的问题还是很多的,也很有可能会给第一次接触这些的同学一些误解,下一篇将更深入的理解SEH机制,将尽可能的减少这些误解(也包括自己理解错误),内容包括global unwind,异常嵌套和一些很基础很基础的SEH安全机制的总结。

Comments