標 題: 【原創】Windows系統程式設計之結構化異常處理 作 者: 北極星2003 時 間: 2006-09-20,20:21 鏈 接: http://bbs.pediy.com/showthread.php?t=32222
目錄: 一、 SEH的概念、特性 二、 SEH的基本使用方法 1、 結束異常程式 (1)try塊的自然退出與非自然退出 (2)finally塊的清理功能及對程式結構的影響 (3)關鍵字__leave 2、 例外處理常式 (1)異常處理的基本流程 (2)異常過濾器 (3)全域展開 (4)暫停全域展開 3、 未處理異常(頂層異常處理) 三、 SEH相關資料結構的介紹 1、 EXCEPTION_POINTER結構 2、 EXCEPTION_RECORD結構 3、 EXCEPTOIN_REGISTRATION結構 4、異常處理鏈結構圖 四、 VC++編譯器級SEH的具體實現 1、 VC擴充異常幀 2、 VC異常幀堆棧布局 3、 兩個執行個體程式:顯示異常幀資訊 4、 執行個體分析及特性介紹 5、 VC中的定層異常處理 6、 VC搜尋例外處理常式流程 五、參考資料 -------------------------------------------------------------------------- 前言: 對於這片文章應該是我寫的最認真的一篇了,斷斷續續地寫了將近一個月,從最初的複習性的回顧,收集更多資料,反覆地整理思路,查閱Windows源碼中的相關的源碼,設計文章的整體架構,到每一個部分的詳細設計,包括流程圖的設計。每一個過程都進展的並不順利,由於時間的關係每星期只能有2~3次的大段時間的在電腦面前,所以思路一直在被打斷,整理文章寫的並不流暢。有些思路或許已經不知道遺忘在哪個角落,但我也盡量把相關方面的知識點都講到。本文的重點在於SEH原理方面的介紹,從單純的使用角度來看,比較簡單,但如果適當的使用SEH機制,這在很大程度上與使用者對SEH的理解程度有很大關係,因而對於具體的實現,文中只介紹重點及需要注意的地方,具體使用方法可參見參考文獻[1]。 一、 SEH的概念、特性 SEH,結構化異常處理,是作為一種系統機制引入到作業系統中的,本身與語言無關。在我們自己的程式中使用SEH可以讓我們集中精力開發關鍵功能,而把程式中所可能出現的異常進行統一的處理,使程式顯得更加簡潔且增加可讀性。 當在程式中使用SEH時,就變成編譯器相關的。其所造成的負擔主要由編譯器來承擔,例如編譯器會產生一些表(table)來支援SEH的資料結構,還會提供回呼函數。 二、 SEH的基本使用方法 1、 結束異常程式 一個結束異常程式能確保調用和執行一個代碼塊,對應與具體的實現,結束處理常式的結構如下所示: __try { // 受保護的代碼 } __finally { // 結束處理常式 } (1)try塊的自然退出與非自然退出 try塊可能會因為return,goto,異常等非自然退出,也可能會因為成功執行而自然退出。但不論try塊是如何退出的,finally塊的內容都會被執行。 請看下面兩個程式: 通過使用結束處理常式,可以避免return語句的過早執行。當retrun 試圖退出try塊時,編譯器要保證finally塊中的代碼首先被執行。這事實上就是一個局部展開的過程,當從try塊的過早退出強制控制轉移到finally塊時,都將引起局部展開。 (2)finally塊的清理功能及對程式結構的影響 寫過軟體的朋友一般都有這樣一個影響:在編碼的過程中需要加入需要檢測,檢測功能是否成功執行,若成功的話執行這個,不成功的話需要作一些額外的清理工作,例如釋放記憶體,關閉控制代碼等。如果檢測不是很多的話,倒沒什麼影響;但若又許多檢測,且軟體中的邏輯關係比較複雜時,往往需要化很大精力來實現繁蕪的檢測判斷。結果就會使程式看起來結構比較複雜,大大降低程式的可讀性,而且程式的體積也不斷增大。 事實上可以用SEH 來解決,把一些相關函數的清理代碼都放在finally塊,只需要在其中加一些適當的判斷,不需要回到每個可能失敗的地方添加清理代碼。下面的FunSampleA函數是一個常規的函數,FunSampleB引入了SEH結束處理常式機制: 這兩個函數的功能是一樣的。可以看到在FunSampleA中的清理函數(CloseHandle)到處都是,而在FunSampleB中的清理函數則全部集中在finally塊,如果在閱讀代碼時只需看try塊的內容即可瞭解程式流程。這兩個函數本身都很小,可以細細體會下這兩個函數的區別。 (3)關鍵字__leave 在try塊中使用__leave關鍵字會使程式跳轉到try塊的結尾,從而自然的進入finally塊。 對於上例中的FunSampleB,try塊中的3個return完全可以用__leave來替換。兩者的區別是用return會引起try過早退出系統會進行局部展開而增加系統開銷,若使用__leave就會自然退出try塊,開銷就小的多。 但有一種情況下必須使用__leave而不能使用return,即當finally塊後還需要執行一定的功能,如下所示: 2、例外處理常式 例外處理常式能在程式發生異常時進行相應的處理,對應與具體的實現,例外處理常式的結構如下所示: __try { // 受保護的代碼 } __except ( /*異常過濾器exception filter*/ ) { // 例外處理常式exception handler } (1)異常處理的基本流程(註:此流程圖來源於參考資料[1]) (2)異常過濾器 異常過濾器只有三個可能的值(定義在Windows的Excpt.h中): EXCEPTION_EXECUTE_HANDLER EXCEPTION_CONTINUE_SERCH EXCEPTION_CONTINUE_EXECUTION 下面是兩種基本的使用方法: 方式一:直接使用過濾器的三個傳回值之一 __try { …… } __except ( EXCEPTION_EXECUTE_HANDLER ) { …… } 方式二:自訂過濾器 __try { …… } __except ( MyFilter( GetExceptionCode() ) ) { …… } LONG MyFilter ( DWORD dwExceptionCode ) { if ( dwExceptionCode == EXCEPTION_ACCESS_VIOLATION ) return EXCEPTION_EXECUTE_HANDLER ; else return EXCEPTION_CONTINUE_SEARCH ; } (3)全域展開 首先來看一下全域展開的基本流程(此流程圖來源於參考資料[1]): 接下來看一個全域展開的執行個體(源碼GlobalUnwindSample.cpp):代碼: #include <iostream.h> #include <windows.h> static unsigned int nStep = 1 ; void Function_B () { int x, y = 0 ; __try { x = 5 / y ; // 引發異常 } __finally { cout << "Step " << nStep++ << " : 執行Function_B的finally塊的內容" << endl ; } } void Function_A ( ) { __try { Function_B () ; } __finally { cout << "Step " << nStep++ << " : 執行Function_A的finally塊的內容" << endl ; } } long MyExcepteFilter ( ) { cout << "Step " << nStep++ << " : 執行main的異常過濾器" << endl ; return EXCEPTION_EXECUTE_HANDLER ; } int main () { __try { Function_A () ; } __except ( MyExcepteFilter() ) { cout << "Step " << nStep++ << " : 執行main的except塊的內容" << endl ; } return 0 ; } /*輸出結果: Step 1 : 執行main的異常過濾器 Step 2 : 執行Function_B的finally塊的內容 Step 3 : 執行Function_A的finally塊的內容 Step 4 : 執行main的except塊的內容 */ 這個程式的執行流程如下所示: (4)暫停全域展開 如果程式中出現異常,且已經找到過濾器值為EXCEPTION_EXECUTE_HANDLER所對應的try塊,此時系統會進行全域展開,在正常情況下,系統會執行該try塊以內的所有finally過程,然後再執行該try塊對應的異常處理過程。但如果在某個finally中放入一個return,就可以阻止全域展開。 3、未處理異常(頂層異常處理) 當軟體中出現異常,而在你的程式中沒有相應的例外處理常式,此時就形成了未處理異常。此時系統會彈出異常提示對話方塊,並可以結束進程。 顯示異常對話方塊是這個功能具體是在UnhandledExceptionFilter中實現的,在啟動進程、線程時,系統會安裝一個最頂層的異常處理try-except結構,如下所示: BaseProcessStart用於進程的主線程,而BaseThreadStart用於其他線程。當異常產生時,如果程式中沒有相應的例外處理常式或者全都返回EXCEPTION_CONTINUE_SEARCH時,就會自動調用UnhandleExceptionFilter。 三、 SEH相關資料結構的介紹 1、EXCEPTION_POINTER結構 2、EXCEPTION_RECORD結構 3、EXCEPTOIN_REGISTRATION結構 4、異常處理鏈結構圖 四、VC++編譯器級SEH的具體實現 1、VC擴充異常幀 2、VC異常幀堆棧布局 3、兩個執行個體程式:顯示異常幀資訊(詳見源碼ShowExcptFrame1.cpp,ShowExcptFrame2.cpp) 對上面的結構不熟悉也沒關係,下面通過執行個體來加深理解。代碼: // // 顯示ScopeTable資訊 // void SEHShowScopeTable( PVC_EXCEPTION_REGISTRATION pVCExcRec ) { printf( "Frame: %08X Handler: %08X Prev: %08X Scopetable: %08X\n", \ pVCExcRec, pVCExcRec->handler, pVCExcRec->prev, pVCExcRec->scopetable ); PSCOPETABLE pScopeTableEntry = pVCExcRec->scopetable; for ( int 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 SEHShowExcptFrames( ) { PVC_EXCEPTION_REGISTRATION pVCExcRec; // 取得異常幀鏈首地址,儲存在pVCExcRec __asm mov eax, FS:[0] __asm mov [pVCExcRec], EAX // 遍曆異常幀鏈 while ( (unsigned)pVCExcRec != 0xFFFFFFFF ) { SEHShowScopeTable( pVCExcRec ); pVCExcRec = (PVC_EXCEPTION_REGISTRATION)(pVCExcRec->prev); } } 嵌套異常結構樣本一: 輸出結果: 嵌套異常結構樣本二: 輸出結果: 4、執行個體分析及特性介紹(比較分析,注意結構樣本與結果之間的聯絡) [1] 上面這個兩個結果中各有4個異常幀。 [2] 他們的後兩條異常幀都是一致的,變化的只是前兩個異常幀 [3] 都是兩重函數調用,mainFunction,每一重調用都以為著增加一個異常幀,其中的第一個幀代表最內層異常塊,即Function函數的異常幀(相對於顯示異常幀資訊的函數來說),第二幀代表main函數的異常幀。 [4] scopetable域的項數與該函數中的異常塊數目相關,樣本一中的Function函數中有三個嵌套的異常塊,因而第一幀scopetable中有三項;樣本二中的Function函數中有四個異常塊,因而第二幀scoptable中有四項。 [5] 首先來看樣本一,Function函數中的三個異常塊是嵌套的,所以在scopetable[2]中的PrevTryLevel=1,表示當前異常塊在幀中的索引為2,與它相鄰的外層異常塊的索引為1,這個異常塊的異常過濾器地址為filter:00401204,而異常處理回呼函數的地址為__except: 00401207;因而在樣本一的第一幀scopetable中的三項形成一個鏈表(這也比較好理解:三個異常塊嵌套)。再來看樣本二,還是看第一幀,自己分析下,是否形成了兩個鏈表。到這裡,對於scoptable中的PrevTryLevel的意義應該清楚了。如果對於這部分還不是很理解的話可以通過增加或修改程式中異常塊、或者再增加幾層函數調用,然後觀察分析輸出結果。 [6] 不知道大家有沒有發現EXCEPTION_REGISTRATION結構中有個handler,而在SCOPETABLE中也有個lpfnHandler(即上面例子輸出結果中的__except項),這兩個都是異常處理回呼函數的地址,它們之間到底有什麼聯絡和區別?樣本中前三幀的handler地址都是004014B0,why? 這個也不難回答,scopetable中的lpfnHandler是用於使用者自訂的異常處理函數,而EXCEPTION_REGISTRATION的handler是用於系統預設的例外處理常式,即通常說的頂層異常處理回呼函數。 [7] 分析到這裡,我只分析了4幀中的兩幀,至於後兩幀的作用,這一點將會在後文中講解。 5、VC中的頂層異常處理 是否還記得在介紹第二部分的未處理異常時提到的BaseProcessStart,BaseThreadStart這兩個函數。在啟動主/次線程時,系統會把整個線程置於一個異常塊中,即頂層異常處理體。在上文顯示異常幀的兩個例子中的第四幀正是來源於此。 在預設情況下,VC程式的進入點由執行階段程式庫函數mainCRTStartup和WinMainainCRTStartup來實現(如果對這方面不熟悉的話,請參見參考資料[5]),其中又一次對將要調用的使用者程式入口main,WinMain(預設情況)置於異常處理體中,在上文顯示異常幀的兩個例子中的第三幀來自這裡。 對於這兩個入口函數,CRT中有相應的源碼,大致結構經簡化如下所示: 現在可以來總結下,VC編譯器中預設情況(即我們的程式不顯示的引入SEH)下的異常塊結構樣本: 其中的BaseProcessStart,WinMainCRTStartup這兩個函數並非固定,需要是視具體情況而定,這個只是用做樣本。 到這裡,對於上小節中的遺留下來的關於第三和第四個異常幀的問題已經解決了。 6、VC搜尋例外處理常式流程 在介紹第三部分SEH相關資料結構時,已經介紹了異常處理鏈的結構,但針對VC中的異常處理,異常幀的擴充包括scopetable結構的引入使得支援嵌套的異常結構,因而當出現異常時尋找相應的例外處理常式的過程相對來說變的更加複雜。VC搜尋例外處理常式流程如下所示: 【參考文獻】 [1]. Windows核心編程 Jeffrey Richter著 機械工業出版社 [2]. A Crash Course on the Depths of Win 32® Structured Exception Handling http://www.microsoft.com/msj/0197/Exception/Exception.aspx [3]. Windows 2000 內部揭密 機械工業出版社 [4]. Windows異常處理流程 SoBeIt http://www.nsfocus.net/index.php?act=magazine&do=view&mid=2471 [5]. 在VC中編譯、運行程式的小知識點 http://fmddlmyy.home4u.china.com/text3.html |