1. 程式人生 > >SEH分析筆記(X64篇)

SEH分析筆記(X64篇)

SEH分析筆記(X64篇)
v1.0.0
boxcounter

歷史:
v1.0.0, 2011-11-4:最初版本。

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

在之前的《SEH分析筆記(X86篇)》中,我藉助 wrk1.2 介紹了 x86 下 windows 系統核心中的 SEH 實現。這次我們來看看 x64 位 windows 系統核心中 SEH 的實現。
本文需要大家熟悉 x64 位系統的一些特性,比如呼叫約定、Prolog 和 Epilog。可以通過這幾篇文章熟悉一下:
Overview of x64 Calling Conventions
, MSDN
The history of calling conventions, part 5: amd64 , The Old New Thing
Everything You Need To Know To Start Programming 64-Bit Windows Systems, Matt Pietrek

首先回顧一下前一篇文章。
在 x86 windows 中,函式通過以下幾個步驟來參與 SEH :
1. 在自身的棧空間中分配並初始化一個 EXCEPTION_REGISTRATION(_RECORD) 結構體。
2. 將該 EXCEPTION_REGISTRATION(_RECORD) 掛入當前執行緒的異常連結串列。

當某函式觸發異常時,系統首先會通過呼叫 KiDispatchException 來給核心偵錯程式一個機會,如果核心偵錯程式沒有處理該異常,則該機會被轉給 RtlDispatchException,這個函式就開始分發該異常。分發過程為:
從當前執行緒的異常連結串列頭開始遍歷,對於每一個 SEH 註冊資訊(即 EXCEPTION_REGISTRATION(_RECORD)),呼叫其 Handler。根據 Handler 的返回值做相應的後續處理:
1. 返回 ExceptionContinueExecution,表示 Handler 已經修復了異常觸發點,從異常觸發點繼續執行。
2. 返回 ExceptionContinueSearch,表示該 Handler 沒有處理該異常,繼續遍歷異常連結串列。
3. Handler 沒有修復異常觸發點,但是卻能處理該異常(某個 __except 過濾程式碼返回 EXCEPTION_EXECUTE_HANDLER)。這種情況下,處理完該異常後就從異常解決程式碼(__except 程式碼塊)繼續執行,Handler 不會返回。
以上是簡略的 x86 SEH 流程,其中省略了很多細節,比如展開、錯誤處理、ExceptionNestedException 和 ExceptionCollidedUnwind 等等。


之所以在這裡重溫這個流程,是因為 x64 中 SEH 的流程總體思路也是如此,只是細節上做了一些修改。但這並不表示熟悉 x86 SEH 就能很輕鬆的掌握 x64 SEH。

本文分為四個部分:“異常註冊”、“異常分發”、“展開、解決”和“ExceptionNestedException 和 ExceptionCollidedUnwind”。依然以 MSC 的增強版為分析物件。分析環境為:WDK 7600.16385.1,內建的 cl 的版本是15.00.30729.207,link 的版本是9.00.30729.207,測試虛擬機器系統為 amd64 WinXP + wrk1.2。

在講述之前,需要先定義幾個名詞,以簡化後續的講述。

RVA —— 熟悉 PE 格式的朋友都懂的,表示某個絕對地址相對於所在模組的基地址的偏移。
EXCEPT_POINT —— 異常觸發點。
EXCEPT_FILTER —— __except 小括號內的異常過濾程式碼。
EXCEPT_HANDLER —— __except 大括號內的異常解決程式碼。
FINALLY_HANDLER —— __finally 大括號內的程式碼。

以下面的偽碼為例,

  Code:     1  __try
    2  {
    3      __try
    4      {
    5           *((ULONG*)NULL) = 0; 
    6      }
    7      __except((STATUS_INVALID_PARAMETER == GetExceptionCode()) ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER)
    8      {
    9          ...
    10     }
    11 }
    12 __finally
    13 {
    14     ...
    15 {
EXCEPT_POINT 指的是行5中的程式碼。
EXCEPT_FILTER 指的是行7中的“(STATUS_INVALID_PARAMETER == GetExceptionCode()) ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER”。
EXCEPT_HANDLER 指的是行8到行10中所有的程式碼。
FINALLY_HANDLER 指的是行13到行15中所有的程式碼。


一、異常註冊

在 x64 windows 中,異常註冊資訊發生了巨大的改變。x86 中異常註冊資訊是在函式執行過程中在棧中分配並初始化的。x64 中變成這樣:
異常註冊資訊不再是動態建立,而是編譯過程中生成,連結時寫入 PE+ 頭中的 ExceptionDirectory(參考 winnt.h 中 IMAGE_RUNTIME_FUNCTION_ENTRY 的定義)。ExceptionDirectory 裡包含幾乎所有函式的棧操作、異常處理等資訊。

來看看新異常註冊資訊的資料結構:

  Code:     typedef struct _RUNTIME_FUNCTION {
        ULONG BeginAddress;
        ULONG EndAddress;
        ULONG UnwindData;
    } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

    typedef enum _UNWIND_OP_CODES {
        UWOP_PUSH_NONVOL = 0,
        UWOP_ALLOC_LARGE,       // 1
        UWOP_ALLOC_SMALL,       // 2
        UWOP_SET_FPREG,         // 3
        UWOP_SAVE_NONVOL,       // 4
        UWOP_SAVE_NONVOL_FAR,   // 5
        UWOP_SPARE_CODE1,       // 6
        UWOP_SPARE_CODE2,       // 7
        UWOP_SAVE_XMM128,       // 8
        UWOP_SAVE_XMM128_FAR,   // 9
        UWOP_PUSH_MACHFRAME     // 10
    } UNWIND_OP_CODES, *PUNWIND_OP_CODES;

    typedef union _UNWIND_CODE {
        struct {
            UCHAR CodeOffset;
            UCHAR UnwindOp : 4;
            UCHAR OpInfo : 4;
        };
    
        USHORT FrameOffset;
    } UNWIND_CODE, *PUNWIND_CODE;
    
    #define UNW_FLAG_NHANDLER 0x0
    #define UNW_FLAG_EHANDLER 0x1
    #define UNW_FLAG_UHANDLER 0x2
    #define UNW_FLAG_CHAININFO 0x4

    typedef struct _UNWIND_INFO {
        UCHAR Version : 3;
        UCHAR Flags : 5;
        UCHAR SizeOfProlog;
        UCHAR CountOfCodes;
        UCHAR FrameRegister : 4;
        UCHAR FrameOffset : 4;
        UNWIND_CODE UnwindCode[1];
    
    //
    // The unwind codes are followed by an optional DWORD aligned field that
    // contains the exception handler address or a function table entry if
    // chained unwind information is specified. If an exception handler address
    // is specified, then it is followed by the language specified exception
    // handler data.
    //
    //  union {
    //      struct {
    //          ULONG ExceptionHandler;
    //          ULONG ExceptionData[];
    //      };
    //
    //      RUNTIME_FUNCTION FunctionEntry;
    //  };
    //
    
    } UNWIND_INFO, *PUNWIND_INFO;

    typedef struct _SCOPE_TABLE {
        ULONG Count;
        struct
        {
            ULONG BeginAddress;
            ULONG EndAddress;
            ULONG HandlerAddress;
            ULONG JumpTarget;
        } ScopeRecord[1];
    } SCOPE_TABLE, *PSCOPE_TABLE;
x64 中,MSC 為幾乎所有的函式都登記了完備的資訊,用來在展開過程中完整的回滾函式所做的棧、暫存器操作。登記的資訊包括:
函式是否使用了 SEH、
函式使用的是什麼組合的 SEH(__try/__except?__try/__finally?)、
函式申請了多少棧空間、
函式儲存了哪些暫存器、
函式是否建立了棧幀,
等等,
同時也記錄了這些操作的順序(以保證回滾的時候不會亂套)。

這些資訊就儲存在 UNWIND_INFO 之中。
UNWIND_INFO 相當於 x86 下的 EXCEPTION_REGISTRATION。它的成員分別是:
Version —— 結構體的版本。
Flags —— 標誌位,可以有這麼幾種取值:
UNW_FLAG_NHANDLER (0x0): 表示既沒有 EXCEPT_FILTER 也沒有 EXCEPT_HANDLER。
UNW_FLAG_EHANDLER (0x1): 表示該函式有 EXCEPT_FILTER & EXCEPT_HANDLER。
UNW_FLAG_UHANDLER (0x2): 表示該函式有 FINALLY_HANDLER。
UNW_FLAG_CHAININFO (0x4): 表示該函式有多個 UNWIND_INFO,它們串接在一起(所謂的 chain)。
SizeOfProlog —— 表示該函式的 Prolog 指令的大小,單位是 byte。
CountOfCodes —— 表示當前 UNWIND_INFO 包含多少個 UNWIND_CODE 結構。
FrameRegister —— 如果函式建立了棧幀,它表示棧幀的索引(相對於 CONTEXT::RAX 的偏移,詳情參考 RtlVirtualUnwind 原始碼)。否則該成員的值為0。
FrameOffset —— 表示 FrameRegister 距離函式最初棧頂(剛進入函式,還沒有執行任何指令時的棧頂)的偏移,單位也是 byte。
UnwindCode —— 是一個 UNWIND_CODE 型別的陣列。元素數量由 CountOfCodes 決定。
需要說明幾點:
1. 如果 Flags 設定了 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER,那麼在最後一個 UNWIND_CODE 之後存放著 ExceptionHandler(相當於 x86 EXCEPTION_REGISTRATION::handler)和 ExceptionData(相當於 x86 EXCEPTION_REGISTRATION::scopetable)。
2. UnwindCode 陣列詳細記錄了函式修改棧、儲存非易失性暫存器的指令。
3. MSDN 中有 UNWIND_INFO 和 UNWIND_CODE 的詳細說明,推薦閱讀。

那 UNWIND_INFO 是如何與其描述的函式關聯起來的呢?答案是:通過一個 RUNTIME_FUNCTION 結構體。
RUNTIME_FUNCTION::BeginAddress 同 RUNTIME_FUNCTION::EndAddress 一起以 RVA 形式描述了函式的範圍。
RUNTIME_FUNCTION::UnwindData 就是 UNWIND_INFO 了,它也是一個 RVA 值。

PE+ 中的 ExceptionDirectory 中存放著所有函式的 RUNTIME_FUNCTION,按 RUNTIME_FUNCTION::BeginAddress 升序排列。一旦觸發異常,系統可以通過 EXCEPT_POINT 的 RVA 在 ExceptionDirectory 中二分查詢到 RUNTIME_FUNCTION,進而找到 UNWIND_INFO。

前面有提到,MSC 為幾乎所有的函式都登記了完畢的資訊,那是不是有一些特殊函式沒有登記資訊呢?
是的。x64 新增了一個概念,叫做“葉函式”。熟悉資料結構的朋友可能第一時間就聯想到“葉節點”。沒錯,“葉函式”的含義跟“葉節點”很類似,葉函式不會有子函式,也就是說它不會再​呼叫任何函式。另外 x64 對這個概念額外加了一些要求:不修改棧指標(比如分配棧空間)、沒有使用 SEH。總結下來就是:既不呼叫函式、又沒有修改棧指標,也沒有使用 SEH 的函式就叫做“葉函式”。
葉函式可以沒有登記資訊,原因很簡單,它根本就沒資訊需要登記~

還有一個 SCOPE_TABLE 結構,熟悉 x86 SEH 的朋友應該很眼熟 :-),它等同於 x86 SEH 中的 REGISTRATIOIN_RECORD::scopetable 的型別。其成員有:
Count —— 表示 ScopeRecord 陣列的大小。
ScopeRecord —— 等同於 x86 中的 scopetable_entry 成員。其中,
BeginAddress 和 EndAddress 表示某個 __try 保護域的範圍。
HandlerAddress 和 JumpTarget 表示 EXCEPTION_FILTER、EXCEPT_HANDLER 和 FINALLY_HANDLER。具體對應情況為:
對於 __try/__except 組合,HandlerAddress 代表 EXCEPT_FILTER,JumpTarget 代表 EXCEPT_HANDLER。
對於 __try/__finally 組合,HandlerAddress 代表 FINALLY_HANDLER,JumpTarget 等於 0。
這四個域通常都是 RVA,但當 EXCEPT_FILTER 簡單地返回或等於 EXCEPTION_EXECUTE_HANDLER 時,HandlerAddress 可能直接等於 EXCEPTION_EXECUTE_HANDLER,而不再是一個 RVA。

我們可以通過 windbg 中的 .fnent 命令來檢視某個函式的異常註冊資訊。比如,

1 kd> .fnent passThrough!SehTest
2 Debugger function entry 00000000`00778210 for:
3 d:\workspace\code\mycode\r0\passthrough\passthrough.c(51)
4 (fffffadf`f140f020) PassThrough!SehTest | (fffffadf`f140f0c0) PassThrough!Caller2
5 Exact matches:
6 PassThrough!SehTest (void)

8 BeginAddress = 00000000`00001020
9 EndAddress = 00000000`000010b2
10 UnwindInfoAddress = 00000000`00002668
11 
12 Unwind info at fffffadf`f1410668, 10 bytes
13 version 1, flags 1, prolog 4, codes 1
14 handler routine: PassThrough!_C_specific_handler (fffffadf`f140f4ce), data 3
15 00: offs 4, unwind op 2, op info 4 UWOP_ALLOC_SMALL.

行8到行10描述的是 RUNTIME_FUNCTION。
行12到行15描述的是 UNWIND_INFO。

對於葉函式,輸出是這樣的,

kd> .fnent passthrough!LeafTest
No function entry for fffffadf`f240c080

到這裡,異常註冊就講完了,我們認識了相關的資料結構和定位方法。下面我們進入異常分發流程。


二、異常分發

x64 異常分發過程使用的仍然是 KiDispatchException、RtlDispatchException、RtlpExecuteHandlerForException 等函式。

其中,KiDispatchException 中有關核心異常部分的程式碼完全沒有變化,這裡我偷懶直接拷貝《SEH分析筆記(X86篇)》中的部分描述,

原型:
Code:     VOID
    KiDispatchException (
        IN PEXCEPTION_RECORD ExceptionRecord,
        IN PKEXCEPTION_FRAME ExceptionFrame,
        IN PKTRAP_FRAME TrapFrame,
        IN KPROCESSOR_MODE PreviousMode,
        IN BOOLEAN FirstChance
        );
對於核心異常,它的處理步驟如下:
如果 FirstChance 為 TRUE,那麼,
1. 首先將該異常傳達給核心偵錯程式(KD),如果 KD 處理了該異常,那麼函式返回。
2. KD 沒有處理,呼叫 RtlDispatchException 進行異常分發。如果分發成功,RtlDispatchExcetpion 返回 TRUE,或者根本不返回。
3. RtlDispatchException 分發失敗,那麼再給 KD 一次處理機會,如果還是沒有處理,那麼 BUGCHECK。
如果 FirstChance 為 FALSE,那麼將該異常傳達給 KD,如果 KD 沒有處理,那麼 BUGCHECK。
它的原始碼實現位於 $WRK-v1.2\base\ntos\ke\amd64\exceptn.c:430。

RtlDispatchException 發生了一些改變。
從之前描述的異常註冊資訊的資料結構可以發現,x64 中已經不存在異常註冊鏈這個概念了(雖然 _NT_TIB 結構體中還保留了 ExceptionList 域)。那 x64 中 RtlDispatchException 是如何遍歷異常註冊資訊的呢?前面雖然有提到如何通過 EXCEPT_POINT 找到 UNWIND_INFO 結構,但是假如這個 UNWIND_INFO 沒有處理該異常,如何繼續遍歷呢?

在解答這個問題之前,我們先來認識一個新函式和相關的資料結構,

  Code:     #define UNWIND_HISTORY_TABLE_SIZE 12
    
    typedef struct _UNWIND_HISTORY_TABLE_ENTRY {
            ULONG64 ImageBase;
            PRUNTIME_FUNCTION FunctionEntry;
    } UNWIND_HISTORY_TABLE_ENTRY, *PUNWIND_HISTORY_TABLE_ENTRY;
    
    #define UNWIND_HISTORY_TABLE_NONE 0
    #define UNWIND_HISTORY_TABLE_GLOBAL 1
    #define UNWIND_HISTORY_TABLE_LOCAL 2
    
    typedef struct _UNWIND_HISTORY_TABLE {
            ULONG Count;
            UCHAR Search;
            ULONG64 LowAddress;
            ULONG64 HighAddress;
            UNWIND_HISTORY_TABLE_ENTRY Entry[UNWIND_HISTORY_TABLE_SIZE];
    } UNWIND_HISTORY_TABLE, *PUNWIND_HISTORY_TABLE;

    PRUNTIME_FUNCTION
    RtlLookupFunctionEntry (
        IN ULONG64 ControlPc,
        OUT PULONG64 ImageBase,
        IN OUT PUNWIND_HISTORY_TABLE HistoryTable OPTIONAL
        );
RtlLookupFunctionEntry 的功能是查詢指定地址所在函式的 RUNTIME_FUNCTION 和所在模組的基地址。它的引數分為為,
ControlPc —— 需要查詢的指令地址,
ImageBase —— 返回的模組基地址,
HistoryTable —— 用於加速查詢。

工作流程:
RtlLookupFunctionEntry 在搜尋的過程會根據是否傳入 HistoryTable 而採取不同的搜尋方法:
如果傳入 HistoryTable,則根據 HistoryTable->Search 表示的搜尋方式在表中進行搜尋:
如果搜尋方式為 UNWIND_HISTORY_TABLE_NONE,那麼不在 HistoryTable 中進行搜尋。
如果搜尋方式為 UNWIND_HISTORY_TABLE_GLOBAL,則首先在全域性表 RtlpUnwindHistoryTable 開始搜尋,如果搜尋到,則結束搜尋,函式返回。否則再在 HistoryTable 中搜索。
如果搜尋方式為 UNWIND_HISTORY_TABLE_LOCAL,那麼在 HistoryTable 中搜索。
如果上述過程中沒有搜尋到需要的結果,那麼找到模組基地址,從模組的 PE+ 頭結構中解析出 RUNTIME_FUNCTION。如果搜尋方式為 UNWIND_HISTORY_TABLE_NONE,還會將解決加入到 HistoryTable。

之前的描述中 RtlDispatchException 定位 EXCEPT_POINT 所對應的 RUNTIME_FUNCTION 就是通過呼叫 RtlLookupFunctionEntry 實現的。

回到剛才的問題,如何推動遍歷呢?為了解決這個問題,x64 又引進了一個新函式。現在我們有請 x64 SEH 核心成員 RtlVirtualUnwind 登場~

先來看看它的原型:
Code:     PEXCEPTION_ROUTINE
    RtlVirtualUnwind (
        IN ULONG HandlerType,
        IN ULONG64 ImageBase,
        IN ULONG64 ControlPc,
        IN PRUNTIME_FUNCTION FunctionEntry,
        IN OUT PCONTEXT ContextRecord,
        OUT PVOID *HandlerData,
        OUT PULONG64 EstablisherFrame,
        IN OUT PKNONVOLATILE_CONTEXT_POINTERS ContextPointers OPTIONAL
        );
它的主要功能是:
根據傳入的 ControlPc 和 ContextRecord 等引數虛擬(模擬)展開該函式,並返回該函式的一些資訊,比如 HandlerData(SCOPE_TABLE)、EstablisherFrame(rsp 或 棧幀)。
流程是:
1. 通過 FunctionEntry 和 ImageBase 找到 UNWIND_INFO。根據 UNWIND_INFO 中記錄的資訊,查詢 EstablisherFrame(即棧幀或者 rsp)。
2. 根據 ControlPc 分如下兩種情況展開:
a. ControlPc >= EpilogOffset,即 ControlPc 在 Epilog 之中。那麼把剩餘的 Epilog 指令模擬執行完畢即可。
所謂模擬是指,如果下一條 EpiLog 指令是“sub rsp, 0x32”,那麼將 ContextRecord->rsp 減去0x32。並不是真正執行 sub 指令。
b. ControlPc < EpilogOffset,那麼把 Prolog 反向模擬回滾一遍即可。
所謂反向回滾是指,如果 Prolog 的指令是
mov [RSP + 8], RCX
push R15
push R14
push R13 
那麼反向回滾就是
pop ContextRecord->R13 (實際上是從 ContextRecord->Rsp 指向的記憶體中取出值存入 ContextRecord->R13,然後 ContextRecord->Rsp 加上8。並不是真正執行 pop)
pop ContextRecord->R14
pop ContextRecord->R15
mov ContextRecord->RCX, [ContextRecord->RSP+8]
然後把 ContextRecord->Rip 修改為 ControlPc 所在函式的返回地址,即父函式中的某一處 call 的下一條指令。
這樣,ContextRecord 就被恢復成父函式在呼叫 ControlPc 所在函式之後的狀態了。
3. 如果 HandlerType 包含 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER,那麼將 UNWIND_INFO::ExceptionData 賦給傳出引數 HandlerData,並返回 UNWIND_INFO::ExceptionRoutine。
對於 MSC 編譯器生成的模組,UNWIND_INFO::ExceptionRoutine 一般指向 nt!__C_specific_handler。UNWIND_INFO::ExceptionData 指向 ControlPc 所在函式的 SCOPE_TABLE。
RtlVirtualUnwind 的實現原始碼位於 $WRK-v1.2\base\ntos\rtl\amd64\exdsptch.c:1202。

RtlVirtualUnwind 返回後,RtlDispatchException 就可以根據 ContextRecord->Rip 找到父函式對應的 RUNTIME_FUNCTION,進而找到 UNWIND_INFO。就這樣推動整個遍歷過程。

這是一般情況,對於沒有 UNWIND_INFO 的葉函式呢?
對於葉函式,RtlLookupFunctionEntry 返回 NULL,於是 RtlDispatchException 知道這是個葉函式,就找到該葉函式的父函式,從父函式繼續遍歷。也就是完全無視葉函式,因為葉函式對整個異常處理過程沒有任何影響。

RtlDispatchException 呼叫 UNWIND_INFO::ExceptionHandler 依然是通過 RtlpExecuteHandlerForException,其函式原型沒有變化:

  Code:     EXCEPTION_DISPOSITION
    RtlpExecuteHandlerForException (
        IN PEXCEPTION_RECORD ExceptionRecord,
        IN PVOID EstablisherFrame,
        IN OUT PCONTEXT ContextRecord,
        IN OUT PVOID DispatcherContext
        );
該函式的實現原始碼位於 $\WRK-v1.2\base\ntos\rtl\amd64\xcptmisc.asm:84。
RtlpExecuteHandlerForException 的邏輯較 x86 版本沒什麼大變化,內部註冊了一個異常處理函式 RtlpExceptionHandler。RtlpExceptionHandler 相當於 x86 中的 nt!ExecuteHandler2,其內部會返回 ExceptionNestedException 或 ExceptionContinueSearch。它的實現原始碼位於 $\WRK-v1.2\base\ntos\rtl\amd64\xcptmisc.asm:26。

需要一提的是,最後一個引數 DispatchContext 的型別是 DISPATCHER_CONTEXT,相對於 x86 版本,它擴充了很多,

  Code:     typedef struct _DISPATCHER_CONTEXT {
        ULONG64 ControlPc;
        ULONG64 ImageBase;
        PRUNTIME_FUNCTION FunctionEntry;
        ULONG64 EstablisherFrame;
        ULONG64 TargetIp;
        PCONTEXT ContextRecord;
        PEXCEPTION_ROUTINE LanguageHandler;
        PVOID HandlerData;
        PUNWIND_HISTORY_TABLE HistoryTable;
        ULONG ScopeIndex;
        ULONG Fill0;
    } DISPATCHER_CONTEXT, *PDISPATCHER_CONTEXT;
成員分別為:
ControlPc —— 異常觸發點。
ImagePase —— ControlPc 所在模組的基地址。
FunctionEntry —— ControlPc 所在函式的 RUNTIME_FUNCTION。
EstablisherFrame —— ControlPc 所在函式的棧幀(如果建立了棧幀)或 RSP。
TargetIp —— 解決異常的 EXCEPT_HANDLER 地址,該成員只在展開的過程中被使用。RtlpExecuteHandlerForException 沒有使用它。
ContextRecord —— 供展開過程中使用,只有當展開過程中觸發新異常(返回 ExceptionCollidedUnwind)時,才會被 RtlDispatchException 真正的使用到(參考 RtlDispatchException 處理 ExceptionCollidedUnwind 的程式碼)。
LanguageHandler —— ControlPc 所在函式的 UNWIND_INFO::ExceptionRoutine。
HandlerData —— ControlPc 所在函式的 UNWIND_INFO::ExceptionData。
ScopeIndex —— UNWIND_INFO::ExceptionData 中 SCOPE_TABLE::ScopeRecord 的索引,通常設定為0(注:請不要與 x86 中執行時不斷改變的 EXCEPTION_REGISTRATION::trylevel 相混淆,ScopeIndex 不會在在函式執行過程中改變)
Fill0 —— 未用。

再看一下它的 .fnent 輸出,

1 kd> .fnent nt!RtlpExecuteHandlerForException
2 Debugger function entry 00000000`01458210 for:
3 (fffff800`008bd950) nt!RtlpExecuteHandlerForException | (fffff800`008bd970) nt!RtlpUnwindHandler
4 Exact matches:
5 nt!RtlpExecuteHandlerForException (void)

7 BeginAddress = 00000000`000bd950
8 EndAddress = 00000000`000bd963
9 UnwindInfoAddress = 00000000`000dfeb8
10 
11 Unwind info at fffff800`008dfeb8, 10 bytes
12 version 1, flags 3, prolog 4, codes 1
13 handler routine: nt!RtlpExceptionHandler (fffff800`008bd920), data 0
14 00: offs 4, unwind op 2, op info 4 UWOP_ALLOC_SMALL.

行12中顯示 flags 等於3,即 UNW_FLAG_EHANDLER (0x1) | UNW_FLAG_UHANDLER (0x2),說明行13中顯示的異常處理函式 nt!RtlpExceptionHandler 既負責解決異常,也負責展開。

RtlpExecuteHandlerForException 會呼叫 DISPATCHER_CONTEXT::LanguageHandler。對於 MSC 編譯得到的模組,它是 nt!__C_specific_handler,我們來看看這個函式,

原型:
Code:     EXCEPTION_DISPOSITION 
    __C_specific_handler (
        IN PEXCEPTION_RECORD pExceptionRecord,
        IN PVOID pEstablisherFrame,
        IN OUT PCONTEXT pContext,
        IN OUT PVOID pDispatcherContext
        );
反彙編碼: Code: kd> uf nt!__C_specific_handler
               nt!__C_specific_handler:
               fffff800`008a42d0 mov     qword ptr [rsp+10h],rdx ; 在棧上儲存 pEstablisherFrame
               fffff800`008a42d5 mov     rax,rsp
               fffff800`008a42d8 sub     rsp,88h
               fffff800`008a42df mov     qword ptr [rax-8],rbx
               fffff800`008a42e3 mov     qword ptr [rax-10h],rbp
               fffff800`008a42e7 mov     rbp,qword ptr [r9]      ; rbp = pDispatcherContext->ControlPc
               fffff800`008a42ea mov     qword ptr [rax-18h],rsi
               fffff800`008a42ee mov     qword ptr [rax-20h],rdi
               fffff800`008a42f2 mov     qword ptr [rax-28h],r12
               fffff800`008a42f6 mov     r12,qword ptr [r9+38h]  ; r12 = pDispatcherContext->HandlerData
               fffff800`008a42fa mov     qword ptr [rax-30h],r13
               fffff800`008a42fe mov     qword ptr [rax-38h],r14
               fffff800`008a4302 mov     r14,qword ptr [r9+8]    ; r14 = pDispatcherContext->ImageBase
               fffff800`008a4306 mov     qword ptr [rax-40h],r15
               fffff800`008a430a mov     r13,r9                  ; r13 = pDispatcherContext
               fffff800`008a430d sub     rbp,r14                 ; l_OffsetInFunc = pDispatcherContext->ControlPc - pDispatcherContext->ImageBase
               fffff800`008a4310 test    byte ptr [rcx+4],66h    ; pExceptionRecord->ExceptionFlags, EXCEPTION_UNWIND (0x66)
               fffff800`008a4314 mov     rsi,rdx                 ; rsi = pEstablisherFrame
               fffff800`008a4317 mov     r15,rcx                 ; r15 = pExceptionRecord
<              fffff800`008a431a jne     nt!__C_specific_handler+0xf5 (fffff800`008a43c5)
:              
:              -------------------------------------------------------------------
:              nt!__C_specific_handler+0x50:
:              fffff800`008a4320 movsxd  rdi,dword ptr [r9+48h]  ; l_ScopeIndex (rdi) = pDispatcherContext->ScopeIndex
:              fffff800`008a4324 mov     qword ptr [rax-58h],rcx ; [rax-58h] = pExceptionRecord,供給 GetExceptionCode(Information) 使用
:              fffff800`008a4328 mov     qword ptr [rax-50h],r8  ; [rax-50h] = pContext,供給 GetExceptionCode(Information) 使用
:              fffff800`008a432c cmp     edi,dword ptr [r12]     ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
:              fffff800`008a4330 mov     rax,rdi                 ; rax = l_ScopeIndex
:<             fffff800`008a4333 jae     nt!__C_specific_handler+0x166 (fffff800`008a4436)
::             
::             nt!__C_specific_handler+0x69:
::             fffff800`008a4339 add     rax,rax             ; 這裡 *2,下面緊接著 *8,目的是跳過指定數目的 ScopeRecord(大小為16位元組)
::             fffff800`008a433c lea     rbx,[r12+rax*8+0Ch] ; rbx = &(pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress)
::             
::             nt!__C_specific_handler+0x71:
::             ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟一
::      >      fffff800`008a4341 mov     eax,dword ptr [rbx-8] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::      :      fffff800`008a4344 cmp     rbp,rax               ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::<     :      fffff800`008a4347 jb      nt!__C_specific_handler+0xdd (fffff800`008a43ad)
:::     :      
:::     :      nt!__C_specific_handler+0x79:
:::     :      ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟二
:::     :      fffff800`008a4349 mov     eax,dword ptr [rbx-4] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::     :      fffff800`008a434c cmp     rbp,rax               ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::<    :      fffff800`008a434f jae     nt!__C_specific_handler+0xdd (fffff800`008a43ad)
::::    :      
::::    :      nt!__C_specific_handler+0x81:
::::    :      ; 判斷是否是 __try/__finally(JumpTarget 為 NULL)。如果是,那麼跳轉到下一個 ScopeRecord 繼續遍歷。
::::    :      fffff800`008a4351 cmp     dword ptr [rbx+4],0 ; cmp pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget, NULL
::::<   :      fffff800`008a4355 je      nt!__C_specific_handler+0xdd (fffff800`008a43ad)
:::::   :      
:::::   :      nt!__C_specific_handler+0x87:
:::::   :      ; 到這裡,已經找到與異常地址最匹配的 __try/__except
:::::   :      fffff800`008a4357 mov     eax,dword ptr [rbx] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress
:::::   :      fffff800`008a4359 cmp     eax,1               ; cmp pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress, EXCEPTION_EXECUTE_HANDLER (0x1)
:::::<  :      fffff800`008a435c je      nt!__C_specific_handler+0xa3 (fffff800`008a4373) ; 如果返回 EXCEPTION_EXECUTE_HANDLER 則跳轉
::::::  :      
::::::  :      nt!__C_specific_handler+0x8e:
::::::  :      ; 是 __try/__except,且過濾域並不是 EXCEPTION_EXECUTE_HANDLER,執行 HandlerAddress 
::::::  :      ; (注:HandlerAddress 指向的函式仍有可能會返回 EXCEPTION_EXECUTE_HANDLER) 
::::::  :      fffff800`008a435e lea     rcx,[rsp+30h]
::::::  :      fffff800`008a4363 add     rax,r14 ; rax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress + pDispatcherContext->ImageBase
::::::  :      fffff800`008a4366 mov     rdx,rsi ; rdx = pEstablisherFrame
::::::  :      fffff800`008a4369 call    rax     ; 呼叫 EXCEPT_FILTER
::::::  :      fffff800`008a436b test    eax,eax
::::::< :      fffff800`008a436d js      nt!__C_specific_handler+0xee (fffff800`008a43be) ; 返回 EXCEPTION_CONTINUE_EXECUTION (-1) 則跳轉
::::::: :      
::::::: :      nt!__C_specific_handler+0x9f:
::::::: :      fffff800`008a436f test    eax,eax
:::::::<:      fffff800`008a4371 jle     nt!__C_specific_handler+0xdd (fffff800`008a43ad) ; 返回 EXCEPTION_CONTINUE_SEARCH (0) 則跳轉
:::::::::      
:::::::::      nt!__C_specific_handler+0xa3:
:::::::::      ; 返回的是 EXCEPTION_EXECUTE_HANDLER
:::::>:::      fffff800`008a4373 mov     ecx,dword ptr [rbx+4] ; ecx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
::::: :::      fffff800`008a4376 mov     r8d,1
::::: :::      fffff800`008a437c mov     rdx,rsi ; rdx = pEstablisherFrame
::::: :::      fffff800`008a437f add     rcx,r14 ; rcx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget + pDispatcherContext->ImageBase
::::: :::      fffff800`008a4382 call    nt!_NLG_Notify (fffff800`008b1460)
::::: :::      fffff800`008a4387 mov     rax,qword ptr [r13+40h] ; rax = pDispatcherContext->HistoryTable
::::: :::      fffff800`008a438b mov     edx,dword ptr [rbx+4]   ; edx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
::::: :::      fffff800`008a438e movsxd  r9,dword ptr [r15]      ; r9 = pExceptionRecord->ExceptionCode
::::: :::      fffff800`008a4391 mov     qword ptr [rsp+28h],rax ; _ARG_6 = pDispatcherContext->HistoryTable
::::: :::      fffff800`008a4396 mov     rax,qword ptr [r13+28h] ; rax = pDispatcherContext->ContextRecord
::::: :::      fffff800`008a439a add     rdx,r14 ; rdx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget + pDispatcherContext->ImageBase
::::: :::      fffff800`008a439d mov     r8,r15  ; r8 = pExceptionRecord
::::: :::      fffff800`008a43a0 mov     rcx,rsi ; rcx = pEstablisherFrame
::::: :::      fffff800`008a43a3 mov     qword ptr [rsp+20h],rax ; _ARG_5 = pDispatcherContext->ContextRecord
::::: :::      fffff800`008a43a8 call    nt!RtlUnwindEx (fffff800`00891e80) ; 這裡不會返回
::::: :::      ; RtlUnwindEx(pEstablisherFrame, 
::::: :::      ;             pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget + pDispatcherContext->ImageBase
::::: :::      ;             pExceptionRecord,
::::: :::      ;             pExceptionRecord->ExceptionCode
::::: :::      ;             pDispatcherContext->ContextRecord,
::::: :::      ;             pDispatcherContext->HistoryTable)
::::: :::      
::::: :::      nt!__C_specific_handler+0xdd:
::>>> :>:      fffff800`008a43ad inc     edi     ; l_ScopeIndex += 1
::    : :      fffff800`008a43af add     rbx,10h ; 調整到下一個 ScopeRecord::HandlerAddress
::    : :      fffff800`008a43b3 cmp     edi,dword ptr [r12] ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
::    : <      fffff800`008a43b7 jb      nt!__C_specific_handler+0x71 (fffff800`008a4341)
::    :        
::    :        nt!__C_specific_handler+0xe9:
::    :        ; pDispatcherContext->HandlerData 遍歷完畢
::<   :        fffff800`008a43b9 jmp     nt!__C_specific_handler+0x166 (fffff800`008a4436)
:::   :        
:::   :        nt!__C_specific_handler+0xee:
:::   >        fffff800`008a43be xor     eax,eax ; eax = ExceptionContinueExecution
:::<           fffff800`008a43c0 jmp     nt!__C_specific_handler+0x16b (fffff800`008a443b)
::::           
::::           -------------------------------------------------------------------------------------
::::           nt!__C_specific_handler+0xf5:
::::           ; 設定了 EXCEPTION_UNWIND,當前是展開過程
>:::           fffff800`008a43c5 movsxd  rdi,dword ptr [r9+48h] ; l_ScopeIndex (rdi) = pDispatcherContext->ScopeIndex
:::           fffff800`008a43c9 mov     rsi,qword ptr [r9+20h] ; rsi = pDispatcherContext->TargetIp
:::           fffff800`008a43cd sub     rsi,r14                ; rsi = pDispatcherContext->TargetIp - pDispatcherContext->ImageBase
:::           fffff800`008a43d0 cmp     edi,dword ptr [r12]    ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
:::           fffff800`008a43d4 mov     rax,rdi                ; rax = l_ScopeIndex
:::<          fffff800`008a43d7 jae     nt!__C_specific_handler+0x166 (fffff800`008a4436)
::::          
::::          nt!__C_specific_handler+0x109:
::::          fffff800`008a43d9 add     rax,rax ; 
::::          fffff800`008a43dc lea     rbx,[r12+rax*8+8] ; rbx = &(pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress)
::::          
::::          nt!__C_specific_handler+0x111:
::::          ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟一
::::        > fffff800`008a43e1 mov     eax,dword ptr [rbx-4] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::::        : fffff800`008a43e4 cmp     rbp,rax ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::::<       : fffff800`008a43e7 jb      nt!__C_specific_handler+0x15a (fffff800`008a442a)
:::::       : 
:::::       : nt!__C_specific_handler+0x119:
:::::       : ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟二
:::::       : fffff800`008a43e9 mov     ecx,dword ptr [rbx] ; ecx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::::       : fffff800`008a43eb cmp     rbp,rcx ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::::<      : fffff800`008a43ee jae     nt!__C_specific_handler+0x15a (fffff800`008a442a)
::::::      : 
::::::      : nt!__C_specific_handler+0x120:
::::::      : ; 到這裡,已經找到與異常地址匹配的最內層(如果有多層) __try/__except
::::::      : fffff800`008a43f0 cmp     rsi,rax ; cmp pDispatcherContext->TargetIp - pDispatcherContext->ImageBase, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::::::<     : fffff800`008a43f3 jb      nt!__C_specific_handler+0x131 (fffff800`008a4401)
:::::::     : 
:::::::     : nt!__C_specific_handler+0x125:
:::::::     : fffff800`008a43f5 cmp     rsi,rcx ; cmp pDispatcherContext->TargetIp - pDispatcherContext->ImageBase, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::::::<    : fffff800`008a43f8 ja      nt!__C_specific_handler+0x131 (fffff800`008a4401)
::::::::    : 
::::::::    : nt!__C_specific_handler+0x12a:
::::::::    : ; 如果標記了 EXCEPTION_TARGET_UNWIND,說明是最後一個需要區域性展開的函式。但是該次區域性展開只展開到 EXCEPT_HANDLER(不包含 EXCEPT_HANDLER),所以需要判斷 TargetIp
::::::::    : fffff800`008a43fa test    byte ptr [r15+4],20h ; test pExceptionRecord->ExceptionFlags, EXCEPTION_TARGET_UNWIND (0x20)
::::::::<   : fffff800`008a43ff jne     nt!__C_specific_handler+0x166 (fffff800`008a4436)
:::::::::   : 
:::::::::   : nt!__C_specific_handler+0x131:
::::::>>:   : fffff800`008a4401 mov     eax,dword ptr [rbx+8] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
::::::  :   : fffff800`008a4404 test    eax,eax ; 判斷 pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget 是否為 NULL,即是否是 __try/__finally
::::::  :<  : fffff800`008a4406 je      nt!__C_specific_handler+0x13f (fffff800`008a440f) ; 如果是 __try/__finally 則跳轉
::::::  ::  : 
::::::  ::  : nt!__C_specific_handler+0x138:
::::::  ::  : fffff800`008a4408 cmp     rsi,rax ; cmp pDispatcherContext->TargetIp, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
::::::  ::< : fffff800`008a440b je      nt!__C_specific_handler+0x166 (fffff800`008a4436)
::::::  ::: : 
::::::  ::: : nt!__C_specific_handler+0x13d:
::::::  :::<: fffff800`008a440d jmp     nt!__C_specific_handler+0x15a (fffff800`008a442a)
::::::  ::::: 
::::::  ::::: nt!__C_specific_handler+0x13f:
::::::  ::::: ; 注意這裡是先修改 pDispatcherContext->ScopeIndex,然後呼叫 EXCEPT_HANDLER。這樣如果 EXCEPT_HANDLER 觸發異常,後續展開就會跳過這個 EXCEPT_HANDLER。
::::::  :>::: fffff800`008a440f mov     rdx,qword ptr [rsp+98h]
::::::  : ::: fffff800`008a4417 lea     eax,[rdi+1]             ; eax = l_ScopeIndex + 1
::::::  : ::: fffff800`008a441a mov     cl,1
::::::  : ::: fffff800`008a441c mov     dword ptr [r13+48h],eax ; pDispatcherContext->ScopeIndex = eax
::::::  : ::: fffff800`008a4420 mov     r8d,dword ptr [rbx+4]   ; r8d = pDispatcherContext->HandlerData->ScopeRecord[i].HandlerAddress
::::::  : ::: fffff800`008a4424 add     r8,r14                  ; r8 = pDispatcherContext->HandlerData->ScopeRecord[i].HandlerAddress + pDispatcherContext->ImageBase
::::::  : ::: fffff800`008a4427 call    r8                      ; 呼叫 __finally 處理塊,會返回(注:對於 __try/__finally,HandlerAddress 儲存的是 __finally 程式碼塊的 RVA)
::::::  : ::: 
::::::  : ::: nt!__C_specific_handler+0x15a:
::::>>  : :>: fffff800`008a442a inc     edi                 ; l_ScopeIndex += 1
::::    : : : fffff800`008a442c add     rbx,10h             ; 調整到下一個 ScopeRecord::HandlerAddress
::::    : : : fffff800`008a4430 cmp     edi,dword ptr [r12] ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
::::    : : < fffff800`008a4434 jb      nt!__C_specific_handler+0x111 (fffff800`008a43e1)
::::    : :   
::::    : :   nt!__C_specific_handler+0x166:
>>:>    > >   fffff800`008a4436 mov     eax,1 ; eax = ExceptionContinueSearch (0n1)
   :           
   :           nt!__C_specific_handler+0x16b:
   >           fffff800`008a443b mov     r15,qword ptr [rsp+48h]
               fffff800`008a4440 mov     r14,qword ptr [rsp+50h]
               fffff800`008a4445 mov     r13,qword ptr [rsp+58h]
               fffff800`008a444a mov     r12,qword ptr [rsp+60h]
               fffff800`008a444f mov     rdi,qword ptr [rsp+68h]
               fffff800`008a4454 mov     rsi,qword ptr [rsp+70h]
               fffff800`008a4459 mov     rbp,qword ptr [rsp+78h]
               fffff800`008a445e mov     rbx,qword ptr [rsp+80h]
               fffff800`008a4466 add     rsp,88h
               fffff800`008a446d ret
nt!__C_specific_handler 相當於 x86 中的 nt!_except_handler3。從上面的反彙編程式碼也可以看出它的邏輯跟 nt!_except_handler3 基本上一致。
函式程式碼不長。主要分為兩個大分支,一個分支處理異常,一個分支處理展開(我用橫線分隔開了)。

異常解決的程式碼負責遍歷 SCOPE_TABLE,依次呼叫 SCOPE_TABLE::ScopeRecord.HandlerAddress 代表的 EXCEPT_FILTER,並針對返回值做出相應的處理:
1. 返回 EXCEPTION_CONTINUE_EXECUTION,說明異常已經被 EXCEPT_FILTER 修復。返回 ExceptionContinueExecution。
2. 返回 EXCEPTION_CONTINUE_SEARCH,繼續遍歷下一個 ScopeRecord。
3. 返回 EXCEPTION_EXECUTE_HANDLER,說明當前 ScopeRecord.JumpTarget 代表的 EXCEPT_HANDLER 可以處理該異常。那麼呼叫 RtlUnwindEx 進行展開。

熟悉 x86 的朋友可能會疑惑:在 x86 中 nt!_except_handler3 先進行全域性展開,然後對本函式自身進行不完全的區域性展開,最後執行 EXCEPT_HANDLER。而在 nt!__C_specific_handler 中卻找不到執行 EXCEPT_HANDLER 的指令,這是怎麼回事?
實際上,x64 對這個流程做了一些調整,EXCEPT_HANDLER 不是由 nt!__C_specific_handler 直接呼叫,而是作為引數傳給 RtlUnwindEx,RtlUnwindEx 處理完展開之後才執行 EXCEPT_HANDLER。後續我們在講展開的時候會看到具體的方法。

__C_specific_handler 的展開分支,是對 SCOPE_TABLE 進行展開,邏輯很簡單,不多講了。

更詳細的資訊,請參考上面反彙編程式碼中我附的註釋。

另外還需要說一下 SCOPE_TABLE。
在 x86 中,遍歷 scopetable 時是通過執行時動態改變的 EXCEPTION_REGISTRATION::trylevel 來確定應該首先遍歷哪一個 scopetable_entry。而 x64 中沒有等同於 trylevel 的資料,有的朋友可能會說“SCOPE_TABLE 中不是有每個 __try 保護域的範圍 RVA 嗎?通過範圍不就可以確定在哪個 __try 中觸發了異常嗎?”。
我們可以先試試這種方法,以下面這段偽碼為例,

  Code:     1 VOID SehTest()
    2 {
    3     __try // 1
    4     {
    5     }
    6     __except()
    7     {
    8     }
    9 
    10    __try // 2
    11    {
    12        __try // 3
    13        {
    14            ...
    15        }
    16        __except()
    17        {
    18        }
    19    }
    20    __except()
    21    {
    22    }
    23
    24    __try // 4
    25    {
    26    }
    27    __finally()
    28    {
    29    }
    30}
上述偽碼中總共有4個 __try,按照 x86 中的方法,SCOPE_TABLE 的內容應該是順序排列的,像這樣:

SCOPE_TABLE::Count 等於4,