1. 程式人生 > >漫談相容核心之二十四:Windows的結構化異常處理(一)

漫談相容核心之二十四:Windows的結構化異常處理(一)

結構化異常處理(Structured Exception Handling),簡稱SEH,是Windows作業系統的一個重要組成部分。

在ReactOS核心的原始碼中,特別是在實現系統呼叫的程式碼中,讀者已經看到很多類似於這樣的程式碼:

   if(MaximumSize != NULL && PreviousMode != KernelMode)

   {

     _SEH_TRY

     {

       ProbeForRead(MaximumSize, sizeof(LARGE_INTEGER), sizeof(ULONG));

       /* make a copy on the stack */

       SafeMaximumSize = *MaximumSize;

       MaximumSize = &SafeMaximumSize;

     }

     _SEH_HANDLE

     {

       Status = _SEH_GetExceptionCode();

     }

     _SEH_END;

     if(!NT_SUCCESS(Status))

     {

       return Status;

     }

   }

這段程式碼取自NtCreateSection(),其引數之一是指標MaximumSize。系統呼叫一般都是從使用者空間呼叫的,因此PreviousMode一般不是KernelMode。所以,只要指標MaximumSize不是NULL,就要從它所指的地方從使用者空間把數值複製到核心空間。那為什麼不直接把它的數值作為引數傳遞,而要這樣繞一下呢?這是因為它的型別為LARGE_INTEGER,而作為引數傳遞的只能是32位(或以下)的普通整數。

然而,從使用者空間複製資料到核心空間(或反過來)恰恰是容易出事的。這是因為,使用者程式的質量相對而言是沒有保證的,這個指標所指向的地址(所在的頁面)也許根本就沒有對映,或者也許不允許讀,那樣就會發生與頁面對映和訪問有關的異常(Exception)。

不過倒也並非只要發生異常就有問題,例如要是頁面已經對映、也允許讀,但是所在頁面已經換出(Swap-Out),那就會發生缺頁異常;而缺頁異常其實不是“異常”而是“正常”,核心從磁碟上換入(Swap-In)目標頁面,就可以從異常處理程式返回、並繼續運行了,就像發生了一次中斷一樣。此時CPU將重新執行發生異常的指令,這一次一般就能正常完成了。所以,(物理上的)異常之是否真的(邏輯上)“異常”,還得看具體的原因。只要沒有特別加以說明,本文中所講的異常都是指真正意義上的異常。

對於異常的處理,核心一般會提供預設的方式,例如“殺掉”當前程序,讓其一死百了,這樣至少不會危害別的程序。但是如果具體的程式預期在某一段程式碼中有可能發生某幾種特定的異常,並願意為之提供解決、補救之道,那當然是更合理、更優雅的方式。舉例言之,假如使用者程式中有除法運算,CPU在碰到除數為0的時候就會發生異常,此時預設的處理方式一般是中止該使用者程式的執行,因為不知該怎樣讓它繼續下去了。然而這可能發生在已經連續計算了幾十個小時以後,離成功也許只有一步之遙了,讓它就這樣退出執行未免損失太大。如果程式的設計人員事先估計到有這樣的可能,也許會選擇在這種情況下彈出一個對話方塊,提示使用者改變幾個引數,然後以新的條件繼續運算;或者至少問一下使用者,是否把發生問題時的“現場”資訊通過郵件傳送給程式的設計者。顯然,這是更好的解決方案。問題在於如何來實現,如何為程式的設計者提供這樣做的手段。

簡而言之,就是要為程式的設計者提供一種手段,使得倘若在執行某一段程式碼的過程中發生了特定種類的異常就執行另一些指定的程式碼。事實上,這正是微軟的“結構化異常處理(Structured Exception Handling)”、即SEH機制要解決的問題之一。後面讀者將會看到,SEH要解決兩類問題,這是其中之一。

在上列的程式碼片斷中,在_SEH_TRY{}裡面是要加以“保護”的程式碼,即使用者估計可能會在執行中發生異常的程式碼;而_SEH_HANDLE{}裡面就是當發生異常時需要執行的程式碼;最後的_SEH_END則說明與SEH有關的程式碼到此為止,從此以後的程式碼恢復常態。這樣,如果在執行_SEH_TRY{}裡面受保護程式碼的過程中發生了某些異常,CPU就轉入_SEH_HANDLE{};而若順利執行完_SEH_TRY{}裡面的程式碼,那就跳過_SEH_HANDLE{}直接到達_SEH_END。

注意在_SEH_TRY{}裡面可能會呼叫別的函式,被呼叫函式的程式碼雖然形式上不在_SEH_TRY{}裡面,但是它的本次被呼叫執行卻同樣是在_SEH_TRY{}所指定的保護範圍之內。在本文中,由_SEH_TRY{}所劃定的範圍稱為一個“SEH保護域”,也稱“SEH框架”,因為在執行這些程式碼時這表現為堆疊上的一個框架。所以在本文中“SEH域”和“SEH框架”是同義詞。說是“保護域”,其實也可以說是“捕捉域”,就是說這是一個需要“捕捉”住異常的域(所以在C++語言中用“catch”表示捕捉到異常之後要執行的程式碼)。注意SEH域和函式是互相獨立的兩個概念。同一個函式,這一次是從_SEH_TRY{}裡面呼叫,它的執行就在SEH域中;下一次不是從_SEH_TRY{}裡面呼叫,就不在這個SEH域中了。所以一個函式(的執行)是否在SEH域裡面是個動態的概念。不過,SEH域總是存在於某個函式的內部,而不可能遊離在函式之外,就像C語句只能存在於函式之內一樣。

在實際應用中,SEH域還可以巢狀,就是在一個SEH域的內部又通過_SEH_TRY{}開闢了第二個SEH域。例如,前者也許是針對頁面異常的SEH域,而在這裡面又有一部分程式碼可能會引起“除數為0”的異常,所以又得將其保護起來,形成一個嵌在外層保護域裡面的內層保護域。

顯然,多個SEH框架巢狀就形成了一個SEH框架棧。SEH框架棧既可以只是實質的,也可以既是實質的、又是形式的。比方說,一個SEH域的內部呼叫了一個函式,而在這個函式中又有一個SEH域,那麼這兩個SEH域(框架)的巢狀是實質的,但卻不是形式的,因為從程式碼上不能一目瞭然看出這樣的巢狀關係,這種巢狀關係是執行起來才形成的。但是,如果在第一個SEH域的_SEH_TRY{}內部直接又有一個_SEH_TRY{},那麼這兩個SEH域的巢狀關係就既是實質的、又是形式的。在本文中,前者所形成的SEH框架棧稱為“實質”SEH框架棧、或“全域性”SEH框架棧,後者所形成的則稱為“形式”SEH框架棧、或“區域性”SEH框架棧。之所以如此,是因為0.3.0版ReactOS的程式碼中對於SEH機制的實現有了一些變動。不過,在0.3.0版ReactOS的程式碼中並未見到使用形式巢狀的SEH域。

回頭看前面_SEH_TRY{}裡面的程式碼。這裡受保護的有三個語句,先看對ProbeForRead()的呼叫。ProbeForRead()是個核心函式,這裡也是在核心中呼叫,所以對這個函式的呼叫本身並沒有問題。

VOID STDCALL

ProbeForRead (IN CONST VOID *Address, IN ULONG Length, IN ULONG Alignment)

{

   ASSERT(Alignment == 1 || Alignment == 2 || Alignment == 4 || Alignment == 8);

   if (Length == 0)

      return;

   if (((ULONG_PTR)Address & (Alignment - 1)) != 0)

   {

      ExRaiseStatus (STATUS_DATATYPE_MISALIGNMENT);

   }

   else if ((ULONG_PTR)Address + Length - 1 < (ULONG_PTR)Address ||

            (ULONG_PTR)Address + Length - 1 > (ULONG_PTR)MmUserProbeAddress)

   {

      ExRaiseStatus (STATUS_ACCESS_VIOLATION);

   }

}

其目的只是檢查引數的合理性,而並不真的去訪問使用者空間。如果使用者空間資料所在的地址不與給定資料型別(在這裡是ULONG)的邊界對齊,或者所在的位置不對、長度不合理,那就要通過ExRaiseStatus()以軟體方法模擬異常。這是為什麼呢?因為在正常的情況下這是不可能發生的,既然發生了就一定是出了問題,按理說最好是CPU在碰到這種情況時能引起一次異常,但是386結構的CPU不會(從486開始就會了,這就是17號異常“Alignment Check”),所以就只好通過軟體手段來模擬一次“軟異常”。注意這裡軟異常的型別為STATUS_DATATYPE_MISALIGNMENT和STATUS_ACCESS_VIOLATION,前者表示與資料型別的邊界不對齊,後者表示越界訪問,這相當於硬異常的異常號,但是豐富得多。

就前述的SEH域而言,由此而引起的效果與硬體異常相同,CPU也會轉入_SEH_HANDLE{}裡面。

熟悉C++的讀者可能會聯想到throw語句,實際上也確實是同一回事。

如果ProbeForRead()沒有檢查出什麼問題,前面的第二個語句是“SafeMaximumSize = *MaximumSize”,這是從使用者空間讀取資料寫入系統空間。這裡寫入系統空間不會有問題,但是讀使用者空間可能會有問題,如果指標MaximumSize所指的頁面無對映就會發生異常。所以要把它放在_SEH_TRY{}裡面。

第三個語句是“MaximumSize = &SafeMaximumSize”,這是對指標MaximumSize進行賦值。作為呼叫引數,這個變數原先在使用者空間堆疊上,CPU因系統呼叫進入核心以後把它複製到了系統空間堆疊上。因而這個賦值操作應該不會引起異常,本可以放在外面,但是放在_SEH_TRY{}裡面也無不可。所以,並非凡是放在_SEH_TRY{}裡面的都必須是可能引起異常的語句。對於不會引起異常的語句,放在_SEH_TRY{}裡面或外面都是一樣。

再看安排在發生異常時加以執行的程式碼、即_SEH_HANDLE{}裡面的程式碼。在這裡只有一個語句,就是對_SEH_GetExceptionCode()的呼叫。顧名思義,這就是獲取具體異常的程式碼,例如STATUS_DATATYPE_MISALIGNMENT、STATUS_ACCESS_VIOLATION等等。然後將獲取的程式碼賦值給變數Status,這就完事了。再往下就是_SEH_END及其後面的if語句了。當然,這裡面也可以有不止一個、甚至很多的語句。

注意變數Status原本已經初始化成STATUS_SUCCESS,而_SEH_TRY{}裡面的程式碼都不會改變它的值;所以只要“!NT_SUCCESS(Status)”為真就一定已經發生過異常,因此這個系統呼叫就出錯返回了,而且所返回的就是所發生異常的程式碼。而根據所返回的值判斷本次系統呼叫是否成功,以及採取什麼措施,那就是使用者軟體的事了。

這裡還要說明一下,並不是所有的異常都會落入這_SEH_HANDLE{}裡面。發生異常時,首先是由核心底層的異常處理程式“認領”和處理,例如缺頁異常就會被其認領並處理,處理完就返回了。即使是不歸其認領處理的異常,也還得看當時是否正在通過除錯工具(debugger)除錯程式,如果是就交由debugger處理。只有不受這二者攔截的異常才會落入_SEH_HANDLE{}。後面讀者將看到,每個SEH域都可以通過一個“過濾函式”檢查本次異常的型別,已決定是否認領。如果存在巢狀的SEH域,則首先要由巢狀在最內層(最底層)的SEH域先作過濾,決定不予認領才會交給上一層SEH域。所以,只有不被攔截、認領,並通過了層層過濾的異常才真正進入本SEH域的_SEH_HANDLE{}。

上面所引的是核心中的程式碼,使用者空間的程式碼同樣也可以利用SEH所提供的功能和機制,其實C++語言中的try{..}catch{…}最終也是利用SEH實現的。

那麼,以_SEH_TRY{}、_SEH_HANDLE{}、以及_SEH_END為程式設計手段的這種SEH機制具體是怎麼實現的呢?這正是本文要加以介紹的內容。從現在起,凡是ReacOS的程式碼均引自其0.3.0版。

先大致介紹一下基本的原理。

從形式上看,由_SEH_TRY{}、_SEH_HANDLE{}、和_SEH_END在程式結構上有點像是條件語句if(){}else{}。可是,用條件語句是實現不了SEH的。這是因為:條件語句所判別的條件必須表現為一個布林量,而對此布林量的測試和相應的程式跳轉只能發生在一個固定的點上,但是_SEH_TRY{}所要保護的卻是一個“域”、一個範圍。誠然,我們可以在程式中放上一個初值為0的全域性量,比方說excepted,如果發生異常就讓底層的異常響應程式將此變數設定成1。但是,總不能讓_SEH_TRY{}裡面的程式每執行完一條指令就來執行基於這個變數的條件語句(例如條件跳轉)吧?怎麼辦呢,辦法是預先設定好一個目標地址,只要發生了異常,就從底層的異常響應程式直接跳轉到預設的目標地址。但是,這樣的跳轉必須發生在返回到因異常而被中斷的程式中之前,因為一旦回到了被中斷的程式,那裡就沒有實現此種跳轉所需的程式碼了。從堆疊的角度看,這是要從內層的“異常框架”跳轉到、而不是返回到外層的SEH框架中。此種垮框架的跳轉稱為“長程跳轉(Long-Jump)”。C語言程式庫中有一對函式setjmp()和longjmp(),就是用來實現長程跳轉的,前者用於設定長程跳轉的目標地址,後者用於實際的跳轉。

不過,這只是單個保護域的異常處理,還不能說是“結構化異常處理”。與單個保護域相連繫的是單個目標地址、即單塊_SEH_HANDLE{}程式碼;但是實際上可能發生的異常卻是多樣的,要在同一塊_SEH_HANDLE{}程式碼中考慮應對所有不同原因的異常顯然不現實。在編寫一段需要受保護的程式碼時,程式設計師一般只能針對這段區域性的程式碼作出估計,就其認為可能會發生的異常安排好應對措施。所以就有了讓保護域巢狀的要求,保護域的巢狀使程式設計師得以將注意力集中在具體的區域性,而又可以從全域性上防止有些異常得不到合適的處理,這與“結構化程式設計”在精神上是一致的。

另一方面,異常既可能發生於系統空間,也可能發生於使用者空間,因此兩個空間都需要有實現SEH域的手段。但是,即使是發生於使用者空間的異常,首先進入的也是核心底層的異常響應程式,從而需要有個將異常提交給使用者空間進行處理的手段。這樣,從核心底層的異常響應/處理程式開始,根據具體情況進入系統空間或使用者空間巢狀在最內層的保護域,再根據具體情況逐層上升到外層保護域,直至窮盡所有預設的保護措施,這就形成了一套完整的異常處理機制而成為一個體系,那才可以說是“結構化異常處理”。可以想像,這麼一套機制的實現並非易事。

為實現結構化異常處理,Windows在系統空間和使用者空間都有一個後進先出的異常處理佇列ExceptionList。為簡化敘述,這裡先從概念上作一說明,實際的實現則還要複雜一點:每當程式進入一個SEH框架時,就把一個帶有長程跳轉目標地址的資料結構掛入相應空間的異常處理佇列,成為其一個節點;在核心中就掛入系統空間的佇列,在使用者空間就掛入使用者空間的佇列。而當離開當前SEH框架時,則從佇列中摘除這資料結構。由於是後進先出佇列,所摘除的一定是最近掛入佇列的資料結構。顯然,佇列中的每一個節點都代表著一個保護域。只要佇列非空,CPU就至少是在某個(最後進入的)保護域中執行。只要佇列中的節點多於一個,後進節點所代表的保護域就一定是巢狀在先進入的保護域內部,而CPU則同時在多個保護域內部執行。所以異常處理佇列本質上是一個堆疊,反映了保護域的層次關係。一般而言,當CPU運行於使用者空間時,系統空間的異常處理佇列應該是空的。

除長程跳轉目標地址外,掛入ExceptionList的資料結構中還可以有兩個函式指標。一個是“過濾(Filter)函式”的指標,這個函式判斷所發生的異常是否就是本保護域所要保護、所要應對的那種異常,如果是才加以認領而執行本SEH域的長程跳轉。另一個是“善後(final)函式”指標,善後函式的目的通常是釋放動態獲取的資源。

說到“善後函式”,這裡有幾個概念需要澄清一下。首先,前面講到_SEH_TRY{}裡面是要加以“保護”的程式碼,但是所謂保護並非讓其不發生異常,而是說要為可能發生的異常準備好應對之道,就好像對於高空作業要在地面上鋪設一張保護網、並準備好應急預案一樣。根據具體的情況,應對之道可簡可繁。前面程式碼中的應對之道就只是獲取異常程式碼,然後使當前的系統呼叫夭折而返回,並把異常程式碼帶回使用者空間。而比較複雜的應對之道,則可能會試圖消除發生異常的原因,例如對於因除數為0而引起的異常就有這樣的可能。既然是應對之道,自然就帶有“善後”的意思,可是這與“善後函式”不同。或許可以說,_SEH_HANDLE{}裡面的程式碼所提供的應對之道是應用層面上的善後、是針對程式主流的善後,而“善後函式”所提供的是輔助性的、技術性的善後。在SEH域巢狀的情況下,這二者有很大的不同。例如,假定在針對頁面異常的SEH域中嵌套了一個針對除數為0的異常,而實際發生的是頁面異常,那麼長程跳轉的目標是上層SEH域的_SEH_HANDLE{},以執行鍼對頁面異常的應對之道,而針對除數為0的應對之道則得不到執行,因為後者所在的函式框架被長程跳轉跨越了。可是,與後者相聯絡的善後函式卻仍須執行,因為後者所在的那個函式可能已經動態分配了某些資源,而函式中本來用於釋放這些資源的程式碼卻被跳過了。

這樣,簡而言之,當發生異常時,異常響應程式就(按後進先出的次序)依次考察相應ExceptionList中的各個節點並執行其過濾函式(如果有的話),如果過濾函式認為這就是本保護域所針對的異常、或預設為相符而無需過濾,就執行本保護域的長程跳轉,進入本SEH域的_SEH_HANDLE{}裡面的程式碼。而對於被跨越的各個內層SEH域,則執行其善後函式(如果有的話)。

應該說,這是設計得很好的一種方案。明白了基本的原理以後,下面就可以看具體的程式碼了。

在ReactOS的程式碼中,_SEH_TRY、_SEH_HANDLE、以及_SEH_END都是巨集定義。不過,在ReactOS的0.3.0版中,這些巨集操作的定義有兩套。其中之一依賴於較新版本的C編譯對__try、__except、__finally等較新語言成分的支援,由C編譯在編譯的時候自動生成相應的細節;另一種則不依賴於C編譯對這些新語言成分的支援。這二者之間的關係有點像是高階語言與組合語言之間的關係。對於深入理解SEH而言,後者反倒有助於讀者更直觀、更清晰地看到此項機制的原理和具體實現。反過來,搞明白了SEH機制在採用“樸素”C編譯工具時的實現,也就明白了在較新版本的C編譯中__try、__except、__finally這些新語言成分的原理。

先看_SEH_TRY的定義:

#define _SEH_TRY /

{                                                                            /

  _SEH2_INIT_CONST int _SEH2TopTryLevel = (_SEHScopeKind != 0);                /

  _SEHPortableFrame_t * const _SEH2CurPortableFrame = _SEHPortableFrame;          /

  {                                                                          /

   static const int _SEHScopeKind = 0;                                            /

   register int _SEH2State = 0;                                                   /

   register int _SEH2Handle = 0;                                                  /

   _SEHFrame_t _SEH2Frame;                                                  /

   _SEHTryLevel_t _SEH2TryLevel;                                              /

   _SEHPortableFrame_t * const _SEHPortableFrame =                               /

        _SEH2TopTryLevel ? &_SEH2Frame.SEH_Header : _SEH2CurPortableFrame;     /

   (void)_SEHScopeKind;                                                       /

   (void)_SEHPortableFrame;                                                    /

   (void)_SEH2Handle;                                                         /

                                                                             /

   for(;;)                                                                     /

   {                                                                         /

    if(_SEH2State)                                                             /

    {                                                                        /

     for(;;)                                                                   /

     {                                                                       /

      {

這裡的變數_SEHScopeKind有個很特別的作用,留待後面再作介紹。先看這裡所涉及的幾種資料結構,包括_SEHFrame_t、_SEHPortableFrame_t、和_SEHTryLevel_t。

實際上還有一種資料結構_SEHRegistration_t,是這裡不能直接看到的,我們從這個資料結構開始:

typedef struct __SEHRegistration

{

  struct __SEHRegistration * SER_Prev;

  _SEHFrameHandler_t SER_Handler;

}_SEHRegistration_t;

這就是要掛入異常處理佇列ExceptionList的資料結構,其中的指標SER_Prev用來構成後進先出的異常處理佇列。而SER_Handler則是個函式指標,其型別定義如下:

typedef int (__cdecl * _SEHFrameHandler_t)

(struct _EXCEPTION_RECORD *, void *, struct _CONTEXT *, void *);

這個函式稱為“框架處理函式”,對於一個具體節點的處理都是由這個函式實施的。如果節點所代表的只是單個SEH框架,那麼這個函式要處理的就只是單個SEH框架,例如呼叫其過濾函式以確定是否認領,以及認領後呼叫其所有內層SEH域的善後函式以釋放資源,執行本SEH域的長程跳轉等等。而若節點所代表的是一個區域性SEH框架棧,則節點內部還有一個區域性的佇列,這個函式就要有處理一個區域性SEH框架棧的能力。在0.3.0版的ReactOS程式碼中,如前所述,ExceptionList佇列中的節點代表著一個區域性SEH框架棧。而在以前的程式碼中則只代表單個SEH框架。讀者以後將會看到,SEH機制中還使用著別的框架處理函式。可見,“框架處理函式”指標的使用帶來了實現上的靈活性。

不過_SEHRegistration_t結構只是_SEHPortableFrame_t結構內部的一個成分:

typedef struct __SEHPortableFrame

{

  _SEHRegistration_t  SPF_Registration;

  unsigned long  SPF_Code;

  _SEHHandler_t  SPF_Handler;

  _SEHPortableTryLevel_t  *SPF_TopTryLevel;

}_SEHPortableFrame_t;

其第一個成分SPF_Registration就是_SEHRegistration_t資料結構。所以,獲得了指向前者的指標,也就同時獲得了指向其所在_SEHPortableFrame_t資料結構的指標。

另一個成分SPF_Code是異常程式碼,其取值範圍實際上是狀態程式碼的一個子集。而狀態程式碼的集合相當大,其完整的定義見於ntstatus.h。例如STATUS_SUCCESS定義為0,STATUS_GUARD_PAGE_VIOLATION定義為0x80000001,STATUS_UNSUCCESSFUL定義為0xC0000001,STATUS_ACCESS_VIOLATION定義為0xC0000005,等等。

_SEHPortableFrame_t又是_SEHFrame_t資料結構內部的一個成分:

typedef struct __SEHFrame

{

  _SEHPortableFrame_t  SEH_Header;

  void  *SEH_Locals;

}_SEHFrame_t;

可見,這實際上只是在_SEHPortableFrame_t結構的基礎上附加了一個指標SEH_Locals,用來指向一個緩衝區。顧名思義,這個緩衝區用來傳遞一些與區域性SEH框架棧有關的附加資料。所以,關鍵性的資料結構其實還是_SEHPortableFrame_t。

回到_SEHPortableFrame_t資料結構,其中SPF_Handler又是個函式指標,其型別定義為:

typedef void  (__stdcall * _SEHHandler_t) (struct __SEHPortableTryLevel *);

這個指標所指向的函式,我們不妨稱之為“實施函式”,因為長程跳轉就是由這個函式實施的。但是,針對具體異常所實施的應對之道並不非得是長程跳轉,也有可能是別的措施。為每個節點都配備一個實施函式,目的就在於為具體的實現提供靈活性,但是實際上都使用著同一個函式。

_SEHPortableFrame_t結構中的SPF_TopTryLevel則是一個結構指標,指向一個_SEHPortableTryLevel_t資料結構的佇列:

typedef struct __SEHPortableTryLevel

{

  struct __SEHPortableTryLevel * SPT_Next;

  const _SEHHandlers_t * SPT_Handlers;

}_SEHPortableTryLevel_t;

這裡的第一個成分SPT_Next是結構指標,用來形成_SEHPortableTryLevel_t結構的後進先出佇列,這個佇列構成一個區域性SEH框架棧,而佇列中的每個_SEHPortableTryLevel_t結構則代表著具體的SEH框架。

第二個成分SPT_Handlers是指標,指向一個_SEHHandlers_t資料結構。這是由兩個函式指標構成的資料結構:

typedef struct __SEHHandlers

{

 _SEHFilter_t SH_Filter;

 _SEHFinally_t SH_Finally;

}_SEHHandlers_t;

這裡SH_Filter和SH_Finally都是函式指標。前者用來指向一個“過濾函式”,後者用來指向一個“善後函式”。

也就是說,每個_SEHHandlers_t結構、從而每個_SEHPortableTryLevel_t結構,給定了一對過濾函式和善後函式。

同時,_SEHPortableTryLevel_t資料結構又是_SEHTryLevel_t結構中的一個成分:

typedef struct __SEHTryLevel

{

 _SEHPortableTryLevel_t  ST_Header;

 _SEHJmpBuf_t  ST_JmpBuf;

}_SEHTryLevel_t;

顯然,這是在_SEHPortableTryLevel_t資料結構的基礎上加上了一個_SEHJmpBuf_t資料結構,這就是用於長程跳轉的。前面講到了長程跳轉的目標地址,那只是就概念上而言;實際需要的不僅僅是一個目標地址,而是一個包括各暫存器內容在內的“現場”映像,這是在設定長程跳轉目標的時候儲存下來的。

這樣,每個_SEHTryLevel_t資料結構實際上給定了一個三元組,即:過濾函式、善後函式、和長程跳轉目標。

但是注意雖然每個_SEHTryLevel_t結構各有自己的過濾函式、善後函式、和長程跳轉目標,但是整個節點、即_SEHFrame_t結構、卻只有一個實施函式(見函式指標SPF_Handler)。所以,同一個區域性SEH框架棧中各SEH框架實施長程跳轉的方式都是一樣的。如果這方面不同,就不能合在一起。

現將這些資料結構的關係和作用總結如下:

1.         ExceptionList佇列中各節點的資料結構是_SEHRegistration_t。

2.         _SEHRegistration_t的外層結構_SEHFrame_t代表著一個形式巢狀的區域性SEH框架棧,所以每個節點代表著一個區域性SEH框架棧。

3.         每個節點有一個函式指標SPF_Handler,提供一個實施函式。

4.         _SEHFrame_t的主體是_SEHPortableFrame_t。

5.         _SEHPortableFrame_t結構內部的指標SPF_TopTryLevel指向一個區域性的_SEHPortableTryLevel_t結構佇列。

6.         _SEHPortableTryLevel_t的外層結構_SEHTryLevel_t代表著一個具體的SEH框架,每個框架給定了一個包括過濾函式、善後函式、和長程跳轉目標的三元組。

每個區域性SEH框架棧的佇列中可以有不止一個的_SEHPortableTryLevel_t,所以就可以有不止一個這樣的三元組。

所以,ExceptionList構成一個實質的、全域性的SEH框架棧,佇列中的每一個節點都是一個形式的、區域性的SEH框架棧,但是各節點之間沒有形式上的連繫。全域性SEH框架棧可以為空,即ExceptionList佇列為空,表示沒有設定任何的SEH域。但是區域性SEH框架棧不能為空,一個區域性SEH框架棧中至少有一個SEH框架,否則這個節點就不應該存在了。前面從概念上敘述時說“實際的實現則還要複雜一點”,就是因為這兩個佇列的劃分。這種劃分是0.3.0版中才有的,以前所有的SEH框架都直接在ExceptionList佇列中,整個佇列就是一個SEH框架棧,而沒有實質與形式之分,不像現在這樣分成兩層。

回到_SEH_TRY的程式碼,這裡為_SEHFrame_t資料結構_SEH2Frame分配了空間,裡面就包含著作為其結構成分的_SEHPortableFrame_t資料結構。同樣,為_SEHTryLevel_t資料結構_SEH2TryLevel分配了空間,裡面就包含著作為其結構成分的_SEHPortableTryLevel_t結構。其餘的程式碼等一下與_SEH_HANDLE和_SEH_END合在一起看會更加清晰。

上面是_SEH_TRY的定義,再看_SEH_HANDLE的定義:

#define _SEH_HANDLE  /

    _SEH_EXCEPT(_SEH_STATIC_FILTER(_SEH_EXECUTE_HANDLER))

顯然,_SEH_EXCEPT本身也是一個巨集操作,它的引數是另一個巨集操作_SEH_STATIC_FILTER的運算結果:

/* Declares a static filter */

#define _SEH_STATIC_FILTER(ACTION_)  ((_SEHFilter_t)((ACTION_) + 2))

其引數ACTION_的取值範圍為:

#define _SEH_CONTINUE_EXECUTION (-1)

#define _SEH_CONTINUE_SEARCH (0)

#define _SEH_EXECUTE_HANDLER (1)

所以,從效果上看,這只是把ACTION_的取值範圍-1到+1調整成了1到3。可是為什麼要作這樣的調整呢?這是因為這個數值的型別_SEHFilter_t實際上是個函式指標:

typedef long  /

(__stdcall *_SEHFilter_t)( struct _EXCEPTION_POINTERS *, struct __SEHPortableFrame *);

作為函式指標,數值0會引起歧義,因為空指標的值也是0。所以對ACTION_的值進行這樣的調整是可以理解的。當然,這也並非唯一可行的做法,例如直接就把上述三個常數定義為1、2、3應該也無不可。可是函式指標怎麼會有1、2、3這樣的數值呢?其實這是對函式指標的變通使用。巨集操作_SEH_EXCEPT()的引數可以是個真正的函式指標,那就是由程式設計師提供的過濾函式,但是也可以不提供特別的過濾函式而採用由SEH機制提供的三種處理方式之一,所以1、2、3起著類似於指令程式碼的作用,而這裡選擇的是_SEH_EXECUTE_HANDLER,實際上是1,調整以後就成了3。那麼這三種處理方式到底是什麼呢?這裡簡單提一下:

l         _SEH_CONTINUE_EXECUTION表示應該忽略本次異常,原來在幹什麼就繼續幹什麼。或者本次異常已被認領並且解決,現在可以返回被中斷的程式了。

l         _SEH_CONTINUE_SEARCH表示不予認領,應該繼續考察佇列中的下一個節點、即上一層SEH域。

l         _SEH_EXECUTE_HANDLER表示認領本次異常,應實施本SEH域的長程跳轉。

如果提供了過濾函式,那麼過濾函式的返回值也應該是這三者之一。

於是,在_SEH_HANDLE中,巨集操作_SEH_EXCEPT()的引數就是3,而_SEH_EXCEPT()本身的定義則是:

#define _SEH_EXCEPT(FILTER_) /

      }                                                                    /

      break;                                                                /

     }                                                                     /

     _SEH2_ASSUME(_SEH2Handle == 0);                                      /

     break;                                                                 /

    }                                                                      /

    else                                                                    /

    {                                                                      /

     _SEH_DECLARE_HANDLERS((FILTER_), 0);                             /

     _SEH2TryLevel.ST_Header.SPT_Handlers = &_SEHHandlers;                   /

     if(_SEH2TopTryLevel)                                                   /

     {                                                                     /

      if(&_SEHLocals != _SEHDummyLocals)                                   /

        _SEH2Frame.SEH_Locals = &_SEHLocals;                               /

      _SEH2Frame.SEH_Header.SPF_Handler = _SEHCompilerSpecificHandler;      /

      _SEHEnterFrame(&_SEH2Frame.SEH_Header, &_SEH2TryLevel.ST_Header);   /

     }                                                                     /

     else                                                                   /

      _SEHEnterTry(&_SEH2TryLevel.ST_Header);                              /

                                                                           /

     if((_SEH2Handle = _SEHSetJmp(_SEH2TryLevel.ST_JmpBuf)) == 0)            /

     {                                                                     /

      _SEH2_ASSUMING(++ _SEH2State);                                     /

      _SEH2_ASSUME(_SEH2State != 0);                                      /

      continue;                                                             /

     }                                                                    /

     else                                                                  /

     {                                                                    /

      break;                                                               /

     }                                                                    /

    }                                                                     /

    break;                                                                 /

   }                                                                      /

   _SEHLeave();                                                           /

   if(_SEH2Handle)                                                         /

   {

這裡有幾個重要的巨集操作。首先是_SEH_DECLARE_HANDLERS:

# define _SEH_DECLARE_HANDLERS(FILTER_, FINALLY_) /

  _SEHHandlers_t _SEHHandlers = { (0), (0) };                                  /

  _SEHHandlers.SH_Filter = (FILTER_);                                          /

  _SEHHandlers.SH_Finally = (FINALLY_);

顯然,這裡建立了一個_SEHHandlers_t資料結構,即_SEHHandlers,為其分配空間,並設定其過濾函式和善後函式指標,前者是數值3,後者則為0,即沒有善後函式。

有了_SEHHandlers_t資料結構,並將其地址設定到前面的_SEHPortableTryLevel_t資料結構中以後,就是巨集操作_SEHEnterFrame()或_SEHEnterTry(),分別定義為函式_SEHEnterFrame_f()和_SEHEnterTry_f()。具體取決於這個SEH域是否形式上巢狀在別的SEH域內部。如果形式上是獨立的,那就是一個區域性SEH框架棧的“頂層”,那是要掛到ExceptionList佇列中的。而若是形式上巢狀在另一個SEH域的內部,則那個作為宿主的SEH框架必然已經在ExceptionList佇列中,所以只是掛到那個節點的_SEHPortableTryLevel_t結構佇列中。

現在可以暫時中斷一下,解釋前面程式碼中_SEHScopeKind的作用了。在檔案framebased.h中,有這麼一行程式碼:

static const int _SEHScopeKind = 1;

注意這是static。需要用到SEH機制的C程式碼檔案必須包含這個.h檔案,因為_SEH_TRY的定義也在這個檔案中。這樣,就相當於具體的C程式碼檔案中有了這麼一個靜態變數。

再看_SEH_TRY程式碼的開頭幾行:

{                                                                          /

  _SEH2_INIT_CONST int _SEH2TopTryLevel = (_SEHScopeKind != 0);              /

  _SEHPortableFrame_t * const _SEH2CurPortableFrame = _SEHPortableFrame;         /

  {                                                                         /

   static const int _SEHScopeKind = 0;                                           /

   . . . . . .

注意每個左花括號都代表著(堆疊上)一個新的框架。所以這裡第一次出現的_SEHScopeKind是對整個C程式碼檔案中的這個靜態變數的引用,因為在本框架中並沒有定義這麼一個變數,而這個變數的值是1。這樣,變數_SEH2TopTryLevel就被賦值為TRUE。而第二次出現_SEHScopeKind,則是在內層框架中定義了一個靜態變數,並直接初始化為0。現在設想在_SEH_TRY{}的內部又形式嵌套了一個_SEH_TRY{}。對於嵌在內層的_SEH_TRY{},其第一次引用的_SEHScopeKind是初始化為0的那個靜態變數,而不是整個檔案的那個同名靜態變數,因為前者定義於最靠近引用處的那層框架中。於是,內層框架的_SEH2TopTryLevel就被賦值為FALSE了。顯然,只要是形式上巢狀在別的_SEH_TRY{}內部,其_SEH2TopTryLevel就總是FALSE。而若不是形式上巢狀在別的_SEH_TRY{}內部,例如在另一個函式中、甚至在另一個檔案中,則其引用的_SEHScopeKind就是整個檔案的靜態變數_SEH2TopTryLevel,所以其_SEH2TopTryLevel為TRUE。

而_SEH2TopTryLevel的值,則被用來作為呼叫_SEHEnterFrame()或_SEHEnterTry()的依據,也就是進入ExceptionList還是進入其中當前節點的區域性SEH框架佇列的依據。所謂“TopTryLevel”,是指一個區域性SEH框架棧的頂層。

我們在這裡主要關心的是_SEHEnterFrame()、即_SEHEnterFrame_f():

void _SEH_FASTCALL _SEHEnterFrame_f (_SEHPortableFrame_t * frame,

                                            _SEHPortableTryLevel_t * trylevel)

{

 /* ASSERT(frame); */

 /* ASSERT(trylevel); */

 frame->SPF_Registration.SER_Handler = _SEHFrameHandler;

 frame->SPF_Code = 0;

 frame->SPF_TopTryLevel = trylevel;

 trylevel->SPT_Next = NULL;

 _SEHRegisterFrame(&frame->SPF_Registration);

}

這個函式把給定的_SEHPortableFrame_t資料結構通過其內部成分SPF_Registration、即_SEHRegistration_t資料結構、掛入系統空間的ExceptionList,並設定好節點的框架處理函式指標SER_Handler、使其指向_SEHFrameHandler()。這個函式的程式碼是與SEH域節點的資料結構和處理方式配套的,適用於所有通過_SEH_TRY、_SEH_HANDLE、和_SEH_END設定的SEH域。

注意在此之前已把_SEHPortableFrame_t結構中的函式指標SPF_Handler設定成指向_SEHCompilerSpecificHandler(),這就是預設的實施函式。

掛入異常處理佇列是由_SEHRegisterFrame()完成的:

__SEHRegisterFrame:

 mov ecx, [esp+4]

 mov eax, [fs:0]

 mov [ecx+0], eax

 mov [fs:0], ecx

 ret

Windows核心對於段暫存器FS有特殊的設定和使用,當CPU運行於系統空間時就使fs:0指向當前CPU的KPCR資料結構。每個CPU都有一個KPCR資料結構,所以在多處理器系統中就有不止一個的KPCR資料結構,當運行於系統空間時每個CPU的fs:0都指向自己的KPCR資料結構。而KPCR結構的第一個成分是KPCR_TIB資料結構,KPCR_TIB的第一個成分則是VOID指標ExceptionList。不言而喻,這是一個由_SEHRegistration_t資料結構連結而成的異常處理佇列。

所謂“登記”,就是把一個_SEHRegistration_t資料結構、即一個區域性SEH框架棧、插入這個鏈的頭部。而函式的呼叫引數就是一個_SEHRegistration_t結構指標,結構中的第一個成分是指標SER_Prev,所以[ecx+0]就是這個指標。注意這個指標的名稱SER_Prev容易把人搞糊塗。從程式碼中看,通過引數傳遞下來的資料結構顯然是插入了ExceptionList的頭部,因為經過這些操作之後fs:0指向了新的資料結構。這說明,這個連結串列的本質是個堆疊,有著“後進先出”的性質。指標的名稱SER_Prev提示我們:在佇列中掛在後面的節點倒是代表著“Previous”即先前的SEH框架、實際上是上一層SEH框架。事實上,只有在SEH域形式上不巢狀而實質巢狀的條件下,這個佇列中才會有不止一個的節點。

順便還要提一下,在使用者空間也有類似的佇列。每個執行緒在使用者空間都有個TEB,TEB資料結構中的第一個成分是NT_TIB資料結構,這裡面的第一個成分即是指標ExceptionList。而且,當CPU運行於使用者空間時,fs:0就是指向當前執行緒的TEB,實際上也就是ExceptionList。

熟悉裝置驅動和中斷處理的讀者可能感覺到,這跟登記一箇中斷處理程式頗為相似。事實上也確實如此,只不過這是在為可能發生的異常、而不是中斷、做好應對的準備。

但是至此還沒有設定長程跳轉的目標。這是由隨後的_SEHSetJmp()完成的:

_SEHSetJmp:

[email protected]:

 ; jump buffer

 mov eax, [esp+4]

 ; program counter

 mov ecx, [esp+0]

 ; stack pointer

 lea edx, [esp+8]

 ; fill the jump buffer

 mov [eax+0], ebp

 mov [eax+4], edx

 mov [eax+8], ecx

 mov [eax+12], ebx

 mov [eax+16], esi

 mov [eax+20], edi

 xor eax, eax

 ret 4

呼叫這個函式時的實際引數是_SEH2TryLevel.ST_JmpBuf,這是一個數組、即“跳轉緩衝區”的起始地址,這裡讓暫存器EAX指向這個陣列。同時,又讓ECX持有返回地址,而讓EDX持有呼叫這個函式前夕的堆疊指標,然後將這二者連同EBP、EBX、ESI、EDI的內容都儲存在跳轉緩衝區中。這樣,跳轉緩衝區的內容就構成了呼叫_SEHSetJmp()前夕的(簡化的)現場,其中的返回地址就是長程跳轉的目標地址,所以跳轉緩衝區的內容就代表著跳轉目標。

值得注意的是,這個函式返回的值、即返回時EAX的內容一定是0,所以對於返回值為0的判定必定為真。這決定了在前面程式碼中的if語句必然會進入其測試條件為真的部分。所以,這個0值實際上標誌著一條路徑,說明這是從執行_SEHSetJmp()而來,等一下我們就可以進一步看到它的意義。

這裡順便也看一下_SEHEnterTry_f()的程式碼。如前所述,其目的是為一個區域性SEH框架棧新增一個SEH框架。ExceptionList連結串列中的節點代表著一個區域性SEH框架棧,節點內部都有一個_SEHPortableTryLevel_t資料結構的連結串列,而連結串列中的每一個數據結構則代表著一個具體的SEH框架。

void _SEH_FASTCALL _SEHEnterTry_f(_SEHPortableTryLevel_t * trylevel)

{

  _SEHPortableFrame_t * frame;

  frame = _SEH_CONTAINING_RECORD (_SEHCurrentRegistration(),

                            _SEHPortableFrame_t,  SPF_Registration);

  trylevel->SPT_Next = frame->SPF_TopTryLevel;

  frame->SPF_TopTryLevel = trylevel;

}

引數trylevel是個_SEHPortableTryLevel_t結構指標,這就是需要插入佇列的資料結構。

如果受保護程式碼的執行順利完成、而並未發生異常,就要從其所在的區域性SEH框架棧中撤銷當前的框架,如果這是其中的最後一個SEH框架則還要從ExceptionList中摘除相應的節點,這是由巨集操作_SEHLeave()、實際上是函式_SEHLeave_f()完成的:

void _SEH_FASTCALL _SEHLeave_f(void)

{

  _SEHPortableFrame_t * frame;

  _SEHPortableTryLevel_t * trylevel;

  frame = _SEH_CONTAINING_RECORD (_SEHCurrentRegistration(),

                                _SEHPortableFrame_t, SPF_Registration);

  /* ASSERT(frame); */

  trylevel = frame->SPF_TopTryLevel;

  /* ASSERT(trylevel); */

  if(trylevel->SPT_Next)

    frame->SPF_TopTryLevel = trylevel->SPT_Next;

  else

    _SEHUnregisterFrame();

}

實際要摘除的總是最後一個SEH框架,這是指標ExceptionList所指節點中的最後一個_SEHPortableTryLevel_t資料結構。函式_SEHCurrentRegistration()獲取ExceptionList指標的內容。如果所指節點內部的佇列中有不止一個的_SEHPortableTryLevel_t資料結構,就只是從這佇列中摘掉最後進入的資料結構。如果只剩下一個_SEHPortableTryLevel_t資料結構,那就要通過_SEHUnregisterFrame()從ExceptionList中摘除整個節點、即撤消整個區域性SEH框架棧的登記:

__SEHUnregisterFrame:

 mov ecx, [fs:0]

 mov ecx, [ecx+0]

 mov [fs:0], ecx

 ret

這幾行程式碼就不需要解釋了。但是另一個事卻很值得一提,那就是,當從ExceptionList中摘除一個節點時,並不需要釋放這個節點所佔的空間。因為這些節點其實都在堆疊上,一旦其所在的框架因從函式呼叫返回或長程跳轉而不復存在,這些資料結構所佔的空間也就自然釋放了。同樣,從一個節點的SPF_TopTryLevel佇列中摘除一個節點、即_SEHPortableTryLevel_t資料結構的時候,也不需要釋放。

最後是_SEH_END:

#define _SEH_END /

   }                                                                     /

  }                                                                      /

 }

於是,經過編譯工具的替換處理以後,本文開頭處那個if語句裡面跟SEH有關的程式、即整個_SEH_TRY{} _SEH_HANDLE{} _SEH_END的過程就變成了這樣:

{                                                                            /

  _SEH2_INIT_CONST int _SEH2TopTryLevel = (_SEHScopeKind != 0);                /

  _SEHPortableFrame_t * const _SEH2CurPortableFrame = _SEHPortableFrame;         /

  {                                                                          /

    static const int _SEHScopeKind = 0;                                            /

    register int _SEH2State = 0;                                                  /

    register int _SEH2Handle = 0;                                                 /

    _SEHFrame_t _SEH2Frame;                                                 /

    _SEHTryLevel_t _SEH2TryLevel;                                             /

    _SEHPortableFrame_t * const _SEHPortableFrame =                              /

        _SEH2TopTryLevel ? &_SEH2Frame.SEH_Header : _SEH2CurPortableFrame;     /

    (void)_SEHScopeKind;                                                      /

    (void)_SEHPortableFrame;                                                   /

    (void)_SEH2Handle;                                                        /

                                                                             /

    for(;;)                                                                    /

    {                                                                        /

      if(_SEH2State)                                                           /

      {                                                                      /

        for(;;)                                                                /

        {                                                                    /

          {

            {

              .ProbeForRead(MaximumSize, sizeof(LARGE_INTEGER), sizeof(ULONG));

              SafeMaximumSize = *MaximumSize;

              MaximumSize = &SafeMaximumSize;

            }

          }                                                                 /

          break;                                                             /

        }                                                                   /

        _SEH2_ASSUME(_SEH2Handle == 0);                                    /

        break;                                                               /

      }                                                                     /

      else                                                                   /

      {                                                                     /

        _SEH_DECLARE_HANDLERS((FILTER_), 0);                             /

        _SEH2TryLevel.ST_Header.SPT_Handlers = &_SEHHandlers;                 /

        if(_SEH2TopTryLevel)                                                 /

        {                                                                   /

          if(&_SEHLocals != _SEHDummyLocals)                                /

              _SEH2Frame.SEH_Locals = &_SEHLocals;                          /

          _SEH2Frame.SEH_Header.SPF_Handler = _SEHCompilerSpecificHandler;   /

          _SEHEnterFrame(&_SEH2Frame.SEH_Header, &_SEH2TryLevel.ST_Header);/

        }                                                                   /

        else                                                                 /

          _SEHEnterTry(&_SEH2TryLevel.ST_Header);                           /

                                                                            /

        if((_SEH2Handle = _SEHSetJmp(_SEH2TryLevel.ST_JmpBuf)) == 0)          /

        {                                                                   /

          _SEH2_ASSUMING(++_SEH2State);                                   /

          _SEH2_ASSUME(_SEH2State != 0);                                    /

          continue;                                                           /

        }                                                                    /

        else                                                                  /

        {                                                                    /

          break;                                                              /

        }                                                                    /

      }                                                                      /

      break;                                                                  /

    }                                                                        /

    _SEHLeave();                                                             /

    if(_SEH2Handle)                                                          /

    {

      Status = _SEH_GetExceptionCode();

    }                                                                        /

  }                                                                          /

}

開始時_SEH2State為0,所以在進入外層for語句後的第一輪迴圈中執行的是if語句的else部分。在這裡“登記”了本保護域的資料結構,並執行_SEHSetJmp()。

由於_SEHSetJmp()的返回值為0,因而其所在if語句的判定條件得到滿足,於是_SEH2State的值遞增成1,並執行continue語句而開始外層for語句的第二輪迴圈。注意_SEHSetJmp()的返回值賦給了_SEH2Handle,所以_SEH2Handle的值也是0。

這一次if(_SEH2State)的條件得到滿足,所以就進入了內層的for迴圈,這裡要執行的就是受保護的程式碼。雖說在形式上這是個無限迴圈,實際上裡面的程式碼卻只執行一次,因為後面馬上就有個break語句。所以,這實際上與do{}while(0)的效果是一樣的。

我們先假定這裡受保護的程式碼順利得到執行、而並未發生異常。執行完這些程式碼以後,就因為break語句而跳出了內層的for迴圈。然後緊接著又是一個break語句,這次跳出的是外層的for迴圈。於是,就到了_SEHLeave()。

執行完_SEHLeave()以後,由於_SEH2Handle的值是0,最後這if語句裡面的程式碼就不會得到執行。這樣,從效果上看,就是保護域中的程式碼得到了正常執行。

那麼,要是在執行保護域中的程式碼時發生了異常又會怎樣呢?如果發生異常,核心中底層的異常響應程式會依次檢查ExceptionList中的資料結構,具體的過程將在下一篇漫談中介紹,如果某個節點中的資料結構表明這次異常正是它要保護的,就會通過_SEHLongJmp執行一次長程跳轉:

_SEHLongJmp:

[email protected]:

 ; return value

 mov eax, [esp+8]

 ; jump buffer

 mov ecx, [esp+4]

 ; restore the saved context

 mov ebp, [ecx+0]

 mov esp, [ecx+4]

 mov edx, [ecx+8]

 mov ebx, [ecx+12]

 mov esi, [ecx+16]

 mov edi, [ecx+20]

 jmp edx

這個函式有兩個引數,第一個就是跳轉緩衝區指標,第二個是數值1。注意第二個引數被置入了EAX,此後EAX的內容一直沒有改變,一直到通過jmp指令實現長程跳轉、即跨堆疊框架的跳轉。所以,在執行長程跳轉的時候EAX的內容為1。由於相應的_SEHSetJmp()是在if語句中執行的,長程跳轉的目標地址就是_SEHSetJmp()當時的返回地址,所以jmp指令以後就是if語句中檢測該函式返回值的指令。這樣,對於if語句而言,長程跳轉的效果就好像是剛從_SEHSetJmp()返回一樣,所不同的是此時的“返回值”是1而不是0,從而將這兩條路線區分開來。

於是,當發生異常而跳轉到這裡的時候就自然會進入它的else部分,並且_SEH2Handle的值為1,而else部分的break語句則使CPU跳出外層的for迴圈。然後先通過_SEHLeave()撤銷當前SEH框架的登記,接著因為_SEH2Handle為1而進入了這裡的if語句裡面,這就是_SEH_HANDLE{}裡面的程式碼。在前面所引的例子中,這就是對_SEH_GetExceptionCode()的呼叫。

這樣,一旦登記了一個SEH框架的異常處理,就好像為隨後的危險動作佈下了一個保護網,萬一發生異常、摔下來也有個應對之道。而一旦CPU平安到達_SEHLeave(),撤銷了登記,這保護網就撤掉了。

讀者在下一篇漫談中還會看到,在實施長程跳轉的前夕,SEH機制會摘除ExceptionList佇列中(以及所在節點的區域性佇列中)目標節點之前的所有節點、並依次執行它們的善後函式。這個過程稱為“展開(Unwinding)”。這些節點代表著堆疊上巢狀在目標SEH框架內部的各層SEH框架;而且這些節點(作為資料結構)本來就存在於堆疊上的這些框架中。一旦實施長程跳轉,這些框架就不復存在、這些節點就失去意義了。“皮之不存毛將焉附”,當然應該把這些節點從連結串列中摘除。不過,只有在存在過濾函式的條件下,才可能會有“目標節點之前的”節點,否則目標節點必定是連結串列中的第一個節點,代表著最後進入的SEH框架。

前面所引的例子中沒有提供過濾函式,這是因為巨集操作_SEH_HANDLE的定義為:

#define _SEH_HANDLE  /

    _SEH_EXCEPT(_SEH_STATIC_FILTER(_SEH_EXECUTE_HANDLER))

這裡的“_SEH_STATIC_FILTER(_SEH_EXECUTE_HANDLER)”實際上可以是一個函式名,這就是過濾函式。所以,如果不用_SEH_HANDLE、而直接引用_SEH_EXCEPT(),那就可以提供過濾函數了。例如下面是取自NtAddAtom()的一段程式碼:

NTSTATUS NTAPI

NtAddAtom(IN PWSTR AtomName, IN ULONG AtomNameLength, OUT PRTL_ATOM Atom)

{