轉自:[已完工][經典文章翻譯]A Crash Course on the Depths of Win32 Structured Exception Handling
原文題目: <<A Crash Course on the Depths of Win32™ Structured Exception Handling>>
原文地址: http://www.microsoft.com/msj/0197/Exception/Exception.aspx
原作者: Matt Pietrek
在Win32的核心, 結構化異常處理(Structured Exception Handling)(SEH) 是操作系統提供的一種服務. 你能找到的所有關於SEH的文檔都會描述某一種編譯器的運行時庫(runtime library)對操作系統實現的某種包裝. 我會層層剝析SEH一直到它的最基本的概念.
這篇文章假設你熟悉Win32,C++
文章示例代碼: Exception.exe (33KB)
Matt Pietrek 是<Windows 95 System Programming Secrets (IDG Books, 1995)>的作者. 他在NuMega Technologies Inc.工作, 可以通過 [email protected]聯繫他.
在所有由Win32操作系統提供的基礎設施中, 可能被最廣泛應用卻沒有文檔說明的就是結構化異常處理了. 可能一想到Win32結構化異常處理, 大家就會想到諸如_try, _finally, 和_except這樣的術語. 你可以在任何合格的講Win32的書中找到關於SEH的還不錯的描述. 甚至Win32SDK也對也對使用_try, _finally, 和_except來進行結構化異常處理有還不錯的概述.
有了這麼多的文檔, 爲什麼我還要說SEH沒有文檔說明呢? 在覈心裏, 結構化異常處理是一種操作系統提供的服務. 你能找到的所有關於SEH的文檔都會描述某一種編譯器的運行時庫(runtime library)對操作系統實現的某種包裝. 關鍵字_try, _finally, 和_except並沒有什麼神祕的. 微軟的操作系統和編譯器團隊定義了這些關鍵字還有這些關鍵字的行爲. 其他的C++編譯器供應商就只是簡單地順從這些關鍵字的語義而已. 當編譯器的SEH層馴服了原始操作系統SEH的瑣碎混亂之處之後, 編譯器就把原始操作系統的關於SEH的細節隱藏起來了.
我收到過很多很多的郵件, 需要實現編譯器層的SEH的人根本找不到關於操作系統基礎設施提供的關於SEH的細節. 在一個合理的世界中, 我將能夠拿出來Visual C++ 或 Borland C++ 的運行庫源碼來分析他們是如何做到的. 可惜的是, 由於某種不知道的原因, 編譯器層次的SEH好像是一個巨大的祕密. 微軟和Borlandboundary不願意拿出來源代碼來爲最底層的SEH提供支持.
在這篇文章裏, 我會剖析異常處理一直到它的最基本的概念. 爲了這麼做, 我會通過生成代碼和運行時庫的支持, 把操作系統提供的東西從編譯器提供的東西中拆分出來. 當我深入到操作系統的關鍵例程的代碼的時候, 我會使用Intel版本的Windows NT 4.0作爲我的基礎. 不過, 我所描述的絕大多數東西也適用於其他處理器.
我會避免真實的C++異常處理, 在C++的異常處理中會使用cache()而不是_except. 在幕後, 真實的C++異常處理跟我在這裏描述的非常相似. 然而真實的C++異常處理會有一些額外的複雜之處, 我不會涉及他們, 因爲他們會混淆我在這片文章中真正想要講到的概念.
在挖掘組成Win32的結構化處理的晦澀的.H 和 .INC文件片段的時候, 最好的信息來源是IBM OS/2的頭文件(尤其是BSEXCPT.H). 如果你在這個行業混過一段時間的話, 那你就不會覺得喫驚了. 這裏描述的SEH機制是微軟還在OS/2上工作的時候所定義的. 基於這個原因, 你會發現在Win32下的SEH跟OS/2異常類似.
SEH in the Buff
============
如果要一次性把SEH的細節都照顧到的話, 那麼任務量有點太大了, 我會從簡單的地方開始, 逐層向上剖析. 如果你從來沒有使用過結構化異常處理, 那你的狀態還算不錯, 不需要什麼知識預備. 如果你以前使用過SEH, 你需要從你的腦子裏把_try, GetExceptionCode, 和 EXCEPTION_EXECUTE_HANDLER這些詞彙清理掉. 假設這些概念對你來說是新的. 深呼吸, 準備好了麼? 很好.
設想一下我告訴你當一個線程出錯的時候, 操作系統會給你一個機會, 讓你得到這個錯誤的通知. 更具體地, 當一個線程出錯的時候, 操作系統會調用一個用戶定義的callback函數. 這個callback函數能夠做它想做的任何事. 比如說, 它可以修復引發錯誤的地方, 或者播放一個搞笑的聲音文件. 不管這個callback函數做什麼, 它最後的動作時返回一個值, 用來告訴系統下一步該做什麼的值(嚴格來說, 不是這樣的, 但是對於現在來說已經足夠接近了).
當你的代碼把事情搞糟的時候讓操作系統來調用你的函數, 那麼這個callback函數應該像什麼樣子呢? 換句話說, 關於這個異常, 你想知道什麼信息呢? 不必過多操心, Win32已經替你想好了. 一個異常callback函數看起來像這樣:
EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext );
這個原型來自於標準的Win32頭文件EXCPT.H, 初次看起來有點嚇人. 如果你慢慢來看的話, 其實並不是那麼難. 對於初學者來說,應該忽略返回值(EXCEPTION_DISPOSITION). 基本上, 你知道的事實是: 這是一個帶有四個參數的叫做_except_handler的函數.
第一個參數是一個指向EXCEPTION_RECORD結構的指針. 這個結構體是在WINNT.H文件中定義的, 如下:
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
這裏的ExceptionCode參數是操作系統賦予這個異常的一個號碼. 你可以在WINNT.H中看到很多exception code的列表, 搜索以"STATUS_"開頭的#define語句就可以了. 比如說, 熟悉的不能再熟悉的STATUS_ACCESS_VIOLATION的代碼是0xC0000005. 一個更詳細更全面的exception code的集合可以在Windows NT DDK中的NTSTATUS.H中找到. EXCEPTION_RECORD結構的第四個元素是exception發生的地址. 其他的EXCEPTION_RECORD中的元素目前可以忽略.
_except_handler函數的第二個參數是一個指向establisher frame結構的指針. 這是結構化異常處理中的一個至關重要的參數, 但是現在你暫時可以忽略它.
_except_handler函數的第三個參數是個指向CONTEXT結構的指針. CONTEXT結構是在WINNT.H中定義的, 它代表着某個線程的寄存器的值. Figure1展示了CONTEXT結構的定義. 當在SEH中使用的時候, CONTEXT結構代表着在異常發生時刻寄存器的值. 意外地是, 在GetThreadContext和SetThreadContext這兩個API中, 這個結構是一樣的.
第四個參數, 也是最後一個參數叫做DispatcherContext. 現在它也可以被忽略.
Figure 1 CONTEXT Structure
typedef struct _CONTEXT { DWORD ContextFlags; DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; FLOATING_SAVE_AREA FloatSave; 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; DWORD EFlags; DWORD Esp; DWORD SegSs; } CONTEXT;
到目前爲止簡單地概括一下, 你有一個callback函數, 在異常發生的時候會被調用. 這個callback函數有四個參數, 其中的三個是指向結構的指針. 在這三個結構之中, 有些field很重要, 其他的卻不是. 關鍵點是_except_handler 函數會收到豐富的信息, 比如發生的是什麼類型的異常, 在哪裏發生的這個異常. 通過這些信息, 異常callback函數可以決定下一步要做些什麼.
看來是時候允許我丟出一個簡單的小程序來展示_except_handler函數了, 但是還有一點東西需要補充. 特別地, 在異常發生的時候, 操作系統是如何知道到哪裏去調用我們的callback函數呢? 答案是另一個叫做EXCEPTION_REGISTRATION的結構體. 在這片文章中你將會看到這個結構體, 別把這一部分跳過了. 唯一一個我能找到的比較正式的對於EXCEPTION_REGISTRATION的定義的地方在EXSUP.INC文件, 它存在於Visual C++運行時庫的源文件中:
_EXCEPTION_REGISTRATION struct
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends
你將會看到在WINNT.H中NT_TIB定義中, 這個結構被引用爲一個_EXCEPTION_REGISTRATION_RECORD. 再往下, 就沒有定義_EXCEPTION_REGISTRATION_RECORD 的地方了, 所以我即將開始的地方是EXSUP.INC裏的彙編語言結構定義. 這只是我早先時候提到的SEH沒有被文檔記錄的幾個部分的例子之一.
在任何情況下, 都讓我們先回過頭來處理一下手頭的問題. 操作系統是如何得知異常發生的時候到哪裏去調用函數呢?
EXCEPTION_REGISTRATION 結構由兩個fields組成, 其中的第一個你現在可以忽略. 第二個field, 就是handler, 包含一個指向_except_ handler 回調函數的指針. 這讓你離答案更近了一步, 但是問題來了, OS到哪裏去找EXCEPTION_REGISTRATION結構呢?
爲了回答這個問題, 回憶一下結構化異常處理在單線程基礎上的工作機制是有幫助的. 每個線程都有自己的exception handler回調函數. 在我1996年的專欄中, 我描述了一個關鍵的Win32結構, 線程信息塊(TEB或TIB). 這個結構體當中的某些field在Windows NT, Windows® 95, Win32s, 和OS/2是一樣的. TIB的第一個DWORD是一個指向線程的EXCEPTION_REGISTRATION的指針. 在Intel的Win32平臺上, FS寄存器永遠指向當前的TIB. 即, 在FS:[0]的位置, 你可以找到一個指向EXCEPTION_REGISTRATION結構的指針.
現在我們已經比較深入了. 當一個exception發生的時候, 系統會查看出錯線程的TIB結構, 取回一個指向一個EXCEPTION_REGISTRATION結構的指針. 在這個結構中, 有一個指向_except_handler回調函數的指針. 操作系統現在知道了足夠的信息來調用_except_handler回調函數, 如Figure 2所示:
Figure 2 _except_handler_function
小的知識點一塊塊的拼起來了之後, 我寫了一個小程序來示範這個操作系統級的結構化異常處理的描述. Figure 3 展現了MYSEH.CPP, 其中僅有兩個函數. Main函數使用三個內聯的ASM塊. 第一塊通過兩個PUSH指令("PUSH handler" 和"PUSH FS:[0]")在棧上構建了EXCEPTION_REGISTRATION結構. PUSH FS:[0]保存了之前的 FS:[0]的值作爲這個結構的一部分, 但是目前來說這還不重要. 重要的是棧上有了一個8-byte大小的EXCEPTION_REGISTRATION 結構. 緊跟着的下一條指令(MOV FS:[0],ESP)使得TIB中的第一個DWORD指向了新的EXCEPTION_REGISTRATION結構.
Figure 3 MYSEH.CPP
//================================================== // MYSEH - Matt Pietrek 1997 // Microsoft Systems Journal, January 1997 // FILE: MYSEH.CPP // To compile: CL MYSEH.CPP //================================================== #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> DWORD scratch; EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { unsigned i; // Indicate that we made it to our exception handler printf( "Hello from an exception handler\n" ); // Change EAX in the context record so that it points to someplace // where we can successfully write ContextRecord->Eax = (DWORD)&scratch; // Tell the OS to restart the faulting instruction return ExceptionContinueExecution; } int main() { 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 } __asm { mov eax,0 // Zero out EAX mov [eax], 1 // Write to EAX to deliberately cause a fault } printf( "After writing!\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 } return 0; }
如果你好奇爲什麼我在棧上創建了一個EXCEPTION_REGISTRATION結構而不是使用一個全局變量, 我有一個很好的理由. 當你使用編譯器的_try/_except 語法的時候, 編譯器也會在棧上創建EXCEPTION_REGISTRATION結構的. 我只是展現給你了編譯器用來處理_try/_except的方式的一個簡化版本.
回到Main函數, 下一個_asm塊的目的是引發一個錯誤, 先將EAX寄存器清0,然後使用它(EAX)的值作爲下一條指令用來寫入的內存地址(MOV [EAX],1).
最後的__asm塊移除了這個簡單的異常處理器: 首先它恢復之前的FS:[0]的內容, 然後它將EXCEPTION_REGISTRATION記錄從棧中彈出(ADD ESP,8).
現在, 假設你正在運行MYSEH.EXE, 那麼你會看見發生的一切. 當指令MOV [EAX],1執行的時候, 它會引發一個非法訪問的異常. 操作系統會查詢TIB中的FS:[0], 找到指向EXCEPTION_REGISTRATION結構的指針. 在這個結構中有一個指針指向在MYSEH.CPP中的_except_handler 函數. 系統然後將四個所需的參數壓棧, 然後調用_except_handler函數.
在_except_handler中, 代碼首先通過一個printf語句說明"嗨, 我搞糟的地方在這裏!". 然後_except_handler修復了引發錯誤的問題. 也就是EAX寄存器指向一個不能寫入的內存地址(地址0). 修復的方法是修改CONTEXT結構體中EAX的值, 讓它指向一個可寫的地址. 在這個簡單的程序裏, 一個DWORD變量(scratch)就是被設計來完成這個目的的. _except_handler函數的最後的動作時返回值ExceptionContinueExecution, ExceptionContinueExecution是在EXCPT.H文件中定義的.
當操作系統發現返回的值是ExceptionContinueExecution 的時候, 它會理解成這意味着你已經修復了問題, 錯誤的語句可以被再次執行. 因爲我的_except_handler函數修改了EAX寄存器的值, 讓它指向了合法的內存地址, MOV EAX, 1指令第二次就成功地執行了, main函數可以正常地繼續了. 你看到了, 不是那麼複雜的, 對不對?
再深入一點 - Moving In a Little Deeper
======================
研究了這個最簡單的情形後, 讓我們回過頭來填補一些當時留下的空隙吧. 雖然異常回調完成得很棒, 它卻不是一個完美的解決方案. 在任何大小的應用程序中, 書寫一個簡單的函數來處理程序中任何地方都可能會出現的異常, 會非常麻煩. 一個更加可行的方式是擁有多重處理異常的路徑, 每一個都針對應用程序的某部分而特別訂製. 難道你不知道麼, 操作系統提供的就是這個功能.
還記得操作系統用來查找異常回調函數地址的EXCEPTION_REGISTRATION 結構麼? 這個結構的第一個參數, 我們稍早時忽略的那個, 被叫做prev. 它實際上是一個指向另一個EXCEPTION_REGISTRATION 結構的指針. 這第二個EXCEPTION_REGISTRATION 結構能夠擁有完全不同的處理函數. 還有, 它的prev域可以指向第三個EXCEPTION_REGISTRATION 結構, 以此類推. 簡單點說, 它們形成了一個EXCEPTION_REGISTRATION 的鏈表. 這個鏈表的頭永遠是被線程信息塊(TIB)中的第一個DWORD(intel平臺機器裏的FS:[0])所指向的.
操作系統是如何處理這個EXCEPTION_REGISTRATION 的鏈表的呢? 當異常發生的時候, 系統會先遍歷該結構的鏈表, 尋找包含願意處理這個異常的回調函數的EXCEPTION_REGISTRATION結構. 在MYSEH.CPP中, 回調函數通過返回值ExceptionContinueExecution來表示同意處理這個異常. 異常回調函數也可以拒絕處理異常. 在這個情況下, 系統會繼續走到鏈表中的下一個EXCEPTION_REGISTRATION結構上, 詢問異常回調函數是否願意處理這個異常. Figure 4展現了這個過程. 一旦操作系統找到了一個能夠處理異常的callback函數, 它就停止遍歷鏈表了.
Figure 4 Finding a Structure to Handle the Exception
我展現了一個異常回調函數的例子, 看看Figure 5裏的MYSEH2.CPP吧. 爲了保持代碼的簡潔, 我使用編譯器層的異常處理玩了個小花樣. main函數只是設立了一個_try/_except塊. 在__try塊中, 有一個對HomeGrownFrame函數的調用. 這個函數與更早的那個MYSEH程序非常類似. 它在棧上創建了一個EXCEPTION_REGISTRATION
結構, 讓FS:[0]指向這個結構.在創建了新的處理函數之後, 這個函數通過向NULL指針寫數據故意地引發了一個錯誤:
*(PDWORD)0 = 0;
Figure 5 MYSEH2.CPP
//================================================== // MYSEH2 - Matt Pietrek 1997 // Microsoft Systems Journal, January 1997 // FILE: MYSEH2.CPP // To compile: CL MYSEH2.CPP //================================================== #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> 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" ); // Punt... We don't want to handle this... Let somebody else handle it 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; }
異常回調函數, 又一次被命名爲_except_ handler, 這一次跟上個版本有很大不同. 代碼首先打印了ExceptionRecord 結構中的異常代碼(exception code)和異常標誌(exception flag), ExceptionRecord 結構的指針被作爲一個參數傳遞給了我們的_except_ handler函數. 打印出exception flag的原因會在晚些時候變的更清楚. 因爲這個_except_ handler函數並沒有打算修復違例的代碼, 該函數返回了ExceptionContinueSearch. 這會引發操作系統繼續搜索鏈表中的下一個EXCEPTION_REGISTRATION 結構. 現在, 相信我的話, 下一個異常回調函數就是爲main函數中的 _try/_except而被設立的了. _except 塊簡單地打印出了信息"Caught the exception in main()". 在這個例子裏, 對異常的處理就跟忽略它的發生一樣的簡單.
這裏需要提及的一個關鍵點是執行控制. 當一個handler拒絕處理一個exception的時候, 它會有效地拒絕去判斷控制將最終在何處被恢復. 接受異常的handler就是那個決定了控制在所有異常處理代碼結束之後最終將在哪個地址上繼續的那個handler. 這裏有一個重要的隱含含義, 它目前還不明顯.
當使用結構化異常處理的時候, 如果一個函數的異常處理函數並沒有處理掉異常的話, 它也許會以一種不正常的方式退出. 比如說, 在MYSEH2中, HomeGrownFrame 中的最小的handler就沒有處理掉異常. 既然鏈表中的某個部分的處理函數處理了異常(main函數), 出錯指令後面的printf就再沒有被執行了. 從某種程度上說, 使用結構化異常處理跟使用運行時的setjmp和longjmp函數是一樣的.
如果你運行MYSEH2, 你會在輸出中發現一些令人喫驚的東西. 看起來對_except_handler 函數的調用有兩次! 根據你瞭解了的知識, 其中的第一次是不難理解的. 但是第二次調用時怎麼回事呢?
Home Grown handler: Exception Code: C0000005 Exception Flags 0 Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING Caught the Exception in main()
這裏有一處明顯的不同: 比較兩行以"Home Grown Handler" 開頭的輸出. 注意, 第一次exception flag是0, 而第二次是2。這把我們帶到議題unwinding上來了. 提前一點, 當異常回調拒絕處理一個異常的時候, 它會再被調用一次. 但是這次回調並不會立即發生. 事實比較複雜, 我需要最後細化異常的secnario一下了.
當一個異常發生的時候, 系統會遍歷EXCEPTION_REGISTRATION結構的鏈表, 直到他找到一個能夠處理異常的handler爲止. 一旦找到了一個handler, 系統會再次遍歷列表, 遍歷會停在這個能夠處理異常的節點上. 在這第二次的遍歷中, 系統會第二次的調用每一個異常處理函數. 關鍵的不同在於, 在第二次調用中, 2這個值會被賦予exception flag. 這個值也就是EH_UNWINDING. (EH_UNWINDING的定義在EXCEPT.INC中, 該文件在Virtual C++運行時庫的源代碼中, 但是跟Win32SDK中的沒啥關係).
那麼EH_UNWINDING是什麼意思呢? 當一個異常回調函數在第二次被執行的時候(flag的值是EH_UNWINDING), 操作系統會給handler function一個機會來執行一些它需要做的清理工作. 那種清理工作(cleanup)呢? 最好的例子是C++類的析構函數. 當一個函數的exception handler拒絕處理一個異常的時候, 典型地, 控制並不會以正常的方式從函數中離開的. 現在, 假設有一個函數, 其中聲明瞭一個C++的類對象作爲一個局部變量. C++標準說, 析構函數是一定會被調用的. 帶着EH_UNWINDING標誌的exception handler的第二次調用就是個供函數來執行諸如調用析構函數和_finally塊的機會.
當一個異常被處理了, 所有前面的exception frame被依次展開了之後, 執行會在任何處理回調函數確定的一個地方繼續下去. 記住, 僅僅設置指令指針到需要的代碼地址是不夠的. 代碼繼續執行的地方還需要棧頂指針和棧框架指針被設置爲合適的值. 所以, 處理異常的handler有責任設置棧頂指針和棧框架指針的值, 設置之後, 棧框架中包含有處理異常的SEH代碼.
Figure 6 Unwinding from an Exception
用更概括的術語, 從一個exception展開的動作在棧上引發了棧的handling frame之下的部分都被移除了. 這幾乎跟那些函數從沒被調用過一樣. 另一個unwind的效果是處理異常的節點之前的所有EXCEPTION_REGISTRATION都被從列表中移除了. 這是合理的, 因爲這些EXCEPTION_REGISTRATION都是建立在棧上的. 在異常被處理了之後, 棧頂和棧框架指針都會比從列表被移出的EXCEPTION_REGISTRATION的地址要高. Figure 6說明了我的觀點.
救命呀! 沒有人處理這個異常! (Help! Nobody Handled It!)
=======================
到目前爲止, 我一直隱含地假設操作系統總是會在EXCEPTION_REGISTRATION結構的鏈表的某處找到一個handler. 那麼如果沒有一個結構願意站出來處理這個異常怎麼辦? 事實上, 這中情況從來就不會發生. 原因是操作系統偷偷地爲每一個線程配置了一個默認的異常處理的handler. 默認的handler永遠是鏈表的最後一個節點, 並且總是會處理掉異常. 它的行爲與一般的異常處理回調函數有某種程度的不同, 我會在稍後展現出來.
讓我們看一下操作系統插入默認的, 最終的異常handler的地方吧. 顯然, 這個動作會在線程執行的非常早期的時候發生, 所謂早期, 是指在任何用戶代碼執行之前. Figure 7 展現了我爲BaseProcessStart方法寫的一些僞代碼, BaseProcessStart是一個Windows NT的KERNEL32.DLL的內部函數. 它帶一個參數, 即線程的入口地址. BaseProcessStart在新進程的context下運行, 並且調用入口地址來開動進程中的首個線程的執行.
Figure 7 BaseProcessStart Pseudocode
BaseProcessStart( PVOID lpfnEntryPoint ) { DWORD retValue DWORD currentESP; DWORD exceptionCode; currentESP = ESP; _try { NtSetInformationThread( GetCurrentThread(), ThreadQuerySetWin32StartAddress, &lpfnEntryPoint, sizeof(lpfnEntryPoint) ); retValue = lpfnEntryPoint(); ExitThread( retValue ); } _except(// filter-expression code exceptionCode = GetExceptionInformation(), UnhandledExceptionFilter( GetExceptionInformation() ) ) { ESP = currentESP; if ( !_BaseRunningInServerProcess ) // Regular process ExitProcess( exceptionCode ); else // Service ExitThread( exceptionCode ); } }
在僞代碼中, 注意對lpfnEntryPoint 的調用是包裝在一個_try 和_except 的構造中的. 這個_try塊就是那個安裝默認的, 最終的exception handler的地方. 所有後續註冊的異常處理handlers都會被插入到鏈表中的這個節點的前面. 如果lpfnEntryPoint 函數返回了, 那麼線程就成功地運行結束了, 沒有引發任何的異常. 如果沒有異常, BaseProcessStart 會調用ExitThread 來結束線程.
另一方面, 如果線程出錯了, 又沒有其他的exception handler處理呢? 在這種情況下, 控制會進入到_except關鍵字後面的括號中. 在BaseProcessStart, 這段代碼調用了UnhandledExceptionFilter 這個API函數, 我稍後會介紹這個函數. 現在, 關鍵點是UnhandledExceptionFilter API包含默認exception handler的重要成分.
如果UnhandledExceptionFilter 返回了EXCEPTION_EXECUTE_HANDLER, 那麼BaseProcessStart 中的_except塊中的代碼會被執行. _except塊內的代碼所作的工作就是通過調用ExitProcess來結束掉當前的進程. 花一秒中的時間在這裏思考一下, 其實這是合理的: 如果一個程序遇到了錯誤, 並且沒有任何人處理這個錯誤的話, 操作系統應該終止這個進程, 這應該算是常識. 只不過你在僞代碼中看到的是這個常識的精確的發生地和發生方式.
對我剛纔描述的要點還有一個最後的補充. 如果出錯的線程是作爲一個服務運行着的, 並且是一個基於線程的服務的話, 那麼_except塊的代碼並不會調用ExitProcess, 取而代之的是調用ExitThread . 你並不需要僅僅因爲一個線程出了點毛病, 就把整個進程幹掉.
那麼, 默認的exception handler中的UnhandledExceptionFilter裏的代碼都做了些什麼呢? 當我在研討會上問起這個問題的時候, 很少有人能猜到在一個沒有被處理的異常發生的時候, 操作系統的默認行爲. 如果我們來一個非常簡單的對於default handler的行爲的demo的話, 事情就簡單多了, 大家也更容易理解. 我簡單地運行一個程序, 故意地引發一個錯誤, 並且指出錯誤的結果(Figure 8).
Figure 8 Unhandled Exception Dialog
在較高的層次上看, UnhandledExceptionFilter 會顯示一個對話框, 告訴你發生了一個錯誤. 在那個時間點上, 你被給予一個機會, 要麼終止進程, 要麼debug出錯了的進程. 在幕後還有更多的事情發生, 我會在本文即將結束的時候描述這些事情.
正如我已經演示了的, 當一個異常發生的時候, 用戶寫的代碼能夠被執行. 一樣地, 在unwind操作的時候, 用戶寫的代碼也能夠被執行. 這用戶寫的代碼可能會有bug, 並引發另一個異常. 基於這個原因, exception的回調函數有另外兩個值可以返回: ExceptionNestedException 和ExceptionCollidedUnwind. 很明顯這很重要, 而且這很明顯是非常高級的話題了, 我並不打算在這裏停下來描述它, 因爲光是理解基本概念就已經夠難了.
編譯器水平的結構化異常處理(Compiler-level SEH )
==================
我已經時不時地引用_try 和_except這兩個關鍵字了, 到現在我寫的東西還都是由操作系統實現的. 然而, 在我的兩個小程序挑逗性地使用着原始的系統結構化異常處理的時候, 編譯器所包裝的這個功能肯定早爲你準備好了. 讓我們看看Virtual C++是如何在操作系統級的SEH基礎架構上構建自己的對結構化異常處理的支持的吧.
在繼續下去之前, 回憶下能夠使用操作系統級的SEH基礎設施來完成完全不同的事情的另一個編譯器是有必要的. 沒有人說編譯器一定要實現由Win32 SDK文檔描述的_try/_except模型. 比如說, 即將發佈的Visual Basic 5.0就在它的運行時代碼中使用了結構化異常處理, 但是數據結構和算法都與我在這裏描述的完全不同.
如果你通讀Win32 SDK文檔關於結構化異常處理的部分, 你會遇到下面的叫做"frame-based"的語法的exception handler:
try { // guarded body of code } except (filter-expression) { // exception-handler block }
簡單地說, 所有在try塊中的代碼都被一個EXCEPTION_REGISTRATION 保護着, 這個EXCEPTION_REGISTRATION 是構建在函數的棧幀上的(stack frame). 在入口處, 這個新的EXCEPTION_REGISTRATION 會被放在exception handler的鏈表的頭部. 在_try塊的結尾, 它的EXCEPTION_REGISTRATION 會被從鏈表的頭部移除. 正如我早些時候提到的, exception handler鏈表的頭是存儲在FS:[0]當中的. 所以, 如果你步入debugger的彙編語言語句的話, 你會看到如下的指令:
MOV DWORD PTR FS:[00000000],ESP
或者
MOV DWORD PTR FS:[00000000],ECX
你可以確定, 這就是在配置和拆除一個_try/_except 塊了.
現在, 你已經知道一個_try 塊跟棧上的EXCEPTION_REGISTRATION 結構的關係, 那在EXCEPTION_ REGISTRATION裏的回調函數又是怎麼回事兒呢? 用Win32的術語來說, 異常回調函數對應着一個過濾表達式(filter-expression)代碼. 清理一下你的記憶, 過濾表達式就是_except關鍵字後面跟着的括號裏的代碼. 就是這段過濾表達式能夠決定緊隨其後的{}裏的代碼是否會執行.
既然你寫了filter-expression代碼, 那麼就由你來決定是否某個特定的exception應該在你代碼的某個特定的位置來處理. 你的filter-experssion代碼既可以知識簡單地打印一句"我處理了這個異常", 也可以在返回系統, 告訴系統下一步該做什麼之前觸發一個極其複雜的函數. 你說了算. 重點是, 你的filter-expression代碼就是我早先描述的異常回調(exception callback)
我剛剛描述的東西儘管簡單的非常合理, 但它確只不過是真實世界的一種樂觀抽象. 事實更加複雜這一點無疑是醜陋的現實. 對於初學者來說, 你的filter-expression代碼並不是直接由操作系統調用的. 其實, 每一個EXCEPTION_REGISTRATION的exception handler域都指向一個相同的函數. 這個函數存在於Visual C++ runtime library裏, 並且被叫做__except_handler3. 實際上是你__except_handler3調用的你的filter-expression code, 晚些時候我會再解釋這一點的.
另一個對與簡單試圖的扭曲之處是: EXCEPTION_REGISTRATION們並不是在每一次進入或離開_try block的時候被構造和拆解的. 取而代之的是, 你可以在一個函數中添加多個_try/_except結構, 但是隻能有一個EXCEPTION_REGISTRATION被創建在棧上. 同理, 你或許有一層_try block內嵌在另一個_try block中, 但是, Visual C++只創建一個EXCEPTION_REGISTRATION.
如果一個單獨的exception handler(比如__except_handler3)足以處理整個exe或dll, 並且如果一個EXCEPTION_REGISTRATION 處理過個_try block的話, 很顯然這裏發生的事情會比眼睛看到的多好多. 這些神奇的事情是通過在你一般看不到的表裏頭的數據來完成的. 然而, 因爲這篇文章的目的是解剖異常處理, 看不到這些數據表也不能阻擋我們的, 讓我們來一起看一下這些數據結構吧.
擴展了的Exception Handling Frame- (The Extended Exception Handling Frame)
====================
Visual C++ SEH的實現並沒有使用原始的EXCEPTION_REGISTRATION. 取而代之的是, 它在這個結構的末尾添加了一些額外的數據域. 這些額外的數據對於允許函數(__except_handler3)處理所有的異常, 還有能夠讓控制路由到合適的filter-expression和_except塊, 這兩點都是至關重要的. Visual C++對於這個結構擴展的格式可以在EXSUP.INC中找到, 該文件存在於Visual C++ runtime library的源代碼中. 在這個文件中, 你可以找到如下的(已註釋的)定義:
;struct _EXCEPTION_REGISTRATION{ ; struct _EXCEPTION_REGISTRATION *prev; ; void (*handler)(PEXCEPTION_RECORD, ; PEXCEPTION_REGISTRATION, ; PCONTEXT, ; PEXCEPTION_RECORD); ; struct scopetable_entry *scopetable; ; int trylevel; ; int _ebp; ; PEXCEPTION_POINTERS xpointers; ;};
你已經見過頭兩個fields了, 一個是prev, 另一個是handler. 它們組成了基本的EXCEPTION_REGISTRATION 結構. 最後的三個field是新加上去的, scopetable, trylevel, 和_ebp. 域scopetable 指向一個元素類型爲scopetable_entries的數組, 而域trylevel就是這個數組的索引值. 隨後的域_edp, 是在EXCEPTION_REGISTRATION 創建之前的棧框架指針(EBP)的值.
域_ebp稱爲擴展的EXCEPTION_REGISTRATION結構的一部分並不是巧合. 它通過PUSH EBP指令被放置在結構中, push ebp指令是絕大多數函數開始的指令. 它的效果是使得所有其他的EXCEPTION_REGISTRATION的field都變成可以訪問的了, 原因是框架指針的負位移. 比如說, trylevel域在[EBP-04]的位置, 所以scopetable指針的位置就在 [EBP-08], 以此類推.
緊挨着擴展的EXCEPTION_REGISTRATION結構的下面, Visual C++還添加了兩個額外的值. 緊接着的一個DWORD裏, 它保留了一個指向EXCEPTION_POINTERS結構的指針(標準Win32的結構). 這個指針在你調用GetExceptionInformation API的時候會被返回. SDK文檔暗示GetExceptionInformation是一個標準Win32API, 事實上, GetExceptionInformation是一個編譯器固有的函數. 當你調用這個函數的時候, Visual C++生成下面的指令:
MOV EAX,DWORD PTR [EBP-14]
正如GetExceptionInformation 是一個編譯器固有函數一樣, 與之相關聯的GetExceptionCode函數也是一個編譯器固有函數. GetExceptionCode 只是尋找並返回GetExceptionInformation 所返回的結構中的一個數據域(field). 我將會把這個留給讀者做一個練習, 練習弄清楚在Visual C++爲GetExceptionCode產生如下指令的時候, 究竟都發生了什麼:
MOV EAX,DWORD PTR [EBP-14]
MOV EAX,DWORD PTR [EAX]
MOV EAX,DWORD PTR [EAX]
返回到擴展了的EXCEPTION_REGISTRATION 結構, 在結構開始前的8個字節, Visual C++會保留一個DWORD來保存所有已經執行了的開場代碼的最終的棧指針(ESP). 這個DWORD就是函數正常執行時ESP寄存器的一個普通值(除非當參數正在壓棧, 並準備調用下一個函數).
看起來我已經丟給了你一大堆信息, 事實上我的確是這樣做的. 在繼續下去之前, 讓我們稍微暫停並回顧一下Vistal C++爲一個使用結構化異常處理而生成的標準的棧內的情況吧.
EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable pointer
EBP-0C handler function address
EBP-10 previous EXCEPTION_REGISTRATION
EBP-14 GetExceptionPointers
EBP-18 Standard ESP in frame
從操作系統的角度來看, 只有兩個fields組成了原始的EXCEPTION_REGISTRATION結構: 在[EBP-10]位置上的prev指針, 還有在位置[EBP-0Ch]上的handler函數指針. 其他的東西都是具體針對Visual C++的實現的. 瞭解了這些之後, 讓我們來看看體現了編譯器等級的結構化異常處理的Visual C++運行時庫的函數__except_handler3吧.
__except_handler3 和 scopetable
============================
我特別希望能夠給你看看Visual C++運行時庫的源代碼, 並且讓你自己看一看函數__except_handler3的實現, 但是我不能這樣做. 作爲替代, 我會讓你看看我拼湊出來的僞代碼(請看Figure 9)
Figure 9 __except_handler3 Pseudocode
int __except_handler3( struct _EXCEPTION_RECORD * pExceptionRecord, struct EXCEPTION_REGISTRATION * pRegistrationFrame, struct _CONTEXT *pContextRecord, void * pDispatcherContext ) { LONG filterFuncRet LONG trylevel EXCEPTION_POINTERS exceptPtrs PSCOPETABLE pScopeTable CLD // Clear the direction flag (make no assumptions!) // 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) if ( ! (pExceptionRecord->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) ) { // 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 *(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 ) { 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 = &pRegistrationFrame->_ebp // Call the filter function filterFuncRet = scopetable[trylevel].lpfnFilter(); POP EBP // Restore handler frame EBP if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH ) { if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION return ExceptionContinueExecution; // 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 __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 __local_unwind2( pRegistrationFrame, trylevel ); // 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 pRegistrationFrame->trylevel = scopetable->previousTryLevel; // Call the _except {} block. Never returns. 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 { 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; } }
儘管__except_handler3看起來有很多代碼, 但請記住它只不過是我在這篇文章開頭描述過的一個異常回調函數罷了. 它接受跟我自制的在MYSEH.EXE 和MYSEH2.EXE中的異常回調函數完全相同的4個參數. 在最頂層的等級, __except_handler3被一個IF語句拆分爲兩個部分. 這是因爲這個函數會被兩次調到, 一次是普通的調用, 另一次是在unwind展開階段的調用. 這個函數的很大一部分都是爲了非展開(non-unwinding)回調而服務的.
這裏的代碼的開始部分首先在棧上創建了一個EXCEPTION_POINTERS 結構體, 使用兩個__except_handler3的參數來初始化這個結構體. 這個結構體的地址, 也就是我起名爲exceptPtrs的, 被放在了[EBP-14]. 這裏初始化了GetExceptionInformation 和GetExceptionCode 兩個函數使用的指針.
下一步, __except_handler3從EXCEPTION_REGISTRATION frame (位置在[EBP-04])中取回當前的trylevel變量. 這個trylevel變量的作用就是scopetable數組的一個索引, 通過使用這個索引, 允許了單個的EXCEPTION_REGISTRATION被一個函數中的多個多個_try塊所使用, 就跟摺疊的_try塊一樣. 每一個scopetable的條目看起來像這樣:
typedef struct _SCOPETABLE
{
DWORD previousTryLevel;
DWORD lpfnFilter
DWORD lpfnHandler
} SCOPETABLE, *PSCOPETABLE;
在SCOPETABLE 中的第二個和第三個參數比較容易理解. 它們是你的filter-expression和corresponding_except代碼塊的地址. 前一個tryLevel數據域有點小難. 簡單來說, 它是嵌套的try塊. 這裏的重點是, 在一個函數中對每一個_try塊, 都有有一個SCOPETABLE 的入口.
正如我早些時候提到的, 當前的trylevel指定了要被使用的scopeable數組入口. 接下來, 指定filter-expression和_except塊的地址. 現在讓我們想象一個_try塊嵌套在另一個_try塊中的場景吧. 如果裏面的_try塊的filter-expression沒有處理掉異常, 那麼外面的_try塊的filter-expression必須得到消息. 那麼__except_handler3如何得知哪個SCOPETABLE 入口關聯到外面的_try塊呢? 它的索引是通過一個SCOPETABLE 入口裏的previousTryLevel來給出的. 使用這個格式, 你可以創建任意嵌套的_try塊. previousTryLevel 數據域表現的像鏈表中的節點一樣, 該鏈表中存儲的都是函數中可能的exception handler. 鏈表的結尾是通過一個trylevel 值爲0xFFFFFFFF的節點來標識的.
在__except_handler3獲得當前的trylevel的指向相關聯的SCOPETABLE 入口的代碼點, 調用filter- expression的代碼之後, 回到__except_handler3的代碼. 如果filter-expression 返回EXCEPTION_CONTINUE_SEARCH, 那麼__except_handler3會繼續到下一個SCOPETABLE 的入口, 即previousTryLevel 域指定好了的入口. 如果通過遍歷鏈表沒有找到任何的handler, __except_handler3 會返回DISPOSITION_CONTINUE_SEARCH, 這會引發系統繼續執行到下一個EXCEPTION_REGISTRATION 的frame.
如果filter-expression返回EXCEPTION_EXECUTE_HANDLER, 那這意味着異常應該被當前關聯的_except代碼塊來處理. 這意味着任何前面的EXCEPTION_REGISTRATION真必須被從鏈表中移除, 並且_except代碼塊需要被執行. 這些瑣事的第一是被名爲__global_unwind2的函數處理的, 我會稍後解釋它. 在一些其他的清理代碼(我現在暫時忽略)執行過後, 代碼的執行會離開__except_handler3並繼續到_except 塊. 奇怪的是控制從來沒有回到過_except 塊, 即使__except_handler3 函數明確地CALL它也不行.
當前的trylevel是如何設置的呢? 這是由編譯器隱式地處理的, 編譯器會對"擴展了的EXCEPTION_REGISTRATION結構"的trylevel域進行on-the-fly的修改. 如果你查看爲使用SEH的函數生成的彙編代碼, , 你會在函數的不同的點發現在[EBP-04]的修改當前trylevel的代碼.
__except_handler3 是如何對_except代碼進行CALL的動作, 而控制從來不會返回, 這是怎麼做到的呢? 因爲CALL指令push一個返回值到棧上, 你會覺得CALL某函數卻不返回會弄亂掉棧的結構. 如果你查看一個爲_except塊生成的代碼的話, 你會發現它所作的第一件事情就是從EXCEPTION_REGISTRATION結構往下8個字節的地方加載DWORD到ESP寄存器中. 作爲這段開場代碼的一部分, 函數保存了ESP到其他地方, 從而_except塊可以晚些時候獲取它.
The ShowSEHFrames Program
=======================
如果現在你覺得像EXCEPTION_REGISTRATIONs, scopetables, trylevels, filter-expressions, 和unwinding這樣的東西有那麼一點難以接受的話, 我會告訴你這很正常. 剛開始的時候我也一樣暈. 編譯器等級的結構化異常處理的目標就不是讓它能被人逐步地學習清楚. 除非你理解全部的細節, 那麼它的很多組成部分對你是沒有意義的. 當面對一堆理論的時候, 我的自然傾向是寫一些能夠應用我學到的東西的代碼. 如果代碼工作正常, 那麼說明我的理解是正確的.
Figure 10 is the source code for ShowSEHFrames.EXE. It uses _try/_except blocks to set up a list of several Visual C++ SEH frames. Afterwards, it displays information about each frame,
as well as the scopetables that Visual C++ builds for each frame. The program doesn't generate or expect any exceptions. Rather, I included all the _try blocks to force Visual C++ to generate multiple EXCEPTION_ REGISTRATION frames, with multiple scopetable
entries per frame.
Figure 10是ShowSEHFrame.exe的源代碼. 它使用_try/_except 塊來建立一個幾個Visual C++ SEH幀的鏈表. 之後, 它展示了每個幀的信息, 還有Visual C++爲每個幀建立的scopetable. 這段程序並沒有生成任何的異常. 值得注意的是, 我還讓所有的_try塊都強制Visual C++來生成多個EXCEPTION_ REGISTRATION幀, 每個幀還是用多個scopetable.
Figure 10 ShowSEHFrames.CPP
//================================================== // ShowSEHFrames - Matt Pietrek 1997 // Microsoft Systems Journal, February 1997 // FILE: ShowSEHFrames.CPP // To compile: CL ShowSehFrames.CPP //================================================== #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #pragma hdrstop //---------------------------------------------------------------------------- // !!! WARNING !!! This program only works with Visual C++, as the data // structures being shown are specific to Visual C++. //---------------------------------------------------------------------------- #ifndef _MSC_VER #error Visual C++ Required (Visual C++ specific information is displayed) #endif //---------------------------------------------------------------------------- // Structure Definitions //---------------------------------------------------------------------------- // The basic, OS defined exception frame struct EXCEPTION_REGISTRATION { EXCEPTION_REGISTRATION* prev; FARPROC handler; }; // Data structure(s) pointed to by Visual C++ extended exception frame struct scopetable_entry { DWORD previousTryLevel; FARPROC lpfnFilter; FARPROC lpfnHandler; }; // The extended exception frame used by Visual C++ struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION { scopetable_entry * scopetable; int trylevel; int _ebp; }; //---------------------------------------------------------------------------- // Prototypes //---------------------------------------------------------------------------- // __except_handler3 is a Visual C++ RTL function. We want to refer to // it in order to print it's address. However, we need to prototype it since // it doesn't appear in any header file. extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *, PCONTEXT, PEXCEPTION_RECORD); //---------------------------------------------------------------------------- // Code //---------------------------------------------------------------------------- // // Display the information in one exception frame, along with its scopetable // 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" ); } // // Walk the linked list of frames, displaying each in turn // void WalkSEHFrames( void ) { VC_EXCEPTION_REGISTRATION * pVCExcRec; // Print out the location of the __except_handler3 function printf( "_except_handler3 is at address: %08X\n", _except_handler3 ); printf( "\n" ); // Get a pointer to the head of the chain at FS:[0] __asm mov eax, FS:[0] __asm mov [pVCExcRec], EAX // Walk the linked list of frames. 0xFFFFFFFF indicates the end of list while ( 0xFFFFFFFF != (unsigned)pVCExcRec ) { ShowSEHFrame( pVCExcRec ); pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev); } } void Function1( void ) { // Set up 3 nested _try levels (thereby forcing 3 scopetable entries) _try { _try { _try { WalkSEHFrames(); // Now show all the exception frames } _except( EXCEPTION_CONTINUE_SEARCH ) { } } _except( EXCEPTION_CONTINUE_SEARCH ) { } } _except( EXCEPTION_CONTINUE_SEARCH ) { } } int main() { int i; // Use two (non-nested) _try blocks. This causes two scopetable entries // to be generated for the function. _try { i = 0x1234; // Do nothing in particular } _except( EXCEPTION_CONTINUE_SEARCH ) { i = 0x4321; // Do nothing (in reverse) } _try { Function1(); // Call a function that sets up more exception frames } _except( EXCEPTION_EXECUTE_HANDLER ) { // Should never get here, since we aren't expecting an exception printf( "Caught Exception in main\n" ); } return 0; }
ShowSEHFrames 裏的重要函數是WalkSEHFrames 和ShowSEHFrame. WalkSEHFrames 首先打印出__except_handler3的地址, 這麼做的原因稍後就會清楚. 下一步, 函數從FS:[0]中獲得了一個指針, 保存到了exception list的頭節點中. 每個節點的類型都是VC_EXCEPTION_REGISTRATION, 這是我定義的用來描述Visual C++異常處理幀的一個結構. 對於鏈表中的每個節點, WalkSEHFrames 都會傳一個指向節點的指針給ShowSEHFrame 函數.
ShowSEHFrame 函數通過打印exception frame的地址, handler callback的地址, 前一個exception frame的地址, 還有指向scopetable的指針來開始. 接下來, 對每個scopetable入口, 代碼都打印出前一個trylevel, 打印出filter-expression 的地址, 還有_except塊的地址. 我怎麼知道scopetable中有多少條目呢? 我也不知道. 但是我假設當前的VC_EXCEPTION_REGISTRATION 結構中的trylevel的數量少於scopetable條目的總數.
Figure 11展現了運行ShowSEHFrames的結果. 首先, 看看每個用"Frame:"開頭的行吧. 注意每個連續的實例如何展現在棧的更高地址的exception frame的. 接下來, 在頭三個Frame: 行, 注意Handler的值是相同的(004012A8). 看看輸出的開頭的部分, 你會看到這個004012A8 不是別的, 正是Visual C++ runtime library裏的__except_handler3的地址. 這證明了我早些時候的斷言: 所有的exception都被同一個入口點來處理.
Figure 11 Running ShowSEHFrames
你可能在想, 爲什麼有三個exception frame使用__except_handler3 作爲他們的callback, 而ShowSEHFrames 僅僅有兩個函數使用SEH. 答案是第三個frame來自Visual C++ runtime library. 在Visual C++ runtime library 源代碼CRT0.C的代碼中, 調用main或WinMain函數的代碼也包裝在了_try/_except 塊當中了. 針對這個_try塊的filter-expression代碼可以在WINXFLTR.C文件中找到.
回到ShowSEHFrames, 最後一幀的handler的那一行包含了一個不同的地址, 即77F3AB6C. 到處逛逛, 到處試試, 你會發現這個地址在KERNEL32.DLL中. 這個特別的frame是由KERNEL32.DLL在BaseProcessStart 函數(前面我描述過的)裏安裝的.
Unwinding
=======================
在深入挖掘unwinding的實現代碼之前, 讓我們簡單地回顧一下unwinding是什麼意思吧. 之前, 我描述了潛在的exception handler是如何存儲在一個鏈表中的了, 它被一個線程信息塊(Thread Information Block)的第一個DWORD(FS:[0])所指向. 因爲某個特定異常的handler可能不在鏈表的頭節點, 那麼就需要一個秩序來移除列表中的實際處理該異常的handler的前面的所有的exception handler.
正如你在Visual C++ 的__except_handler3 函數中看到的, unwinding是由__global_unwind2 這個RTL函數執行的. 這個函數僅僅是一個對未歸檔的RtlUnwind這個API的非常簡單的包裝.
__global_unwind2(void * pRegistFrame)
{
_RtlUnwind( pRegistFrame,
&__ret_label,
0, 0 );
__ret_label:
}
雖然RtlUnwind 是實現編譯器水平SEH的關鍵API, 但是它並沒有在任何的文檔中出現. 技術上來說, RtlUnwind 這個KERNEL32 函數, 即Windows NT KERNEL32 .DLL會把這個Call 發送到NTDLL.DLL, 而NTDLL.DLL也有一個RtlUnwind 函數. 我能做出一些僞代碼來說明它, 請看Figure 12.
Figure 12 RtlUnwind Pseudocode
void _RtlUnwind( PEXCEPTION_REGISTRATION pRegistrationFrame, PVOID returnAddr, // Not used! (At least on i386) PEXCEPTION_RECORD pExcptRec, DWORD _eax_value ) { DWORD stackUserBase; DWORD stackUserTop; PEXCEPTION_RECORD pExcptRec; EXCEPTION_RECORD exceptRec; CONTEXT context; // Get stack boundaries from FS:[4] and FS:[8] RtlpGetStackLimits( &stackUserBase, &stackUserTop ); if ( 0 == pExcptRec ) // The normal case { pExcptRec = &excptRec; pExcptRec->ExceptionFlags = 0; pExcptRec->ExceptionCode = STATUS_UNWIND; pExcptRec->ExceptionRecord = 0; // Get return address off the stack pExcptRec->ExceptionAddress = RtlpGetReturnAddress(); pExcptRec->ExceptionInformation[0] = 0; } if ( pRegistrationFrame ) pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING; else pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND); context.ContextFlags = (CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS); RtlpCaptureContext( &context ); context.Esp += 0x10; context.Eax = _eax_value; PEXCEPTION_REGISTRATION pExcptRegHead; pExcptRegHead = RtlpGetRegistrationHead(); // Retrieve FS:[0] // Begin traversing the list of EXCEPTION_REGISTRATION while ( -1 != pExcptRegHead ) { EXCEPTION_RECORD excptRec2; if ( pExcptRegHead == pRegistrationFrame ) { _NtContinue( &context, 0 ); } else { // If there's an exception frame, but it's lower on the stack // then the head of the exception list, something's wrong! if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) ) { // Generate an exception to bail out excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; _RtlRaiseException( &exceptRec2 ); } } PVOID pStack = pExcptRegHead + 8; // 8==sizeof(EXCEPTION_REGISTRATION) if ( (stackUserBase <= pExcptRegHead ) // Make sure that && (stackUserTop >= pStack ) // pExcptRegHead is in && (0 == (pExcptRegHead & 3)) ) // range, and a multiple { // of 4 (i.e., sane) DWORD pNewRegistHead; DWORD retValue; retValue = RtlpExecutehandlerForUnwind( pExcptRec, pExcptRegHead, &context, &pNewRegistHead, pExceptRegHead->handler ); if ( retValue != DISPOSITION_CONTINUE_SEARCH ) { if ( retValue != DISPOSITION_COLLIDED_UNWIND ) { excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &excptRec2 ); } else pExcptRegHead = pNewRegistHead; } PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead; pExcptRegHead = pExcptRegHead->prev; RtlpUnlinkHandler( pCurrExcptReg ); } else // The stack looks goofy! Raise an exception to bail out { excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_BAD_STACK; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &excptRec2 ); } } // If we get here, we reached the end of the EXCEPTION_REGISTRATION list. // This shouldn't happen normally. if ( -1 == pRegistrationFrame ) NtContinue( &context, 0 ); else NtRaiseException( pExcptRec, &context, 0 ); } PEXCEPTION_REGISTRATION RtlpGetRegistrationHead( void ) { return FS:[0]; } _RtlpUnlinkHandler( PEXCEPTION_REGISTRATION pRegistrationFrame ) { FS:[0] = pRegistrationFrame->prev; } void _RtlpCaptureContext( CONTEXT * pContext ) { pContext->Eax = 0; pContext->Ecx = 0; pContext->Edx = 0; pContext->Ebx = 0; pContext->Esi = 0; pContext->Edi = 0; pContext->SegCs = CS; pContext->SegDs = DS; pContext->SegEs = ES; pContext->SegFs = FS; pContext->SegGs = GS; pContext->SegSs = SS; pContext->EFlags = flags; // __asm{ PUSHFD / pop [xxxxxxxx] } pContext->Eip = return address of the caller of the caller of this function pContext->Ebp = EBP of the caller of the caller of this function pContext->Esp = Context.Ebp + 8 }
雖然RtlUnwind 看起來挺威風, 但是如果你有條不紊地分開來看, 它還是不難理解的. 這個API通過從FS:[4]和FS:[8]獲得當前線程的棧的棧頭和棧尾來開始. 這些值很重要, 因爲需要用它們來確保所有的exception frame展開後都在棧的範圍之內, 這樣做是理智的.
RtlUnwind 之後在棧上建立了一個啞的EXCEPTION_RECORD, 並設置它的ExceptionCode爲STATUS_UNWIND. 還有, EXCEPTION_UNWINDING 被設置成了EXCEPTION_RECORD結構中的ExceptionFlags 域的值. 一個指向這個結構的指針會在稍後作爲一個參數傳遞給每個exception callback. 之後, 代碼調用_RtlpCaptureContext 函數來創建一個啞的CONTEXT 結構, 這個就結構也成爲了爲了unwind exception callstack的一個參數.
RtlUnwind 的其餘部分遍歷EXCEPTION_REGISTRATION結構的鏈表. 對每一幀, 代碼都調用RtlpExecuteHandlerForUnwind 函數, 我稍後會解釋這個函數. 正是這個函數調用了exception callback, 並且還設置了EXCEPTION_UNWINDING 標誌. 每個callback之後, 關聯的exception frame都會被RtlpUnlinkHandler的調用所移除.
RtlUnwind 會在它到達作爲傳給他的第一個參數的位置是停止unwinding frames. 穿插在代碼中, 我已經描述了確保萬無一失的sanity-checking(理性檢查). 如果某個意外問題突然發生, 那麼RtlUnwind 會生成一個異常來說明出現了什麼問題, 並且這個異常會包含EXCEPTION_NONCONTINUABLE 標誌. 一個進程的這個標誌位被設置了的話, 那麼這個進程是不允許再繼續執行的了, 所以該進程必須被殺掉.
Unhandled Exceptions
====================
在這篇文章的前面, 我推遲了對UnhandledExceptionFilter API的完整描述. 一般情況下, 你不會直接調用這個API的(儘管你能夠這麼做). 多數時候, 它是被KERNEL32的默認異常回調的filter-expression 的代碼所激活的. 我在前面的BaseProcessStart的僞代碼中已經展現了.
Figure 13展現了我寫的UnhandledExceptionFilter的僞代碼. 這個API的開始有點奇怪(至少我這麼認爲). 假設錯誤是一個EXCEPTION_ACCESS_ VIOLATION, 那麼代碼會調用_BasepCheckForReadOnlyResource. 雖然我還沒提供這個函數的僞代碼, 但是我可以總結它一下. 如果異常發生是由於一個EXE或DLL的resource section(.rsrc)被寫入了, 那麼_BasepCurrentTopLevelFilter會修改錯誤頁面的本來的正常的只讀屬性, 從而允許寫入發生. 如果這個特定的場景發生的話, UnhandledExceptionFilter 會返回EXCEPTION_ CONTINUE_EXECUTION, 並且執行會在錯誤的指令處重新執行.
Figure 13 UnHandledExceptionFilter Pseudocode
UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *pExceptionPtrs ) { PEXCEPTION_RECORD pExcptRec; DWORD currentESP; DWORD retValue; DWORD DEBUGPORT; DWORD dwTemp2; DWORD dwUseJustInTimeDebugger; CHAR szDbgCmdFmt[256]; // Template string retrieved from AeDebug key CHAR szDbgCmdLine[256]; // Actual debugger string after filling in STARTUPINFO startupinfo; PROCESS_INFORMATION pi; HARDERR_STRUCT harderr; // ??? BOOL fAeDebugAuto; TIB * pTib; // Thread information block pExcptRec = pExceptionPtrs->ExceptionRecord; if ( (pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) && (pExcptRec->ExceptionInformation[0]) ) { retValue = _BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]); if ( EXCEPTION_CONTINUE_EXECUTION == retValue ) return EXCEPTION_CONTINUE_EXECUTION; } // See if this process is being run under a debugger... retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &debugPort, sizeof(debugPort), 0 ); if ( (retValue >= 0) && debugPort ) // Let debugger have it return EXCEPTION_CONTINUE_SEARCH; // Did the user call SetUnhandledExceptionFilter? If so, call their // installed proc now. if ( _BasepCurrentTopLevelFilter ) { retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs ); if ( EXCEPTION_EXECUTE_HANDLER == retValue ) return EXCEPTION_EXECUTE_HANDLER; if ( EXCEPTION_CONTINUE_EXECUTION == retValue ) return EXCEPTION_CONTINUE_EXECUTION; // Only EXCEPTION_CONTINUE_SEARCH goes on from here } // Has SetErrorMode(SEM_NOGPFAULTERRORBOX) been called? if ( 0 == (GetErrorMode() & SEM_NOGPFAULTERRORBOX) ) { harderr.elem0 = pExcptRec->ExceptionCode; harderr.elem1 = pExcptRec->ExceptionAddress; if ( EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode ) harderr.elem2 = pExcptRec->ExceptionInformation[2]; else harderr.elem2 = pExcptRec->ExceptionInformation[0]; dwTemp2 = 1; fAeDebugAuto = FALSE; harderr.elem3 = pExcptRec->ExceptionInformation[1]; pTib = FS:[18h]; DWORD someVal = pTib->pProcess->0xC; if ( pTib->threadID != someVal ) { __try { char szDbgCmdFmt[256] retValue = _GetProfileStringA( "AeDebug", "Debugger", 0, szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 ); if ( retValue ) dwTemp2 = 2; char szAuto[8] retValue = GetProfileStringA( "AeDebug", "Auto", "0", szAuto, sizeof(szAuto)-1 ); if ( retValue ) if ( 0 == strcmp( szAuto, "1" ) ) if ( 2 == dwTemp2 ) fAeDebugAuto = TRUE; } __except( EXCEPTION_EXECUTE_HANDLER ) { ESP = currentESP; dwTemp2 = 1 fAeDebugAuto = FALSE; } } if ( FALSE == fAeDebugAuto ) { retValue = NtRaiseHardError( STATUS_UNHANDLED_EXCEPTION | 0x10000000, 4, 0, &harderr, _BasepAlreadyHadHardError ? 1 : dwTemp2, &dwUseJustInTimeDebugger ); } else { dwUseJustInTimeDebugger = 3; retValue = 0; } if ( retValue >= 0 && ( dwUseJustInTimeDebugger == 3) && ( !_BasepAlreadyHadHardError ) && ( !_BaseRunningInServerProcess ) ) { _BasepAlreadyHadHardError = 1; SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE }; HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 ); memset( &startupinfo, 0, sizeof(startupinfo) ); sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent); startupinfo.cb = sizeof(startupinfo); startupinfo.lpDesktop = "Winsta0\Default" CsrIdentifyAlertableThread(); // ??? retValue = CreateProcessA( 0, // lpApplicationName szDbgCmdLine, // Command line 0, 0, // process, thread security attrs 1, // bInheritHandles 0, 0, // creation flags, environment 0, // current directory. &statupinfo, // STARTUPINFO &pi ); // PROCESS_INFORMATION if ( retValue && hEvent ) { NtWaitForSingleObject( hEvent, 1, 0 ); return EXCEPTION_CONTINUE_SEARCH; } } if ( _BasepAlreadyHadHardError ) NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode); } return EXCEPTION_EXECUTE_HANDLER; } LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter ); { // _BasepCurrentTopLevelFilter is a KERNEL32.DLL global var LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter; // Set the new value _BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter; return previous; // return the old value }
下一個UnhandledExceptionFilter 的任務是確定進程是否運行在一個Win32 Debugger之下. 如果是的話, 進程的DEBUG_PROCESS 或BUG_ONLY_THIS_PROCESS 標誌在進程創建時就被標好. UnhandledExceptionFilter 使用NtQueryInformationProcess 函數(該函數我在這個月的Under the Hood專欄裏描述過)來判斷進程是否正在被debug. 如果是的話, 那麼這個API就返回EXCEPTION_CONTINUE_SEARCH, 這就會告訴系統的其他部分來喚醒debugger進程, 並且告訴debugger進程debuggee裏發生了一個異常.
接下來, UnhandledExceptionFilter's 會調用user-installed的爲unhandled exception filter, 如果有的話. 正常情況下, user-installed的callback函數不存在的, 但是用戶可以通過SetUnhandledExceptionFilter 這個API來安裝一個. 我也爲這個API提供了僞代碼. 這個API簡單地使用"用戶的新callback函數的地址"來替換一個global variable, 並且返回老callback的值.
有了前面基礎問題的解決, UnhandledExceptionFilter 能夠開始做它的工作了: 使用永久時尚的應用程序錯誤對話框來通知你你的可恥的編程大錯. :) 有兩種方法可以避免這個對話框.
第一, 如果進程調用了SetErrorMode 並且指定了SEM_NOGPFAULTERRORBOX 標誌.
第二, AeDebug註冊表的Auto鍵設爲1. 在這種情況下, UnhandledExceptionFilter 會跳過應用程序錯誤對話框, 並自動開啓在AeDebug 註冊表的Debugger鍵指定的debugger. 如果你熟悉"just in time debugging", 這就是操作系統支持它的地方了. 晚些時候在繼續這個部分.
在多數情況下, 這兩種避免對話框的情況都不爲true, 並且UnhandledExceptionFilter 會調用NTDLL.DLL中的NtRaiseHardError方法. 這個方法會帶來Application Error 對話框. 這個對話框等你點擊OK來結束進程, 或者點擊Cancel來debug它. (或許只是我這樣想吧, 點擊cancel來開啓debugger看起來有點像是退步)
如果你點擊Application Error對話框的OK按鈕, UnhandledExceptionFilter 會返回EXCEPTION_EXECUTE_HANDLER. 調用UnhandledExceptionFilter 的代碼通常會通過結束它自己來響應(正如你在BaseProcessStart 的代碼中看到的). 這帶來了一個有趣的要點. 大多數人假設系統會使用一個unhandled exception來結束進程. 其實更正確的方法是, 系統配置好了一切, 以便於unhandled exception能夠引發進程終結它自己.
UnhandledExceptionFilter 的執行中真正有趣的代碼是如果你在Application Error 對話框中選擇Cancel, 從而爲出錯的進程開啓一個debugger. 代碼首先調用CreateEvent 來製作一個event來通知debugger來attach到出錯的進程上. 這個event的句柄, 還有當前進程的ProcessID, 會被傳送給sprintf函數, 它會在event中格式化之前創建的NtWaitForSingleObject. 這個調用會阻塞進程直到debugger通知event, 告訴它debugger已經成功地attach到了出錯的進程之上. UnhandledExceptionFilter 的代碼中還有些小零碎, 但是我這裏已經介紹了所有重要的要點了.
地獄深處
===============
如果你已經看到了這麼遠的地方, 那麼如果不完成整個的線路對你就太不公平了. 我已經展現了操作系統如何在異常發生時調用用戶定義的函數. 我也展現了那些callback函數的特別之處, 還有編譯器如何使用他們來實現_try和_catch的了. 我曾經展現了當沒有人處理異常的時候, 系統不得不做一些收尾工作. 所剩下的就是展現一開始exception callback起源於何處了. 是的, 讓我們一個猛子扎到系統的腸子裏來看看結構化異常處理的順序吧.
Figure 14展現了一些我爲KiUserExceptionDispatcher 寫的僞代碼, 還有一些相關的函數. KiUserExceptionDispatcher 函數存在於NTDLL.DLL, 它就是異常發生後執行開始的地方. 要百分百準確的話, 我剛纔說的也不算正確. 打比方說, 在Intel架構中, 一個異常引發控制向量轉移到ring 0(內核態)的handler中. Handler是由interrupt descriptor 表中的關聯到某個異常的一行定義的. 我會跳過所有的內核態代碼, 並假裝CPU直接在exception的情況下調用到KiUserExceptionDispatcher 中.
Figure 14 KiUserExceptionDispatcher Pseudocode
KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD retValue; // Note: If the exception is handled, RtlDispatchException() never returns if ( RtlDispatchException( pExceptRec, pContext ) ) retValue = NtContinue( pContext, 0 ); else retValue = NtRaiseException( pExceptRec, pContext, 0 ); EXCEPTION_RECORD excptRec2; excptRec2.ExceptionCode = retValue; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } int RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD stackUserBase; DWORD stackUserTop; PEXCEPTION_REGISTRATION pRegistrationFrame; DWORD hLog; // Get stack boundaries from FS:[4] and FS:[8] RtlpGetStackLimits( &stackUserBase, &stackUserTop ); pRegistrationFrame = RtlpGetRegistrationHead(); while ( -1 != pRegistrationFrame ) { PVOID justPastRegistrationFrame = &pRegistrationFrame + 8; if ( stackUserBase > justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( stackUsertop < justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( pRegistrationFrame & 3 ) // Make sure stack is DWORD aligned { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( someProcessFlag ) { // Doesn't seem to do a whole heck of a lot. hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0, pRegistrationFrame, 0x10 ); } DWORD retValue, dispatcherContext; retValue= RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame, pContext, &dispatcherContext, pRegistrationFrame->handler ); // Doesn't seem to do a whole heck of a lot. if ( someProcessFlag ) RtlpLogLastExceptionDisposition( hLog, retValue ); if ( 0 == pRegistrationFrame ) { pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // Turn off flag } EXCEPTION_RECORD excptRec2; DWORD yetAnotherValue = 0; if ( DISPOSITION_DISMISS == retValue ) { if ( pExcptRec->ExceptionFlags & EH_NONCONTINUABLE ) { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0 RtlRaiseException( &excptRec2 ); } else return DISPOSITION_CONTINUE_SEARCH; } else if ( DISPOSITION_CONTINUE_SEARCH == retValue ) { } else if ( DISPOSITION_NESTED_EXCEPTION == retValue ) { pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND; if ( dispatcherContext > yetAnotherValue ) yetAnotherValue = dispatcherContext; } else // DISPOSITION_COLLIDED_UNWIND { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0 RtlRaiseException( &excptRec2 ); } pRegistrationFrame = pRegistrationFrame->prev; // Go to previous frame } return DISPOSITION_DISMISS; } _RtlpExecuteHandlerForException: // Handles exception (first time through) MOV EDX,XXXXXXXX JMP ExecuteHandler RtlpExecutehandlerForUnwind: // Handles unwind (second time through) MOV EDX,XXXXXXXX int ExecuteHandler( PEXCEPTION_RECORD pExcptRec PEXCEPTION_REGISTRATION pExcptReg CONTEXT * pContext PVOID pDispatcherContext, FARPROC handler ) // Really a ptr to an _except_handler() // Set up an EXCEPTION_REGISTRATION, where EDX points to the // appropriate handler code shown below PUSH EDX PUSH FS:[0] MOV FS:[0],ESP // Invoke the exception callback function EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext ); // Remove the minimal EXCEPTION_REGISTRATION frame MOV ESP,DWORD PTR FS:[00000000] POP DWORD PTR FS:[00000000] return EAX; } Exception handler used for _RtlpExecuteHandlerForException: { // If unwind flag set, return DISPOSITION_CONTINUE_SEARCH, else // assign pDispatcher context and return DISPOSITION_NESTED_EXCEPTION return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARCH : *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_NESTED_EXCEPTION; } Exception handler used for _RtlpExecuteHandlerForUnwind: { // If unwind flag set, return DISPOSITION_CONTINUE_SEARCH, else // assign pDispatcher context and return DISPOSITION_COLLIDED_UNWIND return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARCH : *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_COLLIDED_UNWIND; }
在KiUserExceptionDispatcher 的深處是對RtlDispatchException的調用. 這啓動了對任何註冊了的exception handler的搜索. 如果一個handler處理了異常並且繼續執行, 那麼對RtlDispatchException 的調用就永遠不會返回. 如果RtlDispatchException 返回了, 就有兩種可能的路徑:要麼NtContinue 被調用了, 它使得進程繼續執行.要麼就是另一個異常發生了. 這時候, 異常不是可繼續的, 並且進程必須終止.
繼續說RtlDispatchExceptionCode, 這是你會發現exception frame walking代碼的地方, 整篇文章我都在提. 這個函數抓住一個指向EXCEPTION_REGISTRATIONs 鏈表的指針, 並且遍歷所有的節點, 查找handler. 因爲有棧崩潰的可能性, 這個過程非常偏執, 讓人不爽. 在調用每個EXCEPTION_REGISTRATION指定的handler之前, 代碼會確保EXCEPTION_REGISTRATION 是DWORD方式對齊(DWORD-aligned)的, 對齊發生在線程的棧中, 並且下一個EXCEPTION_REGISTRATION的位置要比前一個EXCEPTION_REGISTRATION處於更高的地址.
RtlDispatchException 並不直接地調用EXCEPTION_REGISTRATION 結構中指定的地址. 替代的是, 它會調用RtlpExecuteHandlerForException 來做掉這髒活兒. 取決於RtlpExecuteHandlerForException內部發生了什麼, RtlDispatchException 要麼繼續遍歷所有的異常frame, 要麼引發另一個異常. 這裏的第二個異常指明瞭在exception callback中有什麼不好的事情發生了, 並且執行不能繼續下去了.
RtlpExecuteHandlerForException 的代碼跟另一個函數關聯得很緊, 即RtlpExecutehandlerForUnwind. 你會回想起我早些時候描述unwinding的時候提到過這個函數. 這兩個"函數"簡單地在把控制傳給ExecuteHandler 函數之前使用不同的值來加載到EDX寄存器中. 換種說法, RtlpExecuteHandlerForException 和RtlpExecutehandlerForUnwind 是ExecuteHandler函數的分開的獨立的前端而已.
ExecuteHandler 是EXCEPTION_REGISTRATION中抽取出來的handler域, 並且這個域會被調用. 像它看起來一樣奇怪, 對exception callback的調用是有一個結構化的exception handler"自我封裝"的. 在它自己內部使用SEH看起來有點可笑, 但是如果你思考它多一點的話會發現這是合理的. 如果一個exception callback引發了另一個異常, 那麼操作系統需要知道這一點. 取決於異常是否發生在開始的callback中還是發生在unwind callback中, ExecuteHandler 要麼返回DISPOSITION_NESTED_ EXCEPTION , 要麼返回DISPOSITION_COLLIDED_UNWIND. 這兩個基本上意思都是"紅色警報! 現在就關閉所有的東西!" 的代碼了.
如果你像我一樣, 現在已經很難直接把所有的這些函數直接跟SEH聯繫在一起了. 同樣的, 你也很難記住誰調用了誰. 爲了幫助我自己, 我搞了一個小圖表在下面的Figure 15裏.
Figure 15 Who Calls Who in SEH
KiUserExceptionDispatcher() RtlDispatchException() RtlpExecuteHandlerForException() ExecuteHandler() // Normally goes to __except_handler3 --------- __except_handler3() scopetable filter-expression() __global_unwind2() RtlUnwind() RtlpExecuteHandlerForUnwind() scopetable __except block()
現在, 看看在到達ExecuteHandler 代碼之前設置EDX是什麼意思吧. 很簡單, 真的. 如果調用user-installed的handler的時候出了什麼問題, 那麼ExecuteHandler 會使用EDX中的任何東西作爲原始的exception handler. 它將EDX寄存器壓到棧上, 讓它作爲一個最小的EXCEPTION_REGISTRATION 的handler數據域. 本質上說, ExecuteHandler 使用原始的structured exception handling, 就跟我在MYSEH 和MYSEH2程序中展示的一樣.
結論
==============
結構化異常處理是Win32的一個美妙的特性. 感謝諸如Visual C++一類的編譯器放在上面的支持的層次, 一般的程序員都可以花相對較少的學習上的投資從SEH中獲益. 然而, 在操作系統層次, 事情就比你在Win32文檔想要你相信的東西複雜得多了.
不幸的是, 目前並沒有什麼系統級別的SEH寫出來公佈給大家, 因爲絕大多數人都認爲這一塊是個極端困難的話題. 系統等級的細節的文檔缺乏不能幫什麼忙. 在這篇文章中, 我圍繞着一個簡單的callback展現了操作系統等級的SEH. 如果你理解了SEH的原理, 還有在其上面建立起來的各個層次, 系統等級的結構化異常處理也不難理解了.
原文地址:
A Crash Course on the Depths of Win32™ Structured Exception Handling
http://www.microsoft.com/msj/0197/Exception/Exception.aspx
相關鏈接:
CLR 中未處理異常的處置