您的位置:首页 > 编程语言 > C语言/C++

SEH——Structured Exception Handling(结构化异常处理)

2014-03-01 10:53 603 查看
SEH是windows操作系统处理程序错误或异常的技术。SEH是一种系统体制,与具体的程序设计语言无关,但是windows下的编译器多使用SEH实现异常处理。
    系统级别的SEH比较好理解,利用fs:[0]处保存的异常处理回到函数链表对异常进行处理。从这里也可以看出异常是和线程相关的。因为fs:[0]指向的位置是TEB而TEB所包含的的TIB的第一个成员是EXCEPTION_REGISTRATION_RECORD的指针。
struct _EXCEPTION_REGISTRATION_RECORD
{
    struct _EXCEPTION_REGISTRATION_RECORD * Prev;
    PEXCEPTION_HANDLER Handler;
};很明显这个结构体就是一个回调函数的链表。当然这个结构体有很多的变体,但是开头的两个成员都以上面的结构体为原型。
系统当中,对异常的处理实际上是搜索整个异常处理链表,如果某一个异常处理回调函数生成自己对这个异常进行处理,那么上诉搜索过程中止,并且将处理异常之前的回调函数进行展开。因为所有的异常处理毁掉函数都是在堆栈当中生成的,所以需要进行清理。但是如果所有的回调函数都不处理异常,那么系统预定义的回调函数会被调用,并且将所有用户回调函数清理掉。然后,询问用户是进行调试,还是直接终止掉(后面这个过程很熟悉)。
接着分析上诉结构体中回到函数的定义:
EXCEPTION_DISPOSITION
__cdecl _except_handler(
     struct _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     );
函数包含四个参数,第一个参数用于记录异常发生时候的信息。其中ExceptionCode成员表示异常的代号,第四个成员表示异常发生的地址。typedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
}  EXCEPTION_RECORD;
函数的第二个参数是是一个指针用于建立SEH框架,第三个参数是一个指针用于指向程序运行的环境上下文——CONTEXT这个结构体在WINNT.h头文件中定义。这个结构代表异常发生时系统的环境。
typedef struct _CONTEXT { 
    DWORD ContextFlags;
    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;
    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;
    DWORD   Edi;
DWORD   Esi;
  DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;
    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
由于context保存的的是与硬件相关的寄存器,所以他的定义则根据不同的硬件而不同。整体情况如下图所示:



接下来看VC对SEH异常处理的封装。由于上面的上述异常处理。
利用大师Matt Pietrek的示例小程序进行分析。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
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" );

    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 main()
{
    _try
    {
        HomeGrownFrame(); 
    }
    _except( EXCEPTION_EXECUTE_HANDLER)
    {
        printf( "Caught the exception in main()\n" );
    }

    return 0;
}

程序的运行结果如下图所示:
 


在对程序运行结果进行解释之前,首先对一个介绍微软对C语言的扩展的关键字__try、__except、__finally。微软扩展这三个关键字对SEH进行包装。首先__try大括号后面的语句是被保护的,__except语句后面的复合语句是异常处理程序。如果没有异常产生,那么程序直接跳转到__except语句后面继续运行。如果产生异常,则__except语句根据小括号内的值决定下一步的操作,__except有三种类型的返回值:
EXCEPTION_CONTINUE_SEARCH:异常没有被认出来。系统将会继续搜寻整个SEH链表,首先搜寻嵌套的__try、__except异常处理,然后搜寻更高层次的节点。
EXCEPTION_CONTINUE_EXECUTION:异常被认出来,并且被处理,继续从异常发生的地方开始运行。
EXCEPTION_EXECUTE_HANDLER:异常被认出并且被处理,。展开SEH链表,并且执行相应的__except语句后面的复合语句。
但是程序出现错误,程序会跳转到__except语句后面开始执行。
同样微软还扩展了另外一个关键字__finally这个关键字。__finally关键字后面的复合语句在__try关键字后面的语句退出时得到执行机会。并且执行完__finally关键字后面的复合语句之后,还要返回到__try关键字后面的复合语句退出的位置继续执行。我们可以利用跳转语句跳出__try关键字后的复合语句,但是不能利用跳转语句跳进__try关键字后面的复合语句。尤其要注意,可能因为汇编优化的原因,而使得结果不正确。比如在__try语句当中有return,使得__finally语句被执行。在__finally语句也有return。这里到底是得到哪一个返回值呢?由于__finally之后还要返回到__try当中,所以执行返回的是__try当中的语句,然而由于返回是利用EAX寄存器传值,使得EAX的值在__finally当中被覆盖。微软推介的离开__try的方式是__leave语句,这样不会使得__finally还需要返回到__try当中继续执行。另外__finally和__except不能同时存在一个__try之后,但是可以嵌套存在。并且在__finally语句当中执行return语句可以阻止__except的展开操作。
下面用几个例子来验证上诉理论。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#pragma hdrstop
#ifndef _MSC_VER
#error Visual C++ Required (Visual C++ specific information is displayed)
#endif
struct EXCEPTION_REGISTRATION
{
    EXCEPTION_REGISTRATION* prev;
    FARPROC                 handler;
};
struct scopetable_entry
{
    DWORD       previousTryLevel;
    FARPROC     lpfnFilter;
    FARPROC     lpfnHandler;
};
struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
    scopetable_entry *  scopetable;
    int                 trylevel;
    int                 _ebp;
};
 
extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *,
PCONTEXT, PEXCEPTION_RECORD);
 
 
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" );
}  
void WalkSEHFrames( void )
{
    VC_EXCEPTION_REGISTRATION * pVCExcRec;

    printf( "_except_handler3 is at address: %08X\n", _except_handler3 );
    printf( "\n" );

    __asm   mov eax, FS:[0]
__asm   mov [pVCExcRec], EAX

while (  0xFFFFFFFF != (unsigned)pVCExcRec )
{
ShowSEHFrame( pVCExcRec );
pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
}       
}
 
void Function1( void )
{
    _try
    {
        _try
        {
            _try
            {
 WalkSEHFrames();    
            }
            _except( EXCEPTION_CONTINUE_SEARCH )
            {
            }
        }
        _except( EXCEPTION_CONTINUE_SEARCH )
        {
        }
    }
    _except( EXCEPTION_CONTINUE_SEARCH )
    {
    }
}
int main()
{
    int i;

    _try
    {
        i = 0x1234;     
    }
    __finally
    {
        i = 0x4321;    
    }

    _try
    {
        Function1();   
    }
 _except( EXCEPTION_EXECUTE_HANDLER )
    {
        printf( "Caught Exception in main\n" );
    }

    return 0;
}



 由于SEH是在堆栈当中生成的,所以每次退出函数的时候,都需要对SEH链表进行清理。所以编译器对SEH的实现进行了优化,每一个函数只生成一个EXCEPTION_REGISTRATION结构,但是这个EXCEPTION_REGISTRATION结构相对于系统的EXCEPTION_REGISTRATION结构进行了扩展。
struct scopetable_entry
{
    DWORD       previousTryLevel;
    FARPROC     lpfnFilter;
    FARPROC     lpfnHandler;
};
 
struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
 scopetable_entry *  scopetable;
    int                 trylevel;
    int                 _ebp;
};
在进入函数的时候,编译器会把trylevel初始化为-1,这个表示目前的代码在当前的EXCEPTION_REGISTRATION 下,不属于try block保护下,遇到第一个try block的时候,vc把trylevel改为0,进入下一个并列的try block则为1….。struct scopetable_entry *则,保存了一个数组,previousTryLevel告诉我们这个嵌套try block 的上一层block的index。handler指向了同一个代码,vc 的运行时库函数 __except_handler ,根据vc版本后面3啊4啊什么的。lpfnFilter是我们的except filter代码入口,lpfnHandler则是我们的except block 入口。finally在那里呢?finally 并没有filter的概念,当lpfnFilter == null的时候,编译器会认为我们跑的是finally block,那么lpfnHandler则是我们的finally 的terminal handler。
根据结果可以看出,总共有四个EXCEPTION_REGISTRATION。第一个是最内层的Function1,第二个在函数main当中,注意第一个函数的lpfnFilter是0,因为这里是finally。第三个和其最上面的
Frame的处理函数是一样的。因为这一层是编译器假的except_handler。最后一层是系统假的终结异常处理。
另外还有一点要注意的是对SEH的相关操作只能放在except的filter_expression当中执行,因为只有在这里才是异常处理当中。当在异常处理外的时候,操作的结果是未知的。

软件异常

#include<stdio.h>

#include<windows.h>

#include <winbase.h>

void SetValue(int* array,int index)

{

_try{

if (index>=2)

{

RaiseException(STATUS_ARRAY_BOUNDS_EXCEEDED,0,0,0);

}

array[index]=index;

printf("%d\n",array[index]);

}

_except(EXCEPTION_EXECUTE_HANDLER)

{

printf("Memory Overflow!\n");

}

}

int main()

{

int array[2];

int i=0;

for (;i<3;i++)

{

SetValue(array,i);

}

return 0;

}

在SetValue函数当中由于数组越界会生成异常,但是利用RaiseException函数模拟抛出异常,在except块当中得到处理,当然也可以加入自己的修复函数,使得程序得以继续运行下去。运行结果如下所示。



 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  win32 c++ 异常