2011年10月6日星期四

[轉載]SEH分析筆記(X86篇)一


作 者: boxcounter
時 間: 2011-10-05,12:46:17
鏈接: http://bbs.pediy.com/showthread.php?t=140970

[不介意轉載,但請註明出處boxcounter.com
     附件裡有本文的原始稿,一樣的內容,更好的高亮和排版。分別是 html 和 rtl 格式。
     後面的部分代碼可能會因為自動換行變得很亂,需要的朋友手動複製到自己的代碼編輯器就可以正常顯示了]

  這兩天琢磨了下SEH,這裡記錄一下自己琢磨的一些心得。
  SEH 這個概念我就不囉嗦了,任何一個介紹SEH 的資料都有講。我主要記錄一些自己的理解。可能有一些概念理解的不夠清晰,有一些說法比較狹隘,歡迎看到本文的朋友一起討論、修正,非常感謝。
  首先,SEH 是針對於異常的一種處理機制,這個異常分為硬件異常和軟件異常,這裡所說的硬件異常是狹義的異常,也就是CPU 產生的異常。比如除零操作,CPU 執行除零操作時候,會自主啟動異常處理機制。軟件異常,就是程序模擬的異常,比如調用RaiseException 函數。軟件異常是可以隨意觸發的,windows 系統內部遇到問題會觸發,開發人員高興了也可以觸發。
  拋出了問題,就要有解決方案。那這麼多問題和解決方案,如何管理呢? windows 系統當仁不讓的提供了它管理方案—— SEH。我看一些資料有詳細的討論SEH 的確切含義,這裡我不參與討論,而只是簡單的理解為“系統提供的異常處理機制,以及編譯器對其進行增強的部分”。

  來說說系統提供的異常處理機制。
  windows 提供的異常處理機制實際上只是一個簡單的框架,一般情況下開發人員都不會直接用到。咱通常所用的異常處理(比如C++ 的throw、try、catch)都是編譯器在系統提供的異常處理機制上進行加工了的增強版本。這裡先拋開增強版的不提,繼續說原始版本。
  原始版本的機制很簡單:誰都可以觸發異常,誰都可以處理異常(只要它能看得見)。但是不管是觸發還是處理都得先登記。系統把這些登記信息保存在一個鍊錶裡,並且這個鍊錶保存在線程的數據結構裡。也就是說,異常所涉及的一些行為都是線程相關的。比如,線程T1 觸發的異常就只能由線程T1 來處理,其他線程根本就不知道T1 發生了什麼事,更不會狗拿耗子。
  等登記完畢後,線程就可以拋出或處理異常了,系統也可以做相應的管理工作了。 (這裡囉嗦一句,系統提供的SEH 其實是一個針對於“觸發異常-解決異常”的管理機制,系統自身是不提供任何具體異常的解決方案的。解決方案還是要由用戶自身來提供(增強版裡編譯器也會來提供解決方案,來幫“不負責”的程序猿擦屁股,這是後話))
  系統提供的管理工作簡單來說包括(但不限於):找到觸發異常的線程的異常處理鍊錶(前頭登記的那個),然後按照規則(具體的規則後續再說)對該異常進行分發,根據分發後的處理結果再進行下一步的分發或者結束處理。
  系統管理所使用的數據結構和宏:

代碼:
    #define EXCEPTION_CHAIN​​_END ((struct _EXCEPTION_REGISTRATION_RECORD * POINTER_32)-1)

    typedef enum _EXCEPTION_DISPOSITION {
        ExceptionContinueExecution,
        ExceptionContinueSearch,
        ExceptionNestedException,
        ExceptionCollidedUnwind
    } EXCEPTION_DISPOSITION;

    typedef struct _EXCEPTION_RECORD {
        DWORD ExceptionCode;
        DWORD ExceptionFlags;
        struct _EXCEPTION_RECORD *ExceptionRecord;
        PVOID ExceptionAddress;
        DWORD NumberParameters;
        ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD;

    typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;

    typedef
    EXCEPTION_DISPOSITION
    (*PEXCEPTION_ROUTINE) (
        IN struct _EXCEPTION_RECORD *ExceptionRecord,
        IN PVOID EstablisherFrame,
        IN OUT struct _CONTEXT *ContextRecord,
        IN OUT PVOID DispatcherContext
        );

    typedef struct _EXCEPTION_REGISTRATION_RECORD {
        struct _EXCEPTION_REGISTRATION_RECORD *Next;
        PEXCEPTION_ROUTINE Handler;
    } EXCEPTION_REGISTRATION_RECORD;

    typedef EXCEPTION_REGISTRATION_RECORD *PEXCEPTION_REGISTRATION_RECORD;


  其中EXCEPTION_REGISTRATION_RECORD 結構就是登記信息。來介紹下它的成員:
  1. EXCEPTION_REGISTRATION_RECORD::Next 域指向下一個EXCEPTION_REGISTRATION_RECORD,由此構成一個異常登記信息(從字面上說,應該叫做“異常註冊記錄”更恰當)鍊錶。鍊錶中的最後一個結點會將Next 置為EXCEPTION_CHAIN​​_END,表示鍊錶到此結束。
  2. EXCEPTION_REGISTRATION_RECORD::Handler 指向異常處理函數。

  前面有簡單的說過原始版本SEH 的管理工作,這裡再根據以上列出的相關數據結構稍微詳細一點說說。
  當接收到異常後,系統找到當前線程(還記不記得,前面有說過,異常是線程相關的。系統接收到的異常就是當前正在運行的線程觸發的。其實這個說法還不准確,DPC 也會觸發異常,而它是線程無關的,這里為了方便理解,先只考慮線程)的異常鍊錶,從鍊錶中的第一個結點開始遍歷,找到一個EXCEPTION_REGISTRATION_RECORD 就調用它的Handler,並把該異常(由第一個類型為EXCEPTION_RECORD 的參數表示)傳遞給該Handler,Handler 處理並返回一個類型為EXCEPTION_DISPOSITION 的枚舉值。該返回值指示系統下一步該做什麼:
  ExceptionContinueExecution 表示:“我已修正了此異常的故障,請你從事發點重新執行,謝謝”。
  ExceptionContinueSearch 表示:“我沒有處理此異常,請你繼續搜索其他的解決方案,抱歉”。
  ExceptionNestedException 和ExceptionCollidedUnwind 這裡先不做解釋,後面會細說。
  這樣系統根據不同的返回值來繼續遍歷異常鍊錶或者回到觸發點繼續執行。

  需要說明一下,本文主要以內核模式下的異常來說,因為相比用戶模式下的異常處理流程,內核模式少了模式切換、棧切換以及反向回調等步驟。
 
  我們現在來看看詳細的內核異常流程。

  首先,CPU 執行的指令觸發了異常,CPU 改執行IDT 中KiTrap??,KiTrap?? 會調用KiDispatchException。該函數原型如下:
  VOID
  KiDispatchException (
      IN PEXCEPTION_RECORD ExceptionRecord,
      IN PKEXCEPTION_FRAME ExceptionFrame,
      IN PKTRAP_FRAME TrapFrame,
      IN KPROCESSOR_MODE PreviousMode,
      IN BOOLEAN FirstChance
      );
  其名稱明白的說明了函數的主要功能:分派異常。其實現可以參考$wrk-v1.2\base\ntos\ke\i386\exceptn.c:1033。我的筆記:
  在當前棧中分配一個CONTEXT,調用KeContextFromKframes 初始化它。
  檢查 ExceptionRecord->ExceptionCode,如果:
    是STATUS_BREAKPOINT,那麼將CONTEXT::Eip 減一;
    是KI_EXCEPTION_ACCESS_VIOLATION,那麼將檢查是否是由AtlThunk 觸發(這個小環節沒有深究),如果是觸發NX(不可執行),那麼將ExceptionRecord->ExceptionInformation [0] 置為0(貌似表示觸發操作的類型,0表示讀、1表示寫);
        如果 PreviousMode 是 KernelMode,那麼,
    如果FirstChance 為TRUE,那麼將該異常傳達給內核調試器,如果內核調試器沒有處理,那麼調用RtlDispatchException 進行處理。
    如果FirstChance 為FALSE,那麼再次將該異常傳達給內核調試器,如果內核調試器沒有處理,那麼BUGCHECK。
  如果 PreviousMode 是 UserMode,那麼,
    如果FirstChance 為TRUE,那麼將該異常傳達給內核調試器,如果內核調試器沒有處理,那麼將異常傳達給應用層調試器。如果仍然沒有處理,那麼將KTRAP_FRAME 和EXCEPTION_RECORD 拷貝到UserMode 的棧中,並設置KTRAP_FRAME::Eip 設置為ntdll!KiUserExceptionDispatcher,返回(將該異常交由應用層異常處理程序進行處理)。
    如果FirstChance 為FALSE,那麼再次將異常傳達給應用層調試器,如果仍然沒有處理,那麼調用ZwTerminateProcess 結束進程,並BUGCHECK。

  拋開應用層異常不說,我們來看PreviousMode 是KernelMode 的情況,其重點是調用RtlDispatchException 的操作。我們來看一下這個函數:
  BOOLEAN
  RtlDispatchException (
          IN PEXCEPTION_RECORD ExceptionRecord,
      IN PCONTEXT ContextRecord
      );
  它的實現可以參考$wrk-v1.2\base\ntos\rtl\i386\exdsptch.c:126。我的筆記:
  遍歷當前線程的異常鍊錶,挨個調用RtlpExecuteHandlerForException,RtlpExecuteHandlerForException 會調用異常處理函數。再根據返回值做出不同的處理:
  對於ExceptionContinueExecution,結束遍歷,返回。 (對於標記為'EXCEPTION_NONCONTINUABLE'的異常,會調用RtlRaiseException。)
  對於ExceptionContinueSearch,繼續遍歷下一個結點。
  對於ExceptionNestedException,則從指定的新異常繼續遍歷。
  只有正確處理ExceptionContinueExecution 才會返回TRUE,其他情況都返回FALSE。

  在繼續講述異常處理機制之前,咱們需要先來認識一下異常鍊錶。
  之前有提到過:系統將異常鍊錶頭保存在線程結構裡。來看看具體的數據結構:
  線程的內核數據結構體現是_ETHREAD,從它開始進入,直到咱們關注的異常鍊錶。

  kd> dt _ETHREAD
  ntdll!_ETHREAD
     +0x000 Tcb : _KTHREAD
     ... 省略之後的成員

  kd> dt _KTHREAD
  ntdll!_KTHREAD
     ... 省略的域成員
     +0x074 Teb : Ptr32 Void
     ... 省略的域成員

  Teb 成員的類型實際是 _TEB,來看看
  kd> dt _TEB
  ntdll!_TEB
     +0x000 NtTib : _NT_TIB
     ... 省略的域成員

  kd> dt _NT_TIB
  ntdll!_NT_TIB
     +0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
     +0x004 StackBase : Ptr32 Void
     +0x008 StackLimit : Ptr32 Void
     +0x00c SubSystemTib : Ptr32 Void
     +0x010 FiberData : Ptr32 Void
     +0x010 Version : Uint4B
     +0x014 ArbitraryUserPointer : Ptr32 Void
     +0x018 Self : Ptr32 _NT_TIB
  _NT_TIB 的第一個域成員ExceptionList 就是異常鍊錶頭。

  但是系統不是這麼一步一步找的,而是藉助FS 寄存器來加速尋找。先來說說系統對 FS 的使用。
  在應用層,FS 寄存器“指向”當前執行線程的_TEB 結構體。在內核層,FS 寄存器“指向”另一個跟CPU 相關的結構體:_KPCR,來看看它的結構,
  nt!_KPCR
     +0x000 NtTib : _NT_TIB
     +0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
     ... 省略的域成員
  與_TEB 一樣,它的第一個域成員也是_NT_TIB,只不過此時是nt!_NT_TIB,而在應用層是ntdll!_NT_TIB,但它們的結構是一樣的。

  這樣,不論在應用層還是在內核層,系統都可以使用FS:[0] 找到異常鍊錶。

  到這裡,咱們已經聊完了CPU 觸發的異常的處理流程,總結一下它的調用流程:
  CPU 檢測到異常-> KiTrap?? -> KiDispatchException -> RtlDispatchException -> RtlpExecuteHandlerForException

  這是硬件異常,咱們再來看看軟件異常。
  軟件異常跟硬件異常的處理流程非常接近,只有觸發點的不同,調用流程是:
  RtlRaiseException -> RtlDispatchException -> RtlpExecuteHandlerForException

  後面兩個被調用的函數咱已經聊過了,主要來看看RtlRaiseException。這個函數從其名字上就能看出是用來觸發異常的。原型如下:
  VOID
  RtlRaiseException (
      IN PEXCEPTION_RECORD ExceptionRecord
  );
  其實現可以參考$wrk-v1.2\base\ntos\rtl\i386\raise.asm:71。我的筆記:
  RtlRaiseException 首先調用RtlDispatchException 分發異常,如果RtlDispatchException 成功分發(有處理函數處理了這個異常),那麼結束本函數。
  如果沒有成功分發,那麼調用ZwRaiseException 再次觸發該異常,這次傳入的異常的FirstChance 被置為FALSE。

  到這裡,系統提供的SEH 機制(本文又稱之為原始版本)大致講解完畢。咱可以回味一下:
  1. 原始版本的實現較簡單,代碼量不大,而且wrk 基本上有所有關鍵函數的實現代碼。
  2. 原始版本的功能過於簡單,實際過程中很難直接使用。整個異常處理過程無非就是遍歷異常鍊錶,挨個調用異常註冊信息的處理函數,如果其中有某個處理函數處理了該異常(返回值為ExceptionContinueExecution),那麼就從異常觸發點(如果是斷點異常,則要回退一個字節的指令(int 3 指令本身))重新執行。否則不管是整個鍊錶中沒有找到合適的處理函數(返回值為ExceptionContinueSearch),或者遍歷過程中出現問題(返回值為ExceptionNestedException),系統都會簡單粗暴的BUGCHECK。而這也帶來一個問題:
     線程運行過程中會調用很多個函數,每個函數都有可能註冊異常處理,它們提供的異常處理函數既可能處理該函數自身觸發的異常,又可能需要處理其子孫函數觸發的異常。前者還好說,自己出了問題,多少還有可能自己修復。而後者就很頭疼了,它無法了解所有其調用的子孫函數內部的實現,要想修復子孫函數觸發的異常,太困難了。而一旦沒有正確處理,或者沒人處理,系統就崩掉。這個後果太嚴重。於是實際上現實程序設計中,基本上沒有直接使用原始版本的SEH,而是使用編譯器提供的增強版本。


  下面咱們就來聊聊編譯器提供的增強版本。

  首先要說明,增強版本有很多個,不同的編譯器提供的的SEH 增強版本或多或少都有不同處。但是,他們一般都是基於windows 系統提供的原始版本進行完善的。一個典型的增強版就是微軟的編譯器(後面簡稱為MSC)裡提供的__try、__finally,__except。咱們接下來就用這個增強版作為目標進行分析。我使用的MSC 是WDK 7600.16385.1,內置的cl 的版本是15.00.30729.207,link 的版本是9.00.30729.207,測試虛擬機系統為32位Win2k3sp1 + wrk。

  咱們先看看增強版的數據結構,跟之前的原始版本有很多相似之處:

代碼:
    typedef struct _EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION;
    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;
    };
  這個EXCEPTION_REGISTRATION 在增強版中就相當於原始版本中的EXCEPTION_REGISTRATION_RECORD。可以這麼理解它:

代碼:
    struct _EXCEPTION_REGISTRATION{
        struct _EXCEPTION_REGISTRATION_RECORD ExceptionRegistrationRecord;
        struct scopetable_entry *scopetable;
        int trylevel;
        int _ebp;
        PEXCEPTION_POINTERS xpointers;
    }; // 注:本結構體只用於理解原始版和增強版的區別,實際代碼中並沒有這種形式的定義
  也就是說它沿用了老版本的註冊信息結構,只是在域成員名稱上做了些改動,把Next 改名為prev,把Handler 改為handler。除此之外,在原始版本基礎上增加了4個域成員(scopetable、trylevel、_ebp、xpointers),用來支持它的增強功能。
  需要說明的是,這結構體來源於MSC 的crt 源碼裡的exsup.inc,這個文件使用的是彙編語法,該結構體定義是從該文件的註釋中提取出來。在實際的分析過程中,發現它的定義有一些問題:最後一個域成員xpointers 實際上存放在prev 之前,也就是說,實際中__try 增強版用的結構體是這樣的:

代碼:
  
    typedef struct _EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION;
    struct _EXCEPTION_REGISTRATION{
        PEXCEPTION_POINTERS xpointers;
        struct _EXCEPTION_REGISTRATION *prev;
        void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
        struct scopetable_entry *scopetable;
        int trylevel;
        int _ebp;
    };
  相關的宏和結構

代碼:
    TRYLEVEL_NONE equ -1
    TRYLEVEL_INVALID equ -2
  scopetable_entry
     +0x000 previousTryLevel : Uint4B
     +0x004 lpfnFilter : Ptr32 int
     +0x008 lpfnHandler : Ptr32 int

  咱們先來簡單的看一下增強版中出現的幾個新域成員。
  EXCEPTION_REGISTRATION::scopetable 是類型為scopetable_entry 的數組。
  EXCEPTION_REGISTRATION::trylevel 是數組下標,用來索引scopetable 中的數組成員。
  _ebp 是包含該_EXCEPTION_REGISTRATION 結構體的函數的棧幀指針。對於沒有FPO 優化過的函數,一開頭通常有個push ebp 的操作,_ebp 的值就是被壓入的ebp 的值,後續咱們通過代碼就再看實際的應用。
 
  按照原始版本的設計,每一對“觸發異常-處理異常”都會有一個註冊信息即EXCEPTION_REGISTRATION_RECORD。也就是說,如果按照原始的設計,每一個__try/__except(__finally) 都應該對應一個EXCEPTION_REGISTRATION。但是實際的 MSC 實現不是這樣的。
  真正的實現是:
  每個使用__try/__except(__finally) 的函數,不管其內部嵌套或反複使用多少__try/__except(__finally),都只註冊一遍,即只將一個EXCEPTION_REGISTRATION 掛入當前線程的異常鍊錶中(對於遞歸函數,每一次調用都會創建一個EXCEPTION_REGISTRATION,並掛入線程的異常鍊錶中,這是另外一回事)。
  那如何處理函數內部出現的多個__try/__except(__finally) 呢?這多個__except 代碼塊的功能可能大不相同,而註冊信息EXCEPTION_REGISTRATION 中只能提供一個處理函數handler,怎麼辦?
  MSC 的做法是,MSC 提供一個處理函數,即EXCEPTION_REGISTRATION::handler 被設置為MSC 的某個函數,而不是程序猿提供的__except 代碼塊。程序猿提供的多個__except 塊被存儲在EXCEPTION_REGISTRATION::scopetable 數組中。我們看看上面的scopetable_entry 定義,由於我沒有找到它的定義代碼,所以就貼了windbg 中dt 輸出結果。
  其中scopetable_entry::lpfnHandler 就是程序猿提供的__except 異常處理塊代碼。而lpfnFilter 就是__except 的過濾塊代碼。對於__finally 代碼塊,其lpfnFilter 被置為NULL,lpfnHandler 就是其包含的代碼塊。

  下面,我們用一小段簡單的偽代碼來詳細說明。

代碼:
    1 VOID SimpleSeh()
    2 {
    3 __try
    4 {
    5 }
    6 __except(ExceptionFilter_0(...))
    7 {
    8 ExceptCodeBlock_0;
    9 }
    10
    11 __try
    12 {
    13 __try
    14 {
    15 }
    16 __except(ExceptionFilter_1(...))
    17 {
    18 ExceptCodeBlock_1;
    19 }
    20 }
    21 __except(ExceptionFilter_2(...))
    22 {
    23 ExceptCodeBlock_2;
    24 }
    25 }
  編譯時,編譯器會為SimpleSeh 分配一個EXCEPTION_REGISTRATION 和一個擁有3個成員的scopetable 數組,並將EXCEPTION_REGISTRATION::scopetable 指向該數組(請留意:EXCEPTION_REGISTRATION::scopetable 只是一個指針,不是數組)。然後按照__try 關鍵字出現的順序,將對應的__except/__finally 都存入該數組,步驟如下:

  scopetable[0].lpfnFilter = ExceptionFilter_0;
  scopetable[0].lpfnHandler = ExceptCodeBlock_0;

  scopetable[1].lpfnFilter = ExceptionFilter_1;
  scopetable[1].lpfnHandler = ExceptCodeBlock_1;

  scopetable[2].lpfnFilter = ExceptionFilter_2;
  scopetable[2].lpfnHandler = ExceptCodeBlock_2;

  我們假象當前開始執行SimpleSeh 函數,在行14和行15之間觸發了異常。
  根據之前我們的討論的流程:RtlRaiseException -> RtlDispatchException -> RtlpExecuteHandlerForException。
  RtlpExecuteHandlerForException 會調用註冊信息中的處理函數,即EXCEPTION_REGISTRATION::handler。該函數是由MSC 提供的,內部會依次調用scopetable 中的lpfnHandler。
  那咱們來模擬執行一下,在14和15行之前觸發異常,那應該先從scopetable[2] 的ExceptionFilter_2 開始執行,假設該函數返回EXCEPTION_CONTINUE_SEARCH。那接下來應該是scopetable[1],假設ExceptionFilter_1 也返回EXCEPTION_CONTINUE_SEARCH。那麼接下來是不是就應該輪到scopetable[0] 了?不是。咱們再看看上面的偽代碼,行14和行15之間的代碼並沒處於第一個__try/__except 的範圍中,該異常輪不到scopetable[0] 來處理。那怎麼辦? SimpleSeh 執行的過程中怎麼知道到scopetable[1] 就應該停止?
 
  MSC 是通過scopetable_entry::previousTryLevel 來解決這個問題的。上面數組的設置,完整的形式其實是這樣:

  scopetable[0].previousTryLevel = TRYLEVEL_NONE;
  scopetable[0].lpfnFilter = ExceptionFilter_0;
  scopetable[0].lpfnHandler = ExceptCodeBlock_0;

  scopetable[1].previousTryLevel = TRYLEVEL_NONE;
  scopetable[1].lpfnFilter = ExceptionFilter_1;
  scopetable[1].lpfnHandler = ExceptCodeBlock_1;

  scopetable[2].previousTryLevel = 1;
  scopetable[2].lpfnFilter = ExceptionFilter_2;
  scopetable[2].lpfnHandler = ExceptCodeBlock_2;

  scopetable_entry::previousTryLevel 包含的意思是“下一個該輪到數組下標為previousTryLevel 的單元了”。當scopetable_entry::previousTryLevel 等於TRYLEVEL_NONE(-1) 時,就會停止遍歷scopetable。

  咱再來模擬執行一遍,當14和15行之間觸發異常時,首先遍歷到scopetable[2],處理完後,找到scopetable[2].previousTryLevel,發現其值為1,那麼遍歷到scopetable[1] ,處理完後,找到scopetable[1].previousTryLevel,發現其值為TRYLEVEL_NONE,於是停止遍歷。
  好像挺圓滿的,是吧。

  咱們再假設下,如果行4和行5之間觸發了同樣的異常,執行流程應該如何。首先,執行scopetable[2],然後在scopetable[1],然後……(省略若干同上字)。停!這次的異常是在第一個__try/__except 中觸發的,輪不到scopetable[2] 來處理,怎麼辦?
  這個時候就輪到EXCEPTION_REGISTRATION::trylevel 出場了~。 EXCEPTION_REGISTRATION::trylevel 的作用就是標識從那個數組單元開始遍歷。
  與scopetable_entry::previousTryLevel 不同,EXCEPTION_REGISTRATION::trylevel 是動態變化的,也就是說,這個值在SimpleSeh 執行過程中是會經常改變的。比如,
  執行到行4和行5之間,該值就會被修改為0;
  執行到第12行,該值被修改為1;
  執行到14行,該值為2。
  這樣,當異常觸發時候,MSC 就能正確的遍歷scopetable 了。
 
  這裡我畫了一幅草圖來幫助理解:
  圖中下方是低地址端,上方是高地址端。
  (boxcounter: 這幅圖是我借助vim 的列操作手繪的,哪位朋友知道有專門畫這類文本圖的工具嗎(除了emacs 的圖操作模式,這玩意太臃腫了,我不太喜歡)?歡迎告知我ns.boxcounter[a]gmail.com。非常感謝。)



       4G |--------------------------| ...
            | ... | |
        --> |--------------------------| |
       / | ret_addr | |
     func1 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION / previousTryLevel = TRYLEVEL_NONE \
       \ | ... | | -> scopetable[0] | lpfnFilter = ExceptionFilter_0 |
        --> |--------------------------| | / \ lpfnHandler = ExceptionCodeBlock_0 /
       / | ret_addr | | / / previousTryLevel = TRYLEVEL_NONE \ <-
     func2 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION scopetable[1] | lpfnFilter = ExceptionFilter_1 | |
       \ | ... | | \ \ lpfnHandler = ExceptionCodeBlock_1 / |
        --> |--------------------------| | \ / previousTryLevel = 1 \ -^
       / | ret_addr | | -> scopetable[2] | lpfnFilter = ExceptionFilter_2 |
     func3 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION \ lpfnHandler = ExceptionCodeBlock_2 /
       \ | ... | |
        --> |--------------------------| |
       / | ret_addr | |
     func4 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION
       \ | ... | ^
        --> | | |
            | | FS:[0]
       0 -> |--------------------------|

  這幅圖中的​​函數關係是: func1 -> func2 -> func3 -> func4

  到目前位置,咱們已經熟悉了增強版的概要流程。下面結合真實代碼來分析。代碼分為三塊:SEH 創建代碼、MSC 提供的handler 函數,以及展開函數。
  在開始看分析代碼之前,先把後面分析過程中需要用的宏和結構體列出來:
 
代碼:
    #define EXCEPTION_NONCONTINUABLE 0x1 // Noncontinuable exception
    #define EXCEPTION_UNWINDING 0x2 // Unwind is in progress
    #define EXCEPTION_EXIT_UNWIND 0x4 // Exit unwind is in progress
    #define EXCEPTION_STACK_INVALID 0x8 // Stack out of limits or unaligned
    #define EXCEPTION_NESTED_CALL 0x10 // Nested exception handler call
    #define EXCEPTION_TARGET_UNWIND 0x20 // Target unwind in progress
    #define EXCEPTION_COLLIDED_UNWIND 0x40 // Collided exception handler call

    #define EXCEPTION_UNWIND (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND | \
                              EXCEPTION_TARGET_UNWIND | EXCEPTION_COLLIDED_UNWIND)

    nt!_EXCEPTION_RECORD
       +0x000 ExceptionCode : Int4B
       +0x004 ExceptionFlags : Uint4B
       +0x008 ExceptionRecord : Ptr32 _EXCEPTION_RECORD
       +0x00c ExceptionAddress : Ptr32 Void
       +0x010 NumberParameters : Uint4B
       +0x014 ExceptionInformation : [15] Uint4B

    typedef enum _EXCEPTION_DISPOSITION {
        ExceptionContinueExecution,
        ExceptionContinueSearch,
        ExceptionNestedException,
        ExceptionCollidedUnwind
    } EXCEPTION_DISPOSITION;

    // scopetable_entry::lpfnFilter 的返回值,也就是__except 過濾塊的返回值
    #define EXCEPTION_EXECUTE_HANDLER 1
    #define EXCEPTION_CONTINUE_SEARCH 0
    #define EXCEPTION_CONTINUE_EXECUTION -1

没有评论:

发表评论