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

SEHwindows操作系统处理程序错误或异常的技术。SEH是一种系统体制,与具体的程序设计语言无关,但是windows下的编译器多使用SEH实现异常处理。

    系统级别的SEH比较好理解,利用fs:[0]处保存的异常处理回到函数链表对异常进行处理。从这里也可以看出异常是和线程相关的。因为fs:[0]指向的位置是TEBTEB所包含的的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保存的的是与硬件相关的寄存器,所以他的定义则根据不同的硬件而不同整体情况如下图所示:


接下来看VCSEH异常处理的封装。由于上面的上述异常处理。

利用大师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的时候,vctrylevel改为0,进入下一个并列的try block则为1….struct scopetable_entry *则,保存了一个数组,previousTryLevel告诉我们这个嵌套try block 的上一层blockindexhandler指向了同一个代码,vc 的运行时库函数 __except_handler ,根据vc版本后面34啊什么的。lpfnFilter我们的except filter代码入口,lpfnHandler则是我们的except block 入口。finally在那里呢?finally 并没有filter的概念,当lpfnFilter == null的时候,编译器会认为我们跑的是finally block,那么lpfnHandler则是我们的finally terminal handler

根据结果可以看出,总共有四个EXCEPTION_REGISTRATION。第一个是最内层的Function1,第二个在函数main当中,注意第一个函数的lpfnFilter0,因为这里是finally。第三个和其最上面的

Frame的处理函数是一样的。因为这一层是编译器假的except_handler。最后一层是系统假的终结异常处理。

另外还有一点要注意的是对SEH的相关操作只能放在exceptfilter_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块当中得到处理,当然也可以加入自己的修复函数,使得程序得以继续运行下去。运行结果如下所示。


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值