?? 結(jié)構(gòu)化異常處理.htm
字號:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>混沌星辰游戲開發(fā)基地</title>
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<table width="777" height="255" border="0" align="center" cellpadding="0" cellspacing="0">
<tr>
<td bgcolor="#CCCCCC"><img src="image/001.gif" width="1" height="1"></td>
<td width="775" align="center" valign="top"> <img src="image/001.gif" width="1" height="1"> <font color="#666666"> </font>
<table width="90%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td height="40" align="center" class="bt">結(jié)構(gòu)化異常處理(seh) </td>
</tr>
<tr>
<td height="20" align="center" class="zw">作者:tiamo 來源:金點(diǎn)時空 </td>
</tr>
<tr>
<td height="20" align="center" class="zw"> </td>
</tr>
<tr>
<td class="zw"> 畢業(yè)的事情終于要搞定了,幾個月前就答應(yīng)要寫這么一個文章,現(xiàn)在補(bǔ)上.
<br> 結(jié)構(gòu)化異常處理是一種操作系統(tǒng)提供的機(jī)制,用來優(yōu)化程序的結(jié)構(gòu),提供更加健壯的程序執(zhí)行環(huán)境.試想想你寫程序不用考慮哪里有個內(nèi)存訪問錯誤,哪里有個空指針等等一類的錯誤,一直按照程序的邏輯結(jié)構(gòu)向下寫,而不用去檢查函數(shù)是否成功,這會是多么愉悅的事情(這個乃是seh的宣傳詞,不代表我的觀點(diǎn),這里完全是無責(zé)任應(yīng)景之語).
<br> 結(jié)構(gòu)化異常處理---seh,是一個操作系統(tǒng)級的概念,操作系統(tǒng)為每個線程(windows平臺線程是系統(tǒng)調(diào)度的基本單元)維護(hù)一個異常處理鏈表,當(dāng)有異常發(fā)生的時候,控制權(quán)轉(zhuǎn)移到操作系統(tǒng)手上,操作系統(tǒng)按照一定的方式遍歷這個鏈表,尋找合適的處理函數(shù),執(zhí)行處理工作,并且進(jìn)行堆棧的unwind.
<br> 在user mode的線程運(yùn)行的時候,操作系統(tǒng)讓fs寄存器指向線程的環(huán)境塊(teb),這個teb是一個user mode可訪問的數(shù)據(jù)結(jié)構(gòu),在他的開頭嵌入一個叫NT_TIB結(jié)構(gòu)的tib(線程信息塊),這個tib里面保存著seh要用到的鏈表.
<br> struct _TEB
<br> {
<br> NT_TIB NtTib;
<br> ......
<br> };
<br>
<br> struct NT_TIB
<br> {
<br> EXCEPTION_REGISTRATION_RECORD *ExceptionList;
<br> .....
<br> };
<br>
<br> struct EXCEPTION_REGISTRATION_RECORD
<br> {
<br> EXCEPTION_REGISTRATION_RECORD *Next;
<br> enum _EXCEPTION_DISPOSITION (*Handler)( _EXCEPTION_RECORD *ExceptionRecord,void * EstablisherFrame,_CONTEXT *ContextRecord,void * DispatcherContext);
<br> };
<br>
<br> 在線程運(yùn)行的時候fs段就指向的是TEB結(jié)構(gòu).這個能在下面的匯編代碼里面看到.
<br> 先具體的說說究竟異常發(fā)生的時候操作系統(tǒng)都作了什么吧.
<br> 首先要明白什么是異常,顧名思意,異常就是不尋常的地方(-.-b),cpu在遇到異常的時候,會引發(fā)一個中斷,操作系統(tǒng)會獲取到控制權(quán)(具體的情況,我就不能在這里詳細(xì)的描述了),在經(jīng)過必要的保存現(xiàn)成等一系列動作以后,操作系統(tǒng)通過fs索引到TEB,也就是TIB,然后訪問到ExceptionList,調(diào)用他里面的handler函數(shù)指針指向的函數(shù),如果函數(shù)返回了,就檢查函數(shù)的返回值,如果返回值表示他不能處理這個異常,那么就通過Next指針?biāo)饕较乱粋€record,重復(fù),到了鏈表的盡頭了,還是沒有人能處理,就自動的kill掉這個線程.
<br><div style='float:center' class='Affix'><center><img border=0 src='exceptionfig02.gif'></center></div>
<br> 那那個handler是從哪里來的呢?是應(yīng)用程序在執(zhí)行的時候給安裝的,也許你已經(jīng)知道了,那個handler一般都指向了一個叫_except_handler3 的函數(shù),從上面已經(jīng)看出來了,這個函數(shù)是整個seh的關(guān)鍵,下面會詳細(xì)的介紹這個函數(shù).
<br>
<br> 在c語言里面,seh的語法是__try....__except....__finally這樣構(gòu)成的(具體的語法,這里也不詳細(xì)說了),大家都知道,c語言是會轉(zhuǎn)編譯成機(jī)器語言,然后由cpu指向的,那這樣的一個__try結(jié)構(gòu)都會被轉(zhuǎn)換成什么樣子的機(jī)器語言呢?和他對等的匯編語言是什么樣子的呢?因?yàn)閟eh涉及到太多的底層,特別是內(nèi)存布局是非常重要的,所以這里必須要講講這個轉(zhuǎn)換的過程.
<br>
<br> 編譯器遇到了一個__try結(jié)構(gòu),他就知道應(yīng)該要進(jìn)行seh代碼生成了,也就是要完成上面的那個EXCEPTION_REGISTRATION_RECORD的鏈接,
<br> push _except_handler3 ;這個record構(gòu)造在棧上面
<br> mov eax,fs:[0] ;原來的record
<br> push eax
<br> mov fs:[0],esp
<br>
<br> 這個代碼執(zhí)行完了,堆棧是什么樣子的呢?(低地址在上,高地址在下)
<br>
<br> |原來的record指針| fs:[0]指向這里
<br> |現(xiàn)在的hanlder|
<br>
<br> 正好構(gòu)成一個record結(jié)構(gòu),也正好和原來的list連接到了一起.正好滿足操作系統(tǒng)的要求.
<br>
<br> 看明白編譯器怎么安排record以后,我們就要來看真正的handler了,相對的講,每個handler都要作不同的事情,如果為每個try都生成一份處理的handler的話,會非常的麻煩,所以vc在實(shí)現(xiàn)的時候,讓handler指向同一個函數(shù),但是這樣一來,handler本身的功能實(shí)現(xiàn)就復(fù)雜了,因?yàn)樗仨氁獏^(qū)分開究竟當(dāng)前的異常是屬于哪個try的,是屬于哪個函數(shù)的,這就必須要建立適當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu)來讓handler獲取到這份信息,才能進(jìn)行正確的處理.
<br>
<br> vc為每個函數(shù)維護(hù)一個叫scopetable的數(shù)據(jù)結(jié)構(gòu),他記載著函數(shù)里面使用的try的情況.
<br>
<br> typedef struct _SCOPETABLE
<br> {
<br> DWORD previousTryLevel;// 上一個try鏈表指針
<br> DWORD lpfnFilter;// __except后面的小括號里面的代碼地址
<br> DWORD lpfnHandler;//__except下面的大括號里面的代碼地址
<br> } SCOPETABLE, *PSCOPETABLE;
<br>
<br> vc在生成代碼的時候,為每個函數(shù)都生成了一份scopetable數(shù)組,在建立seh record的時候把這個table的指針也放入到堆棧中,同時把當(dāng)前的trylevel也放如到了堆棧里面,這樣__except_handler3就能訪問到這些數(shù)據(jù),就能正確的處理異常.
<br>
<br> 首先解釋下什么是trylevel,trylevel是一個標(biāo)識,他標(biāo)記了當(dāng)前代碼執(zhí)行的位置,他實(shí)際上指示了當(dāng)前位于哪個try里面.
<br> int i = 0;// trylevel = -1
<br> __try
<br> {
<br> i = 1;//執(zhí)行這個代碼之前,讓trylevel = 0
<br> __try
<br> {
<br> i = 2;//執(zhí)行這個代碼之前,讓trylevel = 1
<br> }
<br> __except(EXCEPTION_EXECUTE_HANDLER)
<br> {
<br> i = 3;
<br> }
<br> __try
<br> {
<br> i = 4;//執(zhí)行這個代碼之前,讓trylevel = 2
<br> }
<br> __except(EXCEPTION_EXECUTE_HANDLER)
<br> {
<br> i = 5;
<br> }
<br> }
<br> __except(EXCEPTION_EXECUTE_HANDLER)
<br> {
<br> i = 6;
<br> }
<br> 請無視i這個變量,它完全是為了代碼里面有內(nèi)容而存在.
<br>
<br> trylevel這個就是用來標(biāo)記當(dāng)前代碼位于哪個try里面,這個值會作為一個下標(biāo)索引到scopetable里面,scopetable里面就記錄了當(dāng)前try對應(yīng)的__except表達(dá)式,以及except的處理代碼的地址.scopetable里面還有一個prevtrylevel成員,它把try block鏈接起來了,用于向上搜索處理句柄用,比如上面的代碼,如果i=2的try里面發(fā)生了異常,首先查看的是它對應(yīng)的__except,這個能從trylevel索引到scopetable得到,如果沒有處理,就應(yīng)該查看上一個try對應(yīng)的except,也就是i=6的那個,但是怎么知道這個try所在的scopetable呢(因?yàn)樘幚砗瘮?shù)和過濾函數(shù)地址都記錄在table里面),這個就是prevtrylevel的用處了,剛剛的那個table里面的prevtrylevel = 0,這樣就索引到了第一個try的scopetable,正是我們要找的.你馬上就會想到,i=4對應(yīng)的scopetable里面的prevtrylevel也是等于0的,yes,you are right.只要你明白了這個部分的道理,剩下的就容易多了.
<br>
<br> 接下來看看真正的匯編代碼是怎么生成的.在函數(shù)代碼的開頭,一般是這樣的
<br> push ebp
<br> mov ebp,esp
<br> push 0ffffffffh ; 這里就是trylevel了
<br> push xxxx ;這個就是scopetable數(shù)組的指針了
<br> push __except_handler3
<br> push fs:[0]
<br> mov fs:[0],esp
<br> sub ebp,20h ;這里不一定是這個數(shù)字,它跟函數(shù)使用的局部變量有關(guān)系
<br>
<br> // 以后碰到try語句的話,就
<br> mov [ebp-4],1;也許是2,也許是3,你應(yīng)該明白這里的值是干什么用的了吧
<br>
<br> 可以看到,除了handler以外還設(shè)置了trylevel和scopetable的指針,因?yàn)檫@個要在handler里面使用.你也許要奇怪了,handler里面怎么獲取到trylevel和scopetable的指針呢?這個得看看內(nèi)存布局了.
<br>
<br> [ebp-0] = prev ebp
<br> [ebp-4] = trylevel
<br> [ebp-8] = scopetable pointer
<br> [ebp-0c] = handler
<br> [ebp-10] = prev registration record
<br>
<br> 啊...如果我們有record的指針的話,向前訪問就能訪問到trylevel他們了呀,yes,record的指針會作為一個參數(shù)傳遞給你的,這個確實(shí)就是訪問trylevel等等變量的方式.
<br>
<br> 在說最后一個事情,然后就進(jìn)入handler函數(shù)本體,你應(yīng)該知道GetExceptionInformation()跟GetExceptionCode()函數(shù)吧,你也許很奇怪msdn里面提到說他們只能使用在某些地方,為什么呢?因?yàn)樗麄儗?shí)現(xiàn)代碼非常的奇怪
<br>
<br> GetExceptionInformation的實(shí)現(xiàn)代碼
<br> mov eax,[ebp-14]
<br> ret
<br> 你應(yīng)該知道eax是保存函數(shù)的返回值的,也就是說這個函數(shù)只是返回了[ebp-14]的值,而且它并沒有設(shè)置ebp的值(ebp是一個函數(shù)的frame pointer,你也應(yīng)該知道,ebp-xx多少情況下是表示了一個函數(shù)的局部變量),也就是說它返回的是調(diào)用者的某個局部變量的值.呵呵,這里其實(shí)跟trylevel差不多的.vc在建立代碼的時候保留了這樣一個空間,而handler在執(zhí)行的時候動態(tài)的設(shè)置了這個值,指向了合適的地址.
<br>
<br> ok,進(jìn)入handler本體吧,先看它的幾個參數(shù),第一個不用說了,操作系統(tǒng)會幫你填充這個值,并且你能用GetExceptionInformation獲取到這些信息,第二個是個void*參數(shù),實(shí)際上,操作系統(tǒng)把當(dāng)前的registeration record地址傳遞給了你,這個是一個很關(guān)鍵的指針,第三個也不用多說,它是一個跟體系結(jié)構(gòu)有關(guān)系的context.最后一個參數(shù)有些時候其實(shí)也指向了scopetable,不過這個參數(shù)并沒有使用到.
<br>
<br> 下面給出handler的偽代碼,在這之前,我們先看看handler都要作些什么.
<br>
<br> handler主要的任務(wù)就是要查找合適的__except語句,檢查它的返回值,如果是EXCEPTION_EXECUTE_HANDLER(當(dāng)然還有continue execute)的話就要執(zhí)行except后面的代碼,否則的轉(zhuǎn)到上一個繼續(xù)搜索,至于怎么轉(zhuǎn)到上一個try,上面已經(jīng)說得很清楚了.
<br> handler還要處理一種情況,就是進(jìn)行unwind.操作系統(tǒng)會兩次得調(diào)用你得handler函數(shù),在第一個參數(shù)得某個成員里面告訴你要作的是查找處理還是進(jìn)行unwind.
<br>
<br> // 對比上面的布局想想這個結(jié)構(gòu)的由來
<br> struct _EXCEPTION_REGISTRATION
<br> {
<br> struct _EXCEPTION_REGISTRATION *prev;
<br> void (*handler)(PEXCEPTION_RECORD,PEXCEPTION_REGISTRATION,PCONTEXT,PEXCEPTION_RECORD);
<br> struct scopetable_entry *scopetable;
<br> int trylevel;
<br> int _ebp;
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -