现在的位置: 首页 > 综合 > 正文

结构化异常处理SEH

2013年05月27日 ⁄ 综合 ⁄ 共 8578字 ⁄ 字号 评论关闭

毕业的事情终于要搞定了,几个月前就答应要写这么一个文章,现在补上.
结构化异常处理是一种操作系统提供的机制, 用来优化程序的结构,提供更加健壮的程序执行环境.试想想你写程序不用考虑哪里有个内存访问错误,哪里有个空指针等等一类的错误,一直按照程序的逻辑结构 向下写,而不用去检查函数是否成功,这会是多么愉悦的事情(这个乃是seh的宣传词,不代表我的观点,这里完全是无责任应景之语).
结构化异常处理---seh,是一个操作系统级的概念,操作系统为每个线程(windows平台线程是系统调度的基本单元)维护一个异常处理链表,当 有异常发生的时候,控制权转移到操作系统手上,操作系统按照一定的方式遍历这个链表,寻找合适的处理函数,执行处理工作,并且进行堆栈的unwind.
在user mode的线程运行的时候,操作系统让fs寄存器指向线程的环境块(teb),这个teb是一个user mode可访问的数据结构,在他的开头嵌入一个叫NT_TIB结构的tib(线程信息块),这个tib里面保存着seh要用到的链表.
struct _TEB
{
NT_TIB NtTib;
......
};

struct NT_TIB
{
EXCEPTION_REGISTRATION_RECORD *ExceptionList;
.....
};

struct EXCEPTION_REGISTRATION_RECORD
{
EXCEPTION_REGISTRATION_RECORD *Next;
enum _EXCEPTION_DISPOSITION (*Handler)( _EXCEPTION_RECORD *ExceptionRecord,void * EstablisherFrame,_CONTEXT  *ContextRecord,void * DispatcherContext);
};

在线程运行的时候fs段就指向的是TEB结构.这个能在下面的汇编代码里面看到.
先具体的说说究竟异常发生的时候操作系统都作了什么吧.
首先要明白什么是异常,顾名思意,异常就是不寻常的地方(-.-b),cpu在遇到异常的时候,会引发一个中断,操作系统会获取到控制权(具体的情 况,我就不能在这里详细的描述了),在经过必要的保存现成等一系列动作以后,操作系统通过fs索引到TEB,也就是TIB,然后访问到 ExceptionList,调用他里面的handler函数指针指向的函数,如果函数返回了,就检查函数的返回值,如果返回值表示他不能处理这个异常, 那么就通过Next指针索引到下一个record,重复,到了链表的尽头了,还是没有人能处理,就自动的kill掉这个线程.

那那个handler是从哪里来的呢?是应用程序在执行的时候给安装的,也许你已经知道了,那个handler一般都指向了一个叫_except_handler3 的函数,从上面已经看出来了,这个函数是整个seh的关键,下面会详细的介绍这个函数.

在c语言里面,seh的语法是__try....__except....__finally这样构成的(具体的语法,这里也不详细说了),大家都知 道,c语言是会转编译成机器语言,然后由cpu指向的,那这样的一个__try结构都会被转换成什么样子的机器语言呢?和他对等的汇编语言是什么样子的 呢?因为seh涉及到太多的底层,特别是内存布局是非常重要的,所以这里必须要讲讲这个转换的过程.

编译器遇到了一个__try结构,他就知道应该要进行seh代码生成了,也就是要完成上面的那个EXCEPTION_REGISTRATION_RECORD的链接,
push _except_handler3  ;这个record构造在栈上面
mov eax,fs:[0]   ;原来的record
push eax
mov fs:[0],esp

这个代码执行完了,堆栈是什么样子的呢?(低地址在上,高地址在下)

|原来的record指针| fs:[0]指向这里
|现在的hanlder| 

正好构成一个record结构,也正好和原来的list连接到了一起.正好满足操作系统的要求.

看明白编译器怎么安排record以后,我们就要来看真正的handler了,相对的讲,每个handler都要作不同的事情,如果为每个try都生 成一份处理的handler的话,会非常的麻烦,所以vc在实现的时候,让handler指向同一个函数,但是这样一来,handler本身的功能实现就 复杂了,因为他必须要区分开究竟当前的异常是属于哪个try的,是属于哪个函数的,这就必须要建立适当的数据结构来让handler获取到这份信息,才能 进行正确的处理.

vc为每个函数维护一个叫scopetable的数据结构,他记载着函数里面使用的try的情况.

typedef struct _SCOPETABLE
{
DWORD previousTryLevel;// 上一个try链表指针
DWORD lpfnFilter;// __except后面的小括号里面的代码地址
DWORD lpfnHandler;//__except下面的大括号里面的代码地址
} SCOPETABLE, *PSCOPETABLE;

vc在生成代码的时候,为每个函数都生成了一份scopetable数组,在建立seh record的时候把这个table的指针也放入到堆栈中,同时把当前的trylevel也放如到了堆栈里面,这样__except_handler3就 能访问到这些数据,就能正确的处理异常.

首先解释下什么是trylevel,trylevel是一个标识,他标记了当前代码执行的位置,他实际上指示了当前位于哪个try里面.
int i = 0;// trylevel = -1
__try
{
i = 1;//执行这个代码之前,让trylevel = 0
__try
{
i = 2;//执行这个代码之前,让trylevel = 1
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
i = 3;
}
__try
{
i = 4;//执行这个代码之前,让trylevel = 2
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
i = 5;
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
i = 6;
}
请无视i这个变量,它完全是为了代码里面有内容而存在.

trylevel这个就是用来标记当前代码位于哪个try里面,这个值会作为一个下标索引到scopetable里面,scopetable里面就记 录了当前try对应的__except表达式,以及except的处理代码的地址.scopetable里面还有一个prevtrylevel成员,它把 try block链接起来了,用于向上搜索处理句柄用,比如上面的代码,如果i=2的try里面发生了异常,首先查看的是它对应的__except,这个能从 trylevel索引到scopetable得到,如果没有处理,就应该查看上一个try对应的except,也就是i=6的那个,但是怎么知道这个 try所在的scopetable呢(因为处理函数和过滤函数地址都记录在table里面),这个就是prevtrylevel的用处了,刚刚的那个 table里面的prevtrylevel = 0,这样就索引到了第一个try的scopetable,正是我们要找的.你马上就会想到,i=4对应的scopetable里面的 prevtrylevel也是等于0的,yes,you are right.只要你明白了这个部分的道理,剩下的就容易多了.

接下来看看真正的汇编代码是怎么生成的.在函数代码的开头,一般是这样的
push ebp
mov ebp,esp
push 0ffffffffh ; 这里就是trylevel了
push xxxx ;这个就是scopetable数组的指针了
push __except_handler3
push fs:[0]
mov fs:[0],esp
sub ebp,20h ;这里不一定是这个数字,它跟函数使用的局部变量有关系

// 以后碰到try语句的话,就
mov [ebp-4],1;也许是2,也许是3,你应该明白这里的值是干什么用的了吧

可以看到,除了handler以外还设置了trylevel和scopetable的指针,因为这个要在handler里面使用.你也许要奇怪了, handler里面怎么获取到trylevel和scopetable的指针呢?这个得看看内存布局了.

[ebp-0] = prev ebp
[ebp-4] = trylevel
[ebp-8] = scopetable pointer
[ebp-0c] = handler
[ebp-10] = prev registration record

啊...如果我们有record的指针的话,向前访问就能访问到trylevel他们了呀,yes,record的指针会作为一个参数传递给你的,这个确实就是访问trylevel等等变量的方式.

在说最后一个事情,然后就进入handler函数本体,你应该知道GetExceptionInformation()跟 GetExceptionCode()函数吧,你也许很奇怪msdn里面提到说他们只能使用在某些地方,为什么呢?因为他们实现代码非常的奇怪

GetExceptionInformation的实现代码
mov eax,[ebp-14]
ret
你应该知道eax是保存函数的返回值的,也就是说这个函数只是返回了[ebp-14]的值,而且它并没有设置ebp的值(ebp是一个函数的 frame pointer,你也应该知道,ebp-xx多少情况下是表示了一个函数的局部变量),也就是说它返回的是调用者的某个局部变量的值.呵呵,这里其实跟 trylevel差不多的.vc在建立代码的时候保留了这样一个空间,而handler在执行的时候动态的设置了这个值,指向了合适的地址.

ok,进入handler本体吧,先看它的几个参数,第一个不用说了,操作系统会帮你填充这个值,并且你能用 GetExceptionInformation获取到这些信息,第二个是个void*参数,实际上,操作系统把当前的registeration record地址传递给了你,这个是一个很关键的指针,第三个也不用多说,它是一个跟体系结构有关系的context.最后一个参数有些时候其实也指向了 scopetable,不过这个参数并没有使用到.

下面给出handler的伪代码,在这之前,我们先看看handler都要作些什么.

handler主要的任务就是要查找合适的__except语句,检查它的返回值,如果是EXCEPTION_EXECUTE_HANDLER(当然还有continue execute)的话就要执行except后面的代码,否则的转到上一个继续搜索,至于怎么转到上一个try,上面已经说得很清楚了.
handler还要处理一种情况,就是进行unwind.操作系统会两次得调用你得handler函数,在第一个参数得某个成员里面告诉你要作的是查找处理还是进行unwind.

// 对比上面的布局想想这个结构的由来
 struct _EXCEPTION_REGISTRATION
 {
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD,PEXCEPTION_REGISTRATION,PCONTEXT,PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};

明白了handler的任务以后,看它实际的代码吧.
int __except_handler3(_EXCEPTION_RECORD * pExceptionRecord,EXCEPTION_REGISTRATION * pRegistrationFrame,_CONTEXT   
 *pContextRecord,void * pDispatcherContext )
{
LONG filterFuncRet
LONG trylevel
EXCEPTION_POINTERS exceptPtrs
PSCOPETABLE pScopeTable

CLD // Clear the direction flag (make no assumptions!),这个是c语言编译器默认的操作方式

// 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)

// 检查是不是要进行unwind
if ( ! (pExceptionRecord->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
{
// 设置[ebp-14]的值,记得上面说的[ebp-14]放的是什么么,这里的ExceptionRecord是在handler的堆栈里面的
// 所以它的生存期是有限的,handler函数返回了,这个就不存在了,[ebp-14]这个指针也就指向了未知区域了,所以msdn里面限制
// 了GetExceptionXXX函数的调用地点,明白了么?
// 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
// 想想看,-4指向了什么地方?
*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;

// 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/*-1*/ )
{
// 如果是空,就表示这个是一个finally语句,finally是用来作unwind的
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是一个函数的frame pointer,对于一个函数的执行非常的重要,这里既然是要执行filter(except后面小括号
// 里面的语句),就必须要恢复ebp的值,ebp是怎么恢复的呢?,上面的代码里面可以看到是一个mov ebp,esp,这个esp又
// 是什么呢?对比下上面的内存布局,好好体会这句话的含义,看清楚前面有个取地址符.
EBP = &pRegistrationFrame->_ebp

// Call the filter function调用except小括号里面的语句,检查这个返回值
filterFuncRet = scopetable[trylevel].lpfnFilter();

POP EBP // Restore handler frame EBP

if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )
{
if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION
return ExceptionContinueExecution; // 依靠操作系统完成continue execution

// 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
// 进行unwind,操作系统会变量当前registration record以前的handler一一调用他们,然后断开这些record链表
__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

// 操作系统帮我们完成前面的unwind,当前record的unwind要自己来完成
__local_unwind2( pRegistrationFrame, trylevel );

// 这里是setjmp/longjmp支持代码
// 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
// 修改当前的trylevel为prevtrylevel,很显然,从当前的try block出来了自然就到了上一个try block
pRegistrationFrame->trylevel = scopetable->previousTryLevel;

// Call the _except {} block. Never returns.
// goto except语句,这里并不返回,因为编译器并没有在except语句最后生成一个ret代码
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
{
// 进行unwind(由__global_unwind2函数触发)
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;
}
}

这里也不能不提到编译器为你生成代码的样子

__try
{
i = 0;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
i = 1;
}

这里假设i放在 ebp-20的地方,同时省略fs:[0]的设置

__try:
mov [ebp-4],0 ; trylevel = 0
mov [ebp-18h],esp ; 保存esp
mov [ebp-20h],0 ; 执行i = 0
jmp __finish ; 跳出try语句
__except_filter:
mov eax,EXCEPTION_EXECUTE_HANDLER ; 返回
ret
__except_body:
mov esp,[ebp-18h] ; 首先恢复esp值,也就是回复运行栈
mov [ebp-20h],1 ; 执行 i = 1;
__finish:
mov [ebp-4],0ffffffffh ; trylevel = -1

到这里差不多我要讲的就结束了,更为详细的可以参考我多次提到的msj里面的那个文章.
http://www.microsoft.com/msj/0197/exception/exception.aspx
如果你对vc生成的代码更加的感兴趣,你可以使用ida+softice动态静态跟踪看看. 

抱歉!评论已关闭.