1. 程式人生 > >Windows結構化異常處理(SEH) - by Matt Pietrek

Windows結構化異常處理(SEH) - by Matt Pietrek

原文題目: A Crash Course on the Depths of Win32™ Structured Exception Handling

作者: Matt Pietrek

 

About Matt Pietrek

Matt Pietrek (born January 27, 1966) is an American  computer specialist and author specializing in Microsoft Windows.(維基百科)

 

 

       在Win32的核心, 結構化異常處理(Structured Exception Handling)(SEH) 是作業系統提供的一種服務. 你能找到的所有關於SEH的文件都會描述某一種編譯器的執行時庫(runtime library)對作業系統實現的某種包裝. 我會層層剝析SEH一直到它的最基本的概念.

       在所有由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所示:

exceptionfig02

 

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

exceptionfig04

       我展現了一個異常回調函式的例子, 看看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程式碼.

 

exceptionfig06

 

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

exceptionfig08

       在較高的層次上看, 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是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

exceptionfig11

       你可能在想, 為什麼有三個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