深入研究 Win32 結構化異常處理

 

本文關鍵字:SEH, Windows, VisualC
摘要

就像人們常說的那樣,Win32 結構化異常處理(SEH)是一個操作系統提供的服務。你能找到的所有關於 SEH 的文檔講的都是針對某個特定編譯器的、建立在操作系統層之上的封裝庫。我將從 SEH 的最基本概念講起。

Matt Pietrek 著
董巖 譯
Victor 轉載自 Xfocus 並整理

在所有 Win32 操作系統提供的機制中,使用最廣泛的未公開的機制恐怕就要數結構化異常處理(structured exception handling,SEH)了。一提到結構化異常處理,可能就會令人想起 _try、_finally 和 _except 之類的詞兒。在任何一本不錯的 Win32 書中都會有對 SEH 詳細的介紹。甚至連 Win32 SDK 裏都對使用 _try、_finally 和 _except 進行結構化異常處理作了完整的介紹。既然有這麼多地放都提到了 SEH,那我爲什麼還要說它是未公開的呢?本質上講,Win32 結構化異常處理是操作系統提供的一種服務。編譯器的運行時庫對這種服務操作系統實現進行了封裝,而所有能找到的介紹 SEH 的文檔講的都是針對某一特定編譯器的運行時庫。關鍵字 _try、_finally 和 _except 並沒有什麼神祕的。微軟的 OS 和編譯器定義了這些關鍵字以及它們的行爲。其它的 C++ 編譯器廠商也只需要尊從它們定好的語義就行了。在編譯器的 SEH 層減少了直接使用純操作系統的 SEH 所帶來的危害的同時,也將純操作系統的 SEH 從大家的面前隱藏了起來。

我收到過大量的電子郵件說他們都需要實現編譯器級的 SEH 但卻找不到公開的文檔。本來,我可以指着 Visual C++ 和 Borlang C++ 的運行時庫的源代碼說看一下它們就行了。但是,不知道是什麼原因,編譯器級的 SEH 仍是個天大的祕密。微軟和 Borland 都沒有提供 SEH 最內層的源代碼。

在本文中,我會從最基本的概念上講解結構化異常處理。在講解的時候,我會將操作系統所提供的與編譯器代碼生成和運行時庫支持的分離開來。當深入關鍵性操作系統程序的代碼時,我基於的都是 Intel 版的 Windows NT 4.0。然而。我所講的大部分內容同樣適用於其它的處理器。

我會避免提及實際的 C++ 的異常處理,C++ 下用的是 catch() 而不是 _except。其實,真正的 C++ 異常處理的實現方式和我所講的方式也是極爲相似的。但是,真正 C++ 異常處理特有的複雜性會影響到我這裏所講的概念。對於深挖那些晦澀的 .H 和 .INC 文件並拼湊出 Win32 SEH 的相關代碼,最好的一個信息來源就是 IBM OS/2 的頭文件(特別是 BSEXCPT.H)。這對有相關經驗的人並沒什麼可希奇的,這裏講的 SEH 機制在微軟開發 OS/2 時就定義了。因此,Win32 的 SEH 與 OS/2 的極爲相似。

SEH in the Buff

若將 SEH 的細節都放到一起討論,任務實在艱鉅,因此,我會從簡單的開始,一層一層往深裏講。如果之前從未使用過結構化異常處理,則正好心無雜念。若是用過,那就要努力將 _try、GetExceptionCode 和 EXCEPTION_EXECUTE_HANDLER 從腦子中掃出,假裝這是一個全新的概念。Are you ready?Good。

當線程發生異常時,操作系統會將這個異常通知給用戶使用戶能夠得知它的發生。更特別的是,當線程發生異常時,操作系統會調用用戶定義的回調函數。這個回調函數想做什麼就能做什麼。例如,它可以修正引起異常的程序,也可以播放一段 .WAV 文件。無論回調函數幹什麼,函數最後的動作都是返回一個值告訴系統下面該幹些什麼(這樣說並不嚴格,但目前可以認爲是這樣)。既然在用戶代碼引起異常後,操作系統會回調用戶的代碼,那這個回調函數又是什麼樣的呢?換句話說,關於異常都需要知道哪些信息呢?其實無所謂,因爲 Win32 已經定義好了。異常的回調函數的樣子如下:

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 文件中查找開頭爲“STATUS_”的宏就能找到一大堆這樣的異常代號。例如,大家熟知的 STATUS_ACCESS_VIOLATION 的代號就是 0xC0000005。更爲完整的異常代號可以從 Windows NT DDK 中的 NTSTATUS.H 文件裏找到。EXCEPTION_RECORD 結構體的第四個元素是異常發生處的地址。其餘的 EXCEPTION_RECORD 域目前都可以忽略掉。_except_handler 函數的第二個參數是一個指向 establisher frame 結構體的指針。在 SEH 裏這可是個重要的參數,不過現在先不用管它。第三個參數是一個指向 CONTEXT 結構體的指針。CONTEXT 結構體定義在 WINNT.H 文件中,它保存着某一線程的寄存器的值。圖 1 即爲 CONTEXT 結構體的域。

圖 1:CONTEXT 結構
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;

當用於 SEH 時,CONTEXT 結構體保存着發生異常時各寄存器的值。無獨有偶,GetThreadContext 和 SetThreadContext 使用的也是相同的 CONTEXT 結構體。第四個也是最後的一個參數叫做 DispatcherContext,現在先不去管它。

簡單總結一下,當發生異常時會調用一個回調函數。這個回調函數需要四個參數,其中三個都是結構體指針。在這些結構體中,有些域重要,有些並不重要。關鍵的問題是 _except_handler 回調函數收到了大量的信息,比如異常的類型和發生的位置。異常回調函數需要使用這些信息來決定所採取的行動。

我很想現在就給出一個樣例程序來說明 _except_handler,只是仍有一些東西需要解釋,即當異常發生時操作系統是如何知道在那裏調用回調函數呢?答案在另一個叫 EXCEPTION_REGISTRATION 的結構體中。本文通篇都能見到這個結構體,因此對這部分還是不要囫圇吞棗爲好。唯一能找到 EXCEPTION_REGISTRATION 正式定義的地方就是 Visual C++ 運行時庫源代碼中的 EXSUP.INC 文件:

_EXCEPTION_REGISTRATION struc
        prev    dd              ?
        handler dd              ?
_EXCEPTION_REGISTRATION ends

可以看到,在 WINNT.H 的 NT_TIB 結構體定義中,這個結構體被稱爲 _EXCEPTION_REGISTRATION_RECORD。然而 _EXCEPTION_REGISTRATION_RECORD 的定義是沒有的,因此我所能用的只能是 EXSUP.INC 中的彙編語言的 struc 定義。對於我前面提到的 SEH 的未公開,這就是一例。

不管怎樣,我們回到目前的問題上來。當異常發生時,OS 是如何知道調用位置的呢?EXCEPTION_REGISTRATION 結構體有兩個域,第一個先不用管。第二個域,handler,爲一個指向 _except_ handler 回調函數的指針。有點兒接近答案了,但是還有個問題就是,OS 從哪裏能找到這個 EXCEPTION_REGISTRATION 結構體呢?

爲了回答這個問題,需要記住結構化異常處理是以線程爲基礎的。也就是說,每一個線程都有自己的異常處理回調函數。在 1996年 5 月的專欄中,我講了一個關鍵的 Win32 數據結構,線程信息塊(TEB 或 TIB)。這個結構體中有一個域對於 Windows NT, Windows 95, Win32s 和 OS/2 都是相同的。TIB 中的第一個 DWORD 是一個指向線程的 EXCEPTION_REGISTRATION 結構體的指針。在 Intel 的 Win32 平臺上,FS 寄存器永遠指向當前的 TIB,因此,在 FS:[0] 就可以找到指向 EXCEPTION_REGISTRATION 結構體的指針。答案出來了!當異常發生時,系統察看出錯線程的 TIB 並取回一個指向 EXCEPTION_REGISTRATION 結構體的指針,從而得到一個指向 _except_handler 回調函數的指針。現在操作系統已經有足夠的信息來調用 _except_handler 函數了,見圖 2。

圖 2:_except_handler 函數
 

把目前這一小點兒東西湊到一起,我寫了一個小程序來演示所講到的這個非常簡單的 OS 級的結構化異常處理。圖 3 所示的就是 MYSEH.CPP,它只有兩個函數。main 函數使用了三個內嵌的 ASM 塊。第一個塊使用兩條 PUSH 指令(“PUSH handler”和“PUSH FS:[0]”)在堆棧上構建了一個 EXCEPTION_REGISTRATION 結構體。PUSH FS:[0] 將 FS:[0] 的上一個值保存爲結構體的一部分,但是目前並不重要。重要的是堆棧上有一個 8 字節的 EXCEPTION_REGISTRATION 結構體。下一條指令(MOV FS:[0],ESP)將線程信息塊的第一個 DWORD 指向新的 EXCEPTION_REGISTRATION 結構體。

圖 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 寄存器(MOV EAX,0)然後將寄存器的值作爲內存地址,而下一條指令就向這個地址進行寫入(MOV [EAX],1),這就引發了異常。最後的 __asm 塊移除這個簡單的異常處理:首先恢復以前的 FS:[0] 的內容,然後從堆棧中彈出 EXCEPTION_REGISTRATION 記錄(ADD ESP,8)。

現在假設正在運行 MYSEH.EXE,看一下程序的執行情況。MOV [EAX],1 指令的執行引發了一個 access violation。系統察看 TIB 的 FS:[0] 並找到指向 EXCEPTION_REGISTRATION 結構體的指針。結構體中有一個指向 MYSEH.CPP 文件中的 _except_handler 函數的指針。系統將所需的四個參數入棧並調用 _except_handler 函數。一進入 _except_handler,代碼首先用一條 printf 語句打印“Yo! I made it here!”。然後,_except_handler 修復引起異常的問題。問題在於 EAX 指向了不可寫內存的地址(地址 0)。所做的修復就是修改 CONTEXT 中 EAX 的值,使其指向一個可寫的內存單元。在這個簡單的程序裏,一個 DWORD 類型變量(scratch)就是用於此目的的。_except_handler 函數的最後的動作就是返回 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 結構體組成的鏈表。此鏈表的表頭總是由線程信息塊的第一個 DWORD (Intel 機器上的 FS:[0] )所指向。

操作系統用這個 EXCEPTION_REGISTRATION 結構體鏈表做什麼?當異常發生時,系統遍歷此鏈表並查找回調函數與異常相符的 EXCEPTION_REGISTRATION。對於 MYSEH.CPP 來說,回調函數返回 ExceptionContinueExecution 型的值,與異常相符合。回調函數也可能不適合所發生的異常,這時系統就移向鏈表中下一個 EXCEPTION_REGISTRATION 結構體並詢問異常回調是否要處理此異常。圖 4 所示即爲此過程。

圖 4:查找一個處理異常的結構體

一旦系統找到了處理此異常的回調函數就停止對 EXCEPTION_REGISTRATION 鏈表的遍歷。我給出了一個異常回調不能處理異常的例子,見圖 5 的 MYSEH2.CPP。

圖 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;
}

爲簡單起見,我用了一點編譯器級的異常處理。main 函數只是建立一個 _try/_except 塊。_try 塊中的是一個對 HomeGrownFrame 函數的調用。函數與前面的 MYSEH 程序中的代碼很類似。它在堆棧上創建了一個 EXCEPTION_REGISTRATION 記錄並使 FS:[0] 指向此紀錄。在建立了新的處理程序後,函數主動引起異常,向 NULL 指針處進行寫入:

*(PDWORD)0 = 0;

這裏的異常回調函數,也就是 _except_handler,與前面的那個很不一樣。代碼先打印出函數的 ExceptionRecord 參數的異常代號和標誌。後面會說明打印此異常標誌的原因。因爲這個 _except_handler 函數並不能修復引起異常的代碼,它就返回 ExceptionContinueSearch。這就使得操作系統繼續查找鏈表中的下一個 EXCEPTION_REGISTRATION 記錄。下一個異常回調是用於 main 函數中的 _try/_except 代碼的。_except 塊只是打印“Caught the exception in main()”。此處的異常處理就是簡單地將其忽略。此處的一個關鍵的問題就是執行控制流。當處理程序不能處理異常時,就是在拒絕使控制流在此處繼續。接受異常的處理程序則在所有異常處理代碼完成之後決定控制流在哪裏繼續。這一點並不那麼顯而易見。

當使用結構化異常處理時,如果一場處理程序沒能處理異常,則函數可以用一種非正常的方式退出。例如,MYSEH2 的 HomeGrownFrame 函數中的處理程序並沒有處理異常。因爲異常處理鏈中後面的某個處理程序(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”開始的兩行,其區別是顯然的,即第一次的異常標誌爲 0,而第二次則爲 2。這裏就需要提到 unwinding 的概念。進一步講,當異常回調拒絕處理異常時,就又被調用了一次。這次回調並沒有立即發生,而是更爲複雜。我還需要再進一步明確異常發生時的情景。

當異常發生時,系統遍歷 EXCEPTION_REGISTRATION 結構體鏈表直至找到處理此異常的處理程序。一旦找到了處理程序,系統再一次遍歷此鏈表,直到處理異常的節點。在第二次遍歷中,系統對所有的異常處理函數進行第二次調用。關鍵的區別就是在第二次調用中,異常標誌被設爲值 2。這個值對應着 EH_UNWINDING(EH_UNWINDING 的定義在 Visual C++ 運行時庫源代碼的 EXCEPT.INC 裏,但 Win32 SDK 裏並沒有等價的定義)。

EH_UNWINDING 是什麼意思呢?當異常回調被第二次調用時(帶有 EH_UNWINDING 標誌),操作系統就給處理函數一次做所需清理的機會。什麼樣的清理呢?一個很好的例子就是 C++ 類的析構函數。當函數的異常處理函數拒絕處理異常時,控制流一般並不會以正常的方式從函數中退出。現在考慮一個函數,此函數聲明瞭一個局部的 C++ 類。C++ 規範指出析構函數是必須被調用的。第二次標誌爲 EH_UNWINDING 的異常處理回調就是爲做調用析構函數和 _finally 塊此類的清理工作提供機會。在異常被處理並且所有之前的 exception frames 都被調用以進行 unwind 之後,程序從回調函數選擇的地方繼續。但是記住,這並不等於將指令指針設爲所要的代碼地址並繼續執行。繼續執行出的代碼要求堆棧和幀指針(Intel CPU 的 ESP 和 EBP 寄存器)都設爲在處理異常的堆棧幀中相應的值。因此,接受某一異常的處理程序負責將堆棧指針和堆棧幀指針設爲包含處理異常的 SEH 代碼的堆棧幀中的值。

圖 6:從異常中回滾
圖 6:從異常中回滾

更一般地說,從異常中的 unwinding 使得位於處理幀的堆棧區域之下的所有的東西都被移除,幾乎相當於從未調用過那些函數。unwinding 的另一個效果就是鏈表中位於處理異常的 EXCEPTION_REGISTRATION 之前的所有 EXCEPTION_REGISTRATIONs 都被從鏈表中移除。這是有意義的,因爲這些 EXCEPTION_REGISTRATION 一般都是在堆棧上構建的。在異常被處理後,堆棧指針和堆棧幀指針在內存中的地址要比從鏈表中移除的那些 EXCEPTION_REGISTRATIONs 高。圖 6 所示即爲所述。

Help! Nobody Handled It!

到目前爲止我都是假設操作系統總能在 EXCEPTION_REGISTRATION 鏈表中找到處理程序。要是沒有相應的處理程序怎麼辦?這種情況幾乎不會發生。原因是操作系統私地下爲每個線程都準備了一個默認的異常處理程序。這個默認的異常處理程序總是鏈表的最後一個節點並總被選來處理異常。它的行爲與一般的異常回調函數有些不同,我之後會說明。

我們來看一下系統在那裏安插這個默認的、最終的異常處理程序。這顯然要在線程執行的前期進行,要在任何用戶代碼執行之前。圖 7 爲我爲 BaseProcessStart 寫的僞碼,BaseProcessStart 是 Windows NT 的 KERNEL32.DLL 的一個內部函數。BaseProcessStart 需要一個參數,即線程入口的地址。BaseProcessStart 運行在新進程的上下文中並調用入口點來啓動進程第一個線程。

圖 7:BaseProcessStart 的僞代碼
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 塊就是用來在異常處理鏈表中安裝那個默認的最終異常處理程序的。所有之後註冊的異常處理程序都會插在鏈表中這個處理程序的前面。若 lpfnEntryPoint 函數返回,線程就運行至完成而不引起異常。若是這樣,BaseProcessStart 調用 ExitThread 來結束線程。

要是另一種情況,即線程發生了異常卻再也沒有異常處理程序了怎麼辦?在這種情況下,控制流流進 _except 關鍵字後的大括號裏。在 BaseProcessStart 裏,這段代碼叫 UnhandledExceptionFilter API,我在後面還會回來介紹它。現在的關鍵是 UnhandledExceptionFilter API 包含着默認的異常處理函數。

若 UnhandledExceptionFilter 返回的是 EXCEPTION_EXECUTE_HANDLER,BaseProcessStart 的 _except 塊就執行。_except 塊代碼所作的就是調用 ExitProcess 來結束當前進程。仔細考慮一下,這樣做還是有意義的;一個常識就是,如果程序引起了異常又沒有處理程序能處理此異常,系統就結束該進程。僞碼中所展示的正是這種情況。

還要最後補充一點。如果引發異常的線程是作爲服務運行的且是用於一個基於線程的服務,則 _except 塊並不會調用 ExitProcess 而是調用 ExitThread。沒有人會因爲一個服務出錯而結束整個服務進程。

UnhandledExceptionFilter 中的默認異常處理程序又作了些什麼呢?當我在討論班上提出這個問題時,沒幾個人能猜出未處理的異常發生時操作系統的默認行爲。通過對默認處理程序行爲的演示,答案一點即明,人們就都明白了。我只是運行了一個主動引起異常的程序,並指出其結果(見圖 8)。

圖 8:未捕獲的異常對話框
圖 8:未捕獲的異常對話框

UnhandledExceptionFilter 顯示了一個對話框,告訴你發生了一個異常。此時,要麼可以結束進程,要麼就調試引發異常的進程。在這幕後還有相當多的操作,我在本文結束前再來講這些東西。正如我所提到的,當異常發生時,用戶編寫的代碼可以得到執行(通常是這樣的)。類似地,在 unwind 操作過程中,用戶編寫的代碼也可以得到執行。用戶的代碼可能仍有問題並引起另一個異常。因此,異常回調函數還可以返回另外兩個值:ExceptionNestedException 和 ExceptionCollidedUnwind。顯然這些內容就很深了,我並不想在這裏介紹。其對於理解基本事實來說太難了。

Compiler-level SEH

儘管我偶爾會使用 _try 和 _except,但目前我所講到的都是由操作系統實現的。然而,看看我那兩個使用純操作系統 SEH 的程序的變態樣子,編譯器對此的封裝實在是必要的。我們來看一下 Visual C++ 是如何在操作系統級的 SEH 支持之上構建其結構化異常處理的。

在繼續進行之前要記住一件重要的事,那就是另一種編譯器可能會與純操作系統級的 SEH 的做法完全不同。沒有人說過必須要實現 Win32 SDK 文檔所描述的 _try/_except 模型。例如,Visual Basic 5.0 在其運行時代碼裏使用了結構化異常處理,但其數據結構與算法與我這裏所講的完全不同。若讀一下 Win32 SDK 文檔關於結構化異常處理的描述,就會找到所謂的“frame-based”的異常處理程序的語義,其形式如下:

try {
    // guarded body of code
}
except (filter-expression) {
    // exception-handler block
}

簡單講,try 中的所有的代碼都被一個構建在函數堆棧幀上的 EXCEPTION_REGISTRATION 保護起來。在函數的入口,新的 EXCEPTION_REGISTRATION 被放入異常處理鏈表的表頭。在 _try 塊的結尾處,其 EXCEPTION_REGISTRATION 被從鏈表頭移除。如前所述,異常處理鏈的表頭保存在 FS:[0]。因此,若在調試器中的彙編代碼中單步執行,就會看到以下的指令:

MOV DWORD PTR FS:[00000000],ESP

或是

MOV DWORD PTR FS:[00000000],ECX

可以十分確信代碼正在建立或撤除一個 _try/_except 塊。現在知道了一個 _try 塊對應着堆棧上的一個 EXCEPTION_REGISTRATION 結構體,那 EXCEPTION_REGISTRATION 裏的回調函數呢?使用 Win32 的術語,異常回調函數對應着 filter-expression 代號。filter-expression 就是關鍵字 _except 後括號中的代碼。正是這個 filter-expression 代號決定了是否執行後面 {} 塊中的代碼。

因爲 filter-expression 是程序員寫的,程序員可以決定代碼中某處發生的異常是否在該處處理。filter-expression 代碼可以簡單到只有一個“EXCEPTION_EXECUTE_HANDLER”,也可以調用一個函數把 p 算到兩千萬再返回一個代號告訴系統下一步做什麼,這是程序員的選擇。關鍵一點是:filter-expression 的代號正對應我前面提到的異常回調函數。

我剛纔所講的都十分簡單,但是隻是理想中的美好的情形。殘酷的現實是事情要複雜的多。對於初學者,filter-expression 並不是由操作系統直接調用的。實際的情形是每個 EXCEPTION_REGISTRATION 的異常處理程序域都指向同一個函數。這個函數在 Visual C++ 的運行時庫中,叫做 __except_handler3。是 __except_handler3() 調用了你的 filter-expression 代碼,這是後話。

另外一點就是並不是每次進入或退出 _try 塊都要建立或撤除 EXCEPTION_REGISTRATION。對於使用 SEH 的每個函數,只創建一個 EXCEPTION_REGISTRATION。換句話說,在一個函數裏可以使用多個 _try/_except 組合,但只在堆棧上建立一個 EXCEPTION_REGISTRATION。類似地,可以在一個函數的 _try 塊中嵌套另一個 _try 塊,Visual C++ 仍然只創建一個 EXCEPTION_REGISTRATION。如果對於整個 EXE 或 DLL 來說一個異常處理程序就足夠了以及如果用一個 EXCEPTION_REGISTRATION 就可以處理多個 _try 塊,那顯然還要有比所見到的更多的機制。這是通過一個一般情況下看不到的表中的數據來完成的。然而,既然本文的目的就是要解剖結構化異常處理,我們就來看一下這些數據結構。

The Extended Exception Handling Frame

Visual C++ 的 SEH 實現並沒有使用純粹的 EXCEPTION_REGISTRATION 結構,而是在結構體的末尾加入了額外的數據域。這個額外的數據的關鍵之處在於它允許一個函數(__except_handler3)來處理所有的異常並將控制流轉向相應的 filter-expressions 和代碼中的 _except 塊。關於這個 Visual C++ 擴展的 EXCEPTION_REGISTRATION 的一點信息可以從 Visual C++ 的運行時庫源代碼中的 EXSUP.INC 文件裏找到。在這個文件裏,可以找到一下定義:

; 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;
; };

前兩個域前面已經見過了,prev 和 handler。他們組成了最基本的 EXCEPTION_REGISTRATION 結構體。新加的是最後的三個域:scopetable、trylevel 和 _ebp。scopetable 域指向一個 scopetable_entries 類型結構體數組,而 trylevel 是這個數組的索引。最後一個域,_ebp,是創建 EXCEPTION_REGISTRATION 之前的堆棧幀指針(EBP)的值。

_ebp 域成爲擴展的 EXCEPTION_REGISTRATION 結構體的一部分不是偶然的。結構體包含它是因爲大多數函數都以一個 PUSH EBP 開始。這就使得所有其它的 EXCEPTION_REGISTRATION 域可以通過幀指針的負偏移來訪問。例如,trylevel 在 [EBP-04],scopetable 指針在 [EBP-08] 等等。

在擴展的 EXCEPTION_REGISTRATION 結構體後面,Visual C++ 壓入了兩個額外的值。第一個 DWORD 爲一個指向 EXCEPTION_POINTERS 結構體(一個標準的 Win32 結構體)的指針保留空間。這個指針就是調用 GetExceptionInformation API 返回的指針。儘管 SDK 文檔隱含提到 GetExceptionInformation 是一個標準的 Win32 API,但事實上 GetExceptionInformation 是一個編譯器相關的函數。當調用此函數時,Visual C++ 生成下面的代碼:

MOV EAX,DWORD PTR [EBP-14]

與 GetExceptionInformation 相同,GetExceptionCode 也依賴於編譯器。GetExceptionCode 返回的值是 GetExceptionInformation 返回的數據結構中一個域的值。Visual C++ 會生成以下的代碼,這些代碼的作用留給讀者作爲練習。

MOV EAX,DWORD PTR [EBP-14]
MOV EAX,DWORD PTR [EAX]
MOV EAX,DWORD PTR [EAX]

回到擴展的 EXCEPTION_REGISTRATION 結構體,在結構體起始處之前的 8 個字節處,Visual C++ 保留了一個 DWORD 來保存所有 prologue 代碼執行後最終的堆棧指針(ESP)。這個 DWORD 就是函數執行時一個普通的 ESP 寄存器的值(當然參數壓棧是爲了準備調用另外函數的情況除外)。

看起來我好像一股腦兒倒出了一大堆東西,確實是。在向下繼續之前,我們先暫停一會兒,複習一下 Visual 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

從操作系統的觀點來看,構成純 EXCEPTION_REGISTRATION 的域僅有兩個:[EBP-10] 處的 prev 指針和 [EBP-0Ch] 處的處理函數指針。幀中其它的東西都是依賴於 Visual C++ 實現的。記住這些後,我們來看包含着編譯器級 SEH 的 Visual C++ 的運行時庫函數,__except_handler3。

__except_handler3 and the scopetable

儘管我非常想將 Visual C++ 的運行時庫源代碼指點出來並讓讀者自己去研究 __except_handler3 函數,但是我不能,因爲此函數的代碼並未提供。這裏只好用我倉促拼湊出的 __except_handler3 的僞碼來應付一下了(見圖 9)。

圖 9:__except_handler3 的僞代碼
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 中的 homegrown 異常回調函數一樣,此函數也需要四個參數。在最高一級上,__except_handler3 被一個 if 語句分爲了兩部分。這是因爲函數可以被調用兩次,一次是正常調用,一次是在 unwind 過程中。大部分的代碼都用在了 non-unwinding 的回調中。

這段代碼的開頭首先在堆棧上創建一個 EXCEPTION_POINTERS 結構體,並用 __except_handler3 的兩個參數將其初始化。此結構體的地址,即僞碼中的 exceptPtrs,被放在 [EBP-14]。這就初始化了 GetExceptionInformation 和 GetExceptionCode 函數用到的指針。接着,__except_handler3 從 EXCEPTION_REGISTRATION 幀([EBP-04])取得當前的 trylevel。trylevel 變量用作 scopetable 數組的索引,使得一個 EXCEPTION_REGISTRATION 可以用於一個函數中的多個 _try 塊和嵌套的 _try 塊。每一個 scopetable 的成員定義如下:

typedef struct _SCOPETABLE
{
    DWORD       previousTryLevel;
    DWORD       lpfnFilter
    DWORD       lpfnHandler
} SCOPETABLE, *PSCOPETABLE;

SCOPETABLE 中的第二個和第三個參數都很容易理解。它們是 filter-expression 和相應的 _except 塊代碼的地址。previousTryLevel 域有點複雜。簡言之,它是用於嵌套 try 塊的。重要的一點是對函數中每一個 _try 塊都有一個 SCOPETABLE 成員。

如前所述,當前的 trylevel 指定了要使用的 scopetable 數組成員,也就指定了 filter-expression 和 _except 塊的地址。現在,考慮一種情形,即一個 _try 塊嵌套在另一個 _try 裏。若內層 _try 塊的 filter-expression 並沒有處理異常,則外層的 _try 塊的 filter-expression 就必須處理。 __except_handler3 如何知道哪一個 SCOPETABLE 成員對應着外層的 _try 呢?它的索引由 SCOPETABLE 成員的 previousTryLevel 域給出。使用這種機制,就可以創建任意嵌套的 _try 塊。previousTryLevel 域爲函數中可能的異常處理鏈表的一個節點。鏈表的結尾由一個 0xFFFFFFFF 的 trylevel 指示。

回到 __except_handler3 的代碼,在取得當前 trylevel 之後,代碼就指向了相應的 SCOPETABLE 成員並調用了 filter-expression 代碼。若 filter-expression 返回 EXCEPTION_CONTINUE_SEARCH,__except_handler3 繼續查找下一個 SCOPETABLE 成員,這個成員由 previousTryLevel 域指定。若在遍歷過程中沒有找到處理程序,__except_handler3 就返回 DISPOSITION_CONTINUE_SEARCH,這就使系統移向下一個 EXCEPTION_REGISTRATION 幀。

若 filter-expression 返回 EXCEPTION_EXECUTE_HANDLER,則意味着異常應該由相應的 _except 塊代碼來處理。這就意味着所有之前的 EXCEPTION_REGISTRATION 幀都要從鏈表中移除而且要執行 _except 塊。第一個活兒是通過調用 __global_unwind2 來完成的,之後我再介紹。在一些清理代碼之後,代碼的執行就離開了 __except_handler3 並進入 _except 塊。奇怪的是控制流從未從 _except 塊返回,儘管 __except_handler3 調用了它。如何設置當前的 trylevel 呢?這是由編譯器暗自處理的,編譯器以 on-the-fly 的方式完成對擴展的 EXCEPTION_REGISTRATION 結構體中的 trylevel 域的修改。如果察看使用 SEH 的函數的彙編代碼就會發現函數代碼的不同位置都有修改 [EBP-04] 處的當前 trylevel 的代碼。__except_handler3 如何調用的 _except,而控制流又爲何從不返回呢?因爲一個 CALL 指令將返回地址壓入堆棧,可以認爲這就打亂了堆棧。如果察看一下爲 _except 塊生成的代碼,就會發現它所作的第一件事就是將 EXCEPTION_REGISTRATION 結構體之後的8字節處的 DWORD 加載到 ESP 寄存器中。作爲其 prologue 代碼的一部分,函數將 ESP 的值保存起來,這樣 _except 之後還可以將其取回。

The ShowSEHFrames Program

此時是不是對 EXCEPTION_REGISTRATIONs、scopetables、trylevels、filter-expressions 和 unwinding 這些東西感到有些招架不住,我當初也是這樣的。編譯器級的結構化異常處理的主題對更多的學習並沒有什麼幫助。如果沒有總體上的瞭解的話,其中的很多東西就沒有意義。當面對一大堆理論時,我很自然地傾向於寫些使用這些理論的代碼。如果程序能工作,我就知道我的理解(通常是)是正確的。

圖 10 是 ShowSEHFrames.EXE 的源代碼。它使用 _try/_except 塊來建立起由幾個 Visual C++ SEH 幀構成的鏈表。之後,顯示每一幀的信息,以及 Visual C++ 爲每一幀建立的 scopetables。程序並不生成任何異常。我包含了所有的 _try 塊來強制 Visual C++ 生成多個 EXCEPTION_ REGISTRATION 幀,每一幀有多個 scopetable 成員。

圖 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] 得到一個指向異常鏈表表頭的指針然後遍歷鏈表中的每一個節點。每個節點都是 VC_EXCEPTION_REGISTRATION 類型的,我定義這個結構體是爲了描述 Visual C++ 的異常處理幀。對於鏈表中的每一個節點,WalkSEHFrames 將指向節點的指針傳遞給 ShowSEHFrame 函數。

ShowSEHFrame 首先打印異常幀的地址、回調函數地址、前一異常幀的地址和一個指向 scopetable 的指針。接着,對於每一個 scopetable 成員,代碼打印出 previous trylevel,filter-expression 的地址和 _except 塊的地址。我又是怎麼知道 scopetable 中到底有多少個成員的呢?其實我並不知道。我假設 VC_EXCEPTION_REGISTRATION 中的當前 trylevel 比 scopetable 的成員總數少一。

圖 11 所示即爲 ShowSEHFrames 的運行結果。

圖 11:ShowSEHFrames 的運行結果

首先看以“Frame:”開頭的每一行。注意每個後繼的實例是如何顯示堆棧上高地址的異常幀的。接着,在前三個 Frame: 行裏,注意 Handler 的值都是相同的(004012A8)。看看輸出的開頭就知道這個 004012A8 就是 Visual C++ 運行時庫的 __except_handler3 函數的地址。這就證實了我前面所說的一個成員處理多個異常。

也許有人會疑惑,因爲 ShowSEHFrames 只有兩個使用 SEH 的函數,而卻有三個使用 __except_handler3 作爲回調函數的異常幀。第三個異常幀來自於 Visual C++ 的運行時庫。Visual C++ 的運行時庫的 CRT0.C 源代碼顯示對 main 或 WinMain 的調用被封裝在了 _try/_except 塊中。這個 _try 塊的 filter-expression 代碼在 WINXFLTR.C 文件中。

回到 ShowSEHFrames,最後一幀的 Handler:此行包含一個不同的地址,77F3AB6C。經過查找,就會發現這個地址是在 KERNEL32.DLL 裏。這個特殊的幀是由 KERNEL32.DLL 的 BaseProcessStart 函數安裝的,這個函數我在前面講到過。

Unwinding

在深挖 unwinding 的實現代碼之前,我們先來簡要總結一下 unwinding 的含義。前面我曾提到異常處理程序信息是如何保存在鏈表裏的,又是如何由線程信息塊的第一個 DWORD (FS:[0])來指向的。因爲某一異常的處理程序不一定是鏈表的頭節點,這就需要有一種有序的方法來移除此實際處理程序之前鏈表中的所有異常處理程序。

正如在 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,但卻沒有公開。它是一個 KERNEL32 函數,Windows NT 將 KERNEL32.DLL 調用 forward 到了 NTDLL.DLL,在 NTDLL.DLL 裏的也是一個 RtlUnwind 函數。我拼湊了這個函數的一些僞碼,即圖 12 所示。

圖 12:RtlUnwind 的僞代碼
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] 取得線程堆棧的當前棧頂和棧底。這兩個值對於後面的健壯性檢查是很重要的,這裏的健壯性檢查就是保證所有被 unwound 的異常幀都落在堆棧的範圍內。

接着,RtlUnwind 在堆棧上建立一個 EXCEPTION_RECORD,並將 ExceptionCode 域設爲 STATUS_UNWIND。而且 EXCEPTION_RECORD 的 ExceptionFlags 域中的 EXCEPTION_UNWINDING 標誌也要置位。指向此 EXCEPTION_RECORD 結構體的指針之後會作爲參數傳遞給每一個異常回調函數。此後,代碼調用 _RtlpCaptureContext 函數來創建一個 CONTEXT 結構體,此結構體也會作爲異常回調的 unwind 調用的一個參數。RtlUnwind 後面的部分就遍歷 EXCEPTION_REGISTRATION 結構體的鏈表。對於每一幀,代碼調用 RtlpExecuteHandlerForUnwind 函數,後面會講到此函數。正是這個函數用 EXCEPTION_UNWINDING 標誌調用了異常回調函數。每次回調之後,相應的異常幀通過調用 RtlpUnlinkHandler 將其移除。當 RtlUnwind 到達第一個參數指定地址的幀時,就停止 unwinding 幀。這些代碼中間還有許多用於錯誤檢查的代碼,這些代碼保證了程序的正常執行。如果出現了問題,RtlUnwind 就會引起異常來告知所遇到的問題,而且此異常的 EXCEPTION_NONCONTINUABLE 標誌是置位的。當此標誌置位時,是不允許進程繼續執行的,因此進程必須結束。

Unhandled Exceptions

本文前面部分我完整描述了 UnhandledExceptionFilter API。一般不用直接調用這個 API(儘管可以)。大多數情況下,它是由 KERNEL32 的默認異常回調的 filter-expression 代碼來調用的。前面的 BaseProcessStart 僞碼說明了這一點。

圖 13 是我給出的 UnhandledExceptionFilter 的僞碼。這個 API 的開頭有些奇怪(至少在我看來是這樣的)。若是一個 EXCEPTION_ACCESS_ VIOLATION 異常,代碼就調用 _BasepCheckForReadOnlyResource。儘管我沒有提供此函數的僞碼,但我這裏可以大概說一下。如果是因爲向 EXE 或 DLL 的 resource section(.rsrc)進行寫入而發生異常,_BasepCurrentTopLevelFilter 就修改引起異常的頁的屬性,從而允許寫操作,UnhandledExceptionFilter 返回 EXCEPTION_CONTINUE_EXECUTION,並重新執行引起異常的指令。

圖 13:UnhandledExceptionFilter 的僞代碼
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 調試器下運行。也就是說,進程是以 DEBUG_PROCESS 或 DEBUG_ONLY_THIS_PROCESS 標誌創建的。UnhandledExceptionFilter 使用 NtQueryInformationProcess 函數來判斷進程是否正在被調試。如果是,此 API 就返回 EXCEPTION_CONTINUE_SEARCH,說明系統的其它部分會喚醒調試器進程並告知調試器被調試進程引起了異常。如果有 user-installed unhandled exception filter,則調用它。一般都沒有 user-installed 的回調,但是可以用 SetUnhandledExceptionFilter API 裝一個。我提供了這個 API 的僞碼。這個 API 只是用新的用戶回調的地址來修改一個全局變量,然後返回舊回調的值。

做好準備工作後,UnhandledExceptionFilter 就可以進行其主要的工作:用那個老面孔的應用程序錯誤對話框來通知程序的錯誤。有兩種辦法可以避免此對話框的出現。第一種就是進程調用了 SetErrorMode 並設置了 SEM_NOGPFAULTERRORBOX 標誌。另一種就是將 AeDebug 註冊鍵值下的 Auto 值設爲 1。這時,UnhandledExceptionFilter 略過程序錯誤對話框並自動啓動由 AeDebug 鍵的 Debugger 值所指定的調試器。如果對“just in time debugging”比較熟悉的話,這就是操作系統對其的支持,之後還會討論。

大多數情況下,這兩種逃避此對話框的條件都爲假,UnhandledExceptionFilter 就調用 NTDLL.DLL 函數中的 NtRaiseHardError 函數。正是這個函數喚出了程序錯誤對話框。這個對話框等待用戶點擊 OK 結束進程或 Cancel 調試進程。

若點擊了 OK,UnhandledExceptionFilter 就返回 EXCEPTION_EXECUTE_HANDLER。調用 UnhandledExceptionFilter 的代碼通常以結束自己來回應(就像 BaseProcessStart 代碼中那樣的)。這就帶來一個有趣的問題。多數人認爲系統沒有處理異常而將進程結束。實際上更準確的說法是系統作了一些工作,這樣未處理的異常使進程自己將自己結束。

如果點擊了程序錯誤對話框的 Cancel 才執行了 UnhandledExceptionFilter 真正有意思的代碼,這時會調試器加載引起異常的進程。在調試器 attach 到出錯進程後,代碼首先調用 CreateEvent 來創建一個事件以通知調試器。事件句柄和當前進程 ID 都要傳遞給 sprintf,sprintf 格式化啓動調試器的命令行。萬事俱備後,UnhandledExceptionFilter 調用 CreateProcess 來啓動調試器。若 CreateProcess 成功,代碼對前面創建的事件調用 NtWaitForSingleObject。此調用一直阻塞直到調試器進程通知此事件,指示調試器已經成功地 attach 到出錯進程上。UnhandledExceptionFilter 還有其它的零星代碼,但我這裏只撿重要的說了說。

Into the Inferno

到了目前這個地步,如果還保留什麼就太不公平了。我已經講了發生異常時操作系統如何調用用戶定義的函數;講了一般回調的內部運行以及編譯器如何使用它們來實現 _try 和 _catch;講了沒人處理異常時的情況以及系統對其的處理。所剩下的只有起初異常回調是從何處開始的。是的,我們來深入系統內幕來看看結構化異常處理的開始階段。

圖 14 所示爲我爲 KiUserExceptionDispatcher 和一些相關函數寫的僞碼。KiUserExceptionDispatcher 位於 NTDLL.DLL 中,它是異常發生後執行的起點。這樣說也不是百分之百的準確。例如,在 Intel 體系下,異常會使控制轉到一個 ring 0 (內核模式)的處理程序。此處理程序由對應此異常的中斷描述符表表項所定義。我將跳過所有的內核模式代碼並假設發生異常時 CPU 直接執行 KiUserExceptionDispatcher。

圖 14:KiUserExceptionDispatcher 的僞代碼
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 的調用。這個調用啓動了對註冊的異常處理程序的查找。如果處理程序處理了異常並繼續執行,則對 RtlDispatchException 的調用不再返回。如果 RtlDispatchException 返回了,則有兩種可能:要麼調用了 NtContinue 使進程繼續,要麼就是產生了另一個異常。若是後者,異常就不能再繼續了,進程必須結束。接着說 RtlDispatchExceptionCode,這就是遍歷異常幀的代碼。函數獲得一個指向 EXCEPTION_REGISTRATIONs 鏈表的指針並遍歷每一個節點查找處理程序。因爲堆棧可能崩潰掉,這個函數非常謹慎。在調用每個 EXCEPTION_REGISTRATION 指定的處理程序之前,代碼要保證在線程堆棧中 EXCEPTION_REGISTRATION 是 DWORD 對齊的且前面的 EXCEPTION_REGISTRATION 的地址高。

RtlDispatchException 並不直接調用 EXCEPTION_REGISTRATION 結構體中指定的地址,而是調用 RtlpExecuteHandlerForException 來做這個髒累活兒。根據 RtlpExecuteHandlerForException 內部發生的情況,RtlDispatchException 要麼繼續遍歷異常幀要麼產生另一個異常。這個二級異常指示異常回調函數中出現問題不能繼續執行。RtlpExecuteHandlerForException 的代碼和另一個函數 RtlpExecutehandlerForUnwind 緊密相關。我在前面講 unwinding 時曾提到這個函數。這兩個函數都在將控制送到 ExecuteHandler 函數之前用不同的值加載 EDX 寄存器。換種說法就是 RtlpExecuteHandlerForException 和 RtlpExecutehandlerForUnwind 是同一個 ExecuteHandler 函數的不同的前端。

ExecuteHandler 就是 EXCEPTION_REGISTRATION 的 handler 域被取出和執行的地方。也許看上去有些奇怪,對異常回調函數的調用本身也被一個結構化異常處理程序封裝了起來。在這裏使用 SEH 儘管有點兒怪,但認真考慮一下還是合理的。如果異常回調引起了另一個異常,操作系統需要知道此事件。根據異常是發生在初始的回調還是 unwind 中的回調,ExecuteHandler 返回 DISPOSITION_NESTED_EXCEPTION 或 DISPOSITION_COLLIDED_UNWIND。這兩個可都是“紅色警戒!立即關閉!”級別的代號。讀者也許像我一樣很難讓所有的函數都與 SEH 直接關聯。類似地,也很難記住誰調用了誰。爲了幫助我自己,我畫了個圖即圖 15。

圖 15:SEH 中的調用關係
KiUserExceptionDispatcher()

    RtlDispatchException()

        RtlpExecuteHandlerForException()

            ExecuteHandler() // Normally goes to __except_handler3

---------

__except_handler3()

    scopetable filter-expression()

    __global_unwind2()

        RtlUnwind()

            RtlpExecuteHandlerForUnwind()

    scopetable __except block()

現在,在執行 ExecuteHandler 前設置 EDX 寄存器幹什麼呢?其實很簡單。若調用用戶的處理程序時出錯,則不管 EDX 裏是什麼 ExecuteHandler 都會將其作爲純粹的異常處理程序。它將 EDX 寄存器壓棧作爲最小 EXCEPTION_REGISTRATION 結構體的 handler 域。本質上講,ExecuteHandler 使用的純粹的異常處理和我在 MYSEH 和 MYSEH2 程序裏使用的差不多。

Conclusion

結構化異常處理是 Win32 的一個奇妙特性。多虧了像 Visual C++ 這樣的編譯器在它上面加上的支持層,一般的程序員才能用較少的學習代價而從 SEH 中受益。然而,在操作系統這一級,事情可就比 Win32 文檔所講的複雜多了。不幸的是,因爲幾乎所有的人都覺得系統級 SEH 是個很難的課題,所以至今沒有什麼這方面的文章。系統級細節方面的文檔的缺乏狀況一直未得改善。在本文中,我已經展示了系統級的 SEH 是圍繞一個相對簡單的回調函數展開的。如果理解了回調函數的本質,再在此基礎上層曾構建其它的理解層次,系統級的結構化異常處理其實也沒那麼難掌握。

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