結構化異常處理SEH

畢業的事情終於要搞定了,幾個月前就答應要寫這麼一個文章,現在補上.
結構化異常處理是一種操作系統提供的機制, 用來優化程序的結構,提供更加健壯的程序執行環境.試想想你寫程序不用考慮哪裏有個內存訪問錯誤,哪裏有個空指針等等一類的錯誤,一直按照程序的邏輯結構 向下寫,而不用去檢查函數是否成功,這會是多麼愉悅的事情(這個乃是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動態靜態跟蹤看看. 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章