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
#include
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
#include
#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
#include
#include
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块当中得到处理,当然也可以加入自己的修复函数,使得程序得以继续运行下去。运行结果如下所示。