第8章:Windows 下的異常處理-SEH


開發人員主要使用兩種異常處理技術,一種是 SEH (結構化異常處理),另一種是 VEH (向量化異常處理,XP 以上)

Intel公司在從386開始的IA-32家族處理器中引人了中斷(Interrupt)異常(Exception)的概念。

中斷是由外部硬件設備或異步事件產生的,而異常是由內部事件產生的,又可分為故障、陷阱和終止3類。故障和陷阱,正如其名稱所示的,是可恢復的;終止類異常是不可恢復的,如果發生了這種異常,系統必須重啟。

CPU 訪問無效內存,是硬件異常;操作系統或軟件引發的異常,是軟件異常

同時代碼可以通過函數 RaiseException ,主動引發一個異常

 

Windows 正常啟動后,會運行在保護模式下,當有中斷或異常發生時,CPU 會通過中斷描述符(Interrupt Descriptor Table)來尋找處理函數。

 

IDT (下面主要討論 32 位模式下的 IDT)

 

IDT 是一張位於物理內存中的線性表,共有 256 項。在 32 位模式下每個 IDT 項的長度是 8 字節,在 64 位模式下則為 16 字節。

  操作系統啟動階段會初始化這個表,系統中的每個 CPU 都有一份 IDT 的拷貝。IDT 的位置和長度是由 CPU 的 IDTR 寄存器描述的。IDTR 寄存器共有 48 位,其中高 32 位是表的基址,低 16 位是表的長度。盡管可以使用 SIDTLIDT 指令來讀寫該寄存器,但 LIDT 是特權指令,只能在 Ring 0 特權級下運行。

 

SIDT 指令的功能 (僅對當前的 CPU): 將中斷描述符表寄存器IDTR--64位寬,16~47Bit 存有中斷描述符表 IDT 基地址的內容存入指定地址單元。獲得 IDT 的基地址后,可以修改 IDT,增加一個中斷門安置自己的中斷服務。

 

IDT 的每一項都是一個門結構,它是發生中斷或異常時 CPU 轉移控制權的必經之路,包括如下

任務門(Task-gate) 描述符,主要用於 CPU 的任務切換(TSS功能)。(微軟沒有采用該方式,內存頻繁讀寫,拖慢系統速度,在 x64 中被廢除)

 中斷門( Interrupt-gate)描述符,主要用於描述中斷處理程序的入口

 陷阱門(Trap-gate)描述符,主要用於描述異常處理程序的入口。(Windows 64 位下,系統本身的運行沒有使用任務門)

 

 

 

32 位下有任務門

  當有中斷或異常發生時,CPU 會根據中斷類型號(這里其實把異常也視為一種中斷)轉而執行對應的中斷處理程序,對異常來說就是上面看到的 KiTrapXX 函數。例如,中斷號03對應於一個斷點異常,當該異常發生時,CPU就會執行 nt!KiTrap03 函數來處理該異常。各個異常處理函數除了針對本異常的特定處理之外,通常會將異常信息進行封裝,以便進行后續處理。

封裝的內容主要有兩部分:一部分是異常記錄,包含本次異常的信息,該結構定義如下。 

 

這個結構體其實就是 SEH 處理函數的第一個參數。ExceptionCode 可以自己定義,自定義代碼在 RaiseException 函數中使用

 

  另一部分被封裝的內容稱為陷阱幀,它精確描述了發生異常時線程的狀態( Windows 的任務調度是基於線程的)。該結構與處理器高度相關,因此在不同的平台上(Intel x86/x64、MIPS、Alpha 和 PowerPC 處理器等)有不同的定義。在常見的 x86 平台上,該結構定義如下。

typedef struct _KTRAP_FRAME
{
// 以下四項僅為調試系統服務 ULONG DbgEbp
;     //用戶EBP指針的拷貝,用於支持棧回溯命令KB ULONG DbgEip;     //用於 系統調用時的 EIP 同上,用於 KB 命令 ULONG DbgArgMark;   //標記顯示這里沒有參數 ULONG DbgArgPointer; //指向實際參數

   // 當需要調整棧幀時使用以下值作為臨時變量
WORD TempSegCs; UCHAR Logging; UCHAR Reserved; ULONG TempEsp;
   // 調試寄存器
ULONG Dr0; ULONG Dr1; ULONG Dr2; ULONG Dr3; ULONG Dr6; ULONG Dr7;
   // 段寄存器
ULONG SegGs; ULONG SegEs; ULONG SegDs;
   // 易失寄存器
ULONG Edx; ULONG Ecx; ULONG Eax;
   // 調試系統使用
ULONG PreviousPreviousMode; PEXCEPTION_REGISTRATION_RECORD ExceptionList; ULONG SegFs;
   // 非易失寄存器
ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp;
   // 控制寄存器
ULONG ErrCode; ULONG Eip; ULONG SegCs; ULONG EFlags;
   // 其它特殊變量
ULONG HardwareEsp; ULONG HardwareSegSs; ULONG V86Es; ULONG V86Ds; ULONG V86Fs; ULONG V86Gs; } KTRAP_FRAME, *PKTRAP_FRAME;

可以看到,上述結構中包含每個寄存器的狀態,但該結構一般系統內核自身或者調試系統使用。

當需要把控制權交給用戶注冊的異常處理程序時,會將上述結構轉換成一個名為 CONTEXT 的結構,它包含線程運行時處理器各主要寄存器的完整鏡像,用於保存線程運行環境。

typedef struct _CONTEXT
{
   // 調試寄存器 DWORD ContextFlags +00h DWORD Dr0 +04h DWORD Dr1 +08h DWORD Dr2       +0Ch DWORD Dr3 +10h DWORD Dr6 +14h DWORD Dr7 +18h FLOATING_SAVE_AREA FloatSave
; //浮點寄存器區 +1Ch~~~88h    // 段寄存器 DWORD SegGs   +8Ch DWORD SegFs   +90h DWORD SegEs +94h DWORD SegDs +98h    // 通用寄存器 DWORD Edi +9Ch DWORD Esi +A0h DWORD Ebx +A4h DWORD Edx +A8h DWORD Ecx +ACh DWORD Eax +B0h    // 控制寄存器 DWORD Ebp +B4h DWORD Eip +B8h DWORD SegCs +BCh DWORD EFlag +C0h DWORD Esp +C4h DWORD SegSs +C8h BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT; typedef CONTEXT *PCONTEXT; #define MAXIMUM_SUPPORTED_EXTENSION 512

第一個域 ContexFlags 表示該結構體中的哪些域有效,恢復信息時可有選擇的更新數據。

包裝完畢,異常處理函數會進一步調用系統內核的 nt!KiDispatchException 函數來處理異常。因此,只有深入分析  KiDispatchException 函數的執行過程,才能理解異常是如何被處理的。該函數原型及各參數的含義如下,其第1個和第3個參數正是上面封裝的兩個結構。

 

 

當異常處理過程在內核態中

PreviousModeKernelMode 時,表示是內核模式下產生的異常,此時 KiDispatchException 會按以下步驟分發異常

① 檢測當前系統是否正在被內核調試器調試。如果內核調試器不存在,就跳過本步驟。如果內核調試器存在,系統就會把異常處理的控制權轉交給內核調試器,並注明是第1次處理機會( FirstChance )。內核調試器取得控制權之后,會根據用戶對異常處理的設置來確定是否要處理該異常。如果無法確定該異常是否需要處理,就會發生中斷,把控制權交給用戶,由用戶決定是否處理。如果調試器正確處理了該異常,那么發生異常的線程就會回到原來產生異常的位置繼續執行。

② 如果不存在內核調試器,或者在第1次處理機會出現時調試器選擇不處理該異常,系統就會調用 nt!RtIDispatchException 函數,根據線程注冊的結構化異常處理(Structured Exception Handling,SEH )過程來處理該異常。

③ 如果 nt!RtIDispatchException 函數沒有處理該異常,系統會給調試器第2次處理機會( SecondChance ),此時調試器可以再次取得對異常的處理權。

④如果不存在內核調試器,或者在第2次機會調試器仍不處理,系統就認為在這種情況下不能繼續運行了。為了避免引起更加嚴重的、不可預知的錯誤,系統會直接調用 KeBugCheckEx 產生一個錯誤碼為“KERNEL_MODE_EXCEPTION_NOT_HANDLED”(其值為 0x0000008E )的 BSOD (俗稱藍屏錯誤)。

 

當異常處理過程在用戶態中

  當 PreviousMode UserMode 時,表示是用戶模式下產生的異常。此時 KiDispatchException 函數仍然會檢測內核調試器是否存在。如果內核調試器存在,會優先把控制權交給內核調試器進行處理。所以,使用內核調試器調試用戶態程序是完全可行的,並且不依賴進程的調試端口。在大多數情況下,內核調試器對用戶態的異常不感興趣,也就不會去處理它,此時 nt!KiDispatchException 函數仍然像處理內核態異常一樣按兩次處理機會進行分發,主要過程如下。

① 如果發生異常的程序正在被調試,那么將異常信息發送給正在調試它的用戶態調試器,給調試器第1次處理機會;如果沒有被調試,跳過本步。
② 如果不存在用戶態調試器或調試器未處理該異常,那么在棧上放置 EXCEPTION_RECORDCONTEXT 兩個結構,並將控制權返回用戶態 ntdll.dll 中的 KiUserExceptionDispatcher 函數,由它調用  ntdll!RtIDispatchException 函數進行用戶態的異常處理。這一部分涉及 SEH VEH 兩種異常處理機制。其中,SEH 部分包括應用程序調用 API 函數 SetUnhandledExceptionFilter 設置的頂級異常處理,但如果有調試器存在,頂級異常處理會被跳過,進入下一階段的處理,否則將由頂級異常處理程序進行終結處理(通常是顯示一個應用程序錯誤對話框並根據用戶的選擇決定是終止程序還是附加到調試器)。如果沒有調試器能附加於其上或調試器還是處理不了異常,系統就調用 ExitProcess 函數來終結程序。

③ 如果 ntdlRtIDispatchException 函數在調用用戶態的異常處理過程中未能處理該異常,那么異常處理過程會再次返回 nt!KiDispatchExoception,它將再次把異常信息發送給用戶態的調試器,給調試器第2次處理機會。如果沒有調試器存在,則不會進行第2次分發,而是直接結束進程。

④ 如果第2次機會調試器仍不處理, nt!KiDispatchException 會再次嘗試把異常分發給進程的異常端口進行處理。該端口通常由子系統進程 csrss.exe 進行監聽。子系統監聽到該錯誤后,通常會顯示一個“應用程序錯誤”對話框,如果沒有調試器能附加於其上,或者調試器還是處理不了異常,系統就調用 ExitProcess 函數來終結程序。

 ⑤ 在終結程序之前,系統會再次調用發生異常的線程中的所有異常處理過程,這是線程異常處理過程所獲得的清理未釋放資源的最后機會,此后程序就終結了。

 

SEH 相關的數據結構

TIB (Thread Information Block,線程信息塊)是保存線程基本信息的數據結構。在用戶模式下,它位於 TEB (Thread Environment Block,線程環境塊)的頭部,而TEB是操作系統為了保存每個線程的私有數據創建的,每個線程都有自己的TEB。在Windows 2000 DDK中、TIB的定義如下。

 

FS:[0] 即為 Exceptionlist 的地址

 

 __EXCEPTION_POINTERS 結構

  當一個異常發生時,在沒有調試器干預的情況下,操作系統會將異常信息轉交給用戶態的異常處理過程。實際上,由於同一個線程在用戶態內核態使用的是兩個不同的棧,為了讓用戶態的異常處理程序能夠訪問與異常相關的數據,操作系統必須把與本次異常相關聯的 EXCEPTION_RECORD 結構和 CONTEXT 結構放到用戶態棧中,同時在棧中放置一個 _EXCEPTION_POINTERS 結構,它包含兩個指針,一個指向 EXCEPTION_RECORD 結構,另一個指向 CONTEXT 結構,示例如下。

 

看一下 RtlDispatchException 函數的代碼:

總體的流程是:

① 首先調用 VEH 異常處理,返回值不是 EXCEPTION_CONTINUE_SEARCH 就結束異常分發

② 返回值符合要求,則查詢 SEHOP 是否啟用,未開啟則會進行校驗(對每個 Record 結構體都會進行驗證)

③ 對 Handler 進行增強驗證,即 SafeSEH 機制(通過調用函數 RtlIsValidHandler 來實現)

④ 開始依次執行 Handler,並對返回值進行對比(Switch-Case),執行相應的操作

⑤ 必會執行的最后一步,調用 RtlCallVectoredContinueHandlers 函數,然后程序返回

RtlIsValidHandler()
//Exception Flags
#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

//MmExecutionFlags on Win7
#define MEM_EXECUTE_OPTION_DISABLE 0x1 
#define MEM_EXECUTE_OPTION_ENABLE 0x2
#define MEM_EXECUTE_OPTION_DISABLE_THUNK_EMULATION 0x4
#define MEM_EXECUTE_OPTION_PERMANENT 0x8
#define MEM_EXECUTE_OPTION_EXECUTE_DISPATCH_ENABLE 0x10
#define MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE 0x20
#define MEM_EXECUTE_OPTION_DISABLE_EXCEPTIONCHAIN_VALIDATION 0x40
#define MEM_EXECUTE_OPTION_VALID_FLAGS 0x7f

//NtGlobalFlag
#define FLG_ENABLE_CLOSE_EXCEPTIONS     0x00400000      // kernel mode only
#define FLG_ENABLE_EXCEPTION_LOGGING    0x00800000      // kernel mode only

//Part of ProcessInformationClass
#define ProcessExecuteFlags    34

typedef struct _DISPATCHER_CONTEXT {
    PEXCEPTION_REGISTRATION_RECORD RegistrationPointer;
} DISPATCHER_CONTEXT;

//
// Execute handler for exception function prototype.
//

EXCEPTION_DISPOSITION
    RtlpExecuteHandlerForException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PVOID EstablisherFrame,
    IN OUT PCONTEXT ContextRecord,
    IN OUT PVOID DispatcherContext,
    IN PEXCEPTION_ROUTINE ExceptionRoutine
    );

VOID
    RtlpGetStackLimits (
    OUT PULONG LowLimit,
    OUT PULONG HighLimit
    );

EXCEPTION_DISPOSITION
    RtlCallVectoredExceptionHandlers (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN OUT PCONTEXT ContextRecord
    );

EXCEPTION_DISPOSITION
    RtlCallVectoredContinueHandlers (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN OUT PCONTEXT ContextRecord
    );

PEXCEPTION_REGISTRATION_RECORD
    RtlpGetRegistrationHead (
    VOID
    );

BOOLEAN
    RtlIsValidHandler (
    IN PEXCEPTION_ROUTINE Handler,
    IN ULONG ProcessExecuteFlag
    );

BOOLEAN __stdcall RtlDispatchException(PEXCEPTION_RECORD pExcptRec, CONTEXT *pContext)
{
    BOOLEAN Completion; 
    PEXCEPTION_RECORD pExcptRec;
    EXCEPTION_REGISTRATION_RECORD *RegistrationPointerForCheck;
    EXCEPTION_REGISTRATION_RECORD *RegistrationPointer;
    EXCEPTION_REGISTRATION_RECORD *NestedRegistration;
    EXCEPTION_DISPOSITION Disposition; 
    EXCEPTION_RECORD ExceptionRecord1;
    DISPATCHER_CONTEXT DispatcherContext;
    ULONG ProcessExecuteOption;
    ULONG StackBase,StackLimit; 
    BOOLEAN IsSEHOPEnable;
    NTSTATUS status;

    Completion = FALSE;

    // 首先調用VEH異常處理例程,其返回值包括 EXCEPTION_CONTINUE_EXECUTION (0xffffffff)和 EXCEPTION_CONTINUE_SEARCH (0x0)兩種情況
    // 這是從Windows XP開始加入的新的異常處理方式
    // 返回值不是 EXCEPTION_CONTINUE_SEARCH,那么就結束異常分發過程
    if (RtlCallVectoredExceptionHandlers(pExcptRec, pContext)  !=  EXCEPTION_CONTINUE_SEARCH )
    {
        Completion = TRUE;
    }
    else
    {
        // 獲取棧的內存范圍
        RtlpGetStackLimits(&StackLimit, &StackBase);
        ProcessExecuteOption = 0;

        // 從fs:[0]獲取SEH鏈的頭節點
        RegistrationPointerForCheck = RtlpGetRegistrationHead();

        // 默認假設SEHOP機制已經啟用,這是一種對SEH鏈的安全性進行增強驗證的機制
        IsSEHOPEnable = TRUE; 

        // 查詢進程的ProcessExecuteFlags標志,決定是否進行SEHOP驗證
        status = ZwQueryInformationProcess(NtCurrentProcess(), ProcessExecuteFlags, &ProcessExecuteOption, sizeof(ULONG), NULL) ;

        // 在查詢失敗,或者沒有設置標志位時,進行SEHOP增強驗證
        // 也就是說,只有在明確查詢到禁用了SEHOP時才不會進行增強驗證
        if ( NT_SUCCESS(status) 
            && (ProcessExecuteOption & MEM_EXECUTE_OPTION_DISABLE_EXCEPTIONCHAIN_VALIDATION) )
        {
            // 若確實未開啟SEHOP增強校驗機制,設置此標志
            IsSEHOPEnable = FALSE; 
        }
        else
        {
            // 否則,進行開始SEHOP驗證
            if ( RegistrationPointerForCheck == -1 )
                break;

            //驗證SEH鏈中各個結點的有效性並遍歷至最后一個結點
            do
            {
                // 若發生以下情況,認為棧無效,此時不再執行基於棧的SEH處理
                    // 1.SEH節點不在棧中
                if ( (ULONG)RegistrationPointerForCheck < StackLimit 
                    || (ULONG)RegistrationPointerForCheck + 8 > StackBase
                    // 2.SEH節點的位置沒有按ULONG對齊
                    || (ULONG)RegistrationPointerForCheck & 3 
                    // 3.Handler在棧中
                    || ((ULONG)RegistrationPointerForCheck->Handler < StackLimit || (ULONG)RegistrationPointerForCheck->Handler >= StackBase) )
                {
                    pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID;
                    goto DispatchExit;
                }
                // 取SEH鏈的下一個結點
                RegistrationPointerForCheck = RegistrationPointerForCheck->Next;
            }
            while ( RegistrationPointerForCheck != -1 );

            // 此時RegistrationPointerForCheck指向最后一個節點
            // 如果TEB->SameTebFlags中的RtlExceptionAttached位(第9位)被設置,但最后一個結點的Handler卻不是預設的安全SEH,那么SEHOP校驗不通過,不再執行任何SEHHandler
            if ((NtCurrentTeb()->SameTebFlags & 0x200) && RegistrationPointerForCheck->Handler != FinalExceptionHandler)
            {
                goto DispatchExit;
            }
        }
        
        // 從fs:[0]獲取SEH鏈的頭節點
        RegistrationPointer = RtlpGetRegistrationHead();
        NestedRegistration = NULL;

        // 遍歷SEH鏈表執行Handler
        while ( TRUE )
        {
            if ( RegistrationPointer == -1 ) //-1表示SEH鏈的結束
                goto DispatchExit;

            // 若SEHOP機制未開啟,則這里必須進行校驗,反之則不需要,因為SEHOP機制已經驗證過了
            if ( !IsSEHOPEnable )
            {
                if ( (ULONG)RegistrationPointer < StackLimit 
                    || (ULONG)RegistrationPointer + 8 > StackBase 
                    || (ULONG)RegistrationPointer & 3 
                    || ((ULONG)RegistrationPointer->Handler < StackLimit || (ULONG)RegistrationPointer->Handler >= StackBase) )
                {
                    pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID;
                    goto DispatchExit;
                }
            }

            // 調用RtlIsValidHandler對Handler進行增強驗證,也就是SafeSEH機制
            if (!RtlIsValidHandler(RegistrationPointer->Handler, ProcessExecuteOption))
            {
                pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID;
                goto DispatchExit;
            }

            // 執行SEHHandler
            Disposition = RtlpExecuteHandlerForException(pExcptRec, RegistrationPointer, pContext, &DispatcherContext, RegistrationPointer->Handler);
            if ( NestedRegistration == RegistrationPointer )
            {
                pExcptRec->ExceptionFlags &=  (~EXCEPTION_NESTED_CALL);
                NestedRegistration = NULL;
            }

            // 檢查SEHHandler的執行結果
            switch(Disposition)
            {
            case ExceptionContinueExecution :
                if ((ExceptionRecord->ExceptionFlags &
                    EXCEPTION_NONCONTINUABLE) != 0) {
                        ExceptionRecord1.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION;
                        ExceptionRecord1.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                        ExceptionRecord1.ExceptionRecord = ExceptionRecord;
                        ExceptionRecord1.NumberParameters = 0;
                        RtlRaiseException(&ExceptionRecord1);

                } else {
                    Completion = TRUE;
                    goto DispatchExit;
                }

            case ExceptionContinueSearch :
                if (ExceptionRecord->ExceptionFlags & EXCEPTION_STACK_INVALID)
                    goto DispatchExit;

                break;

            case ExceptionNestedException :
                ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;
                if (DispatcherContext.RegistrationPointer > NestedRegistration) {
                    NestedRegistration = DispatcherContext.RegistrationPointer;
                }

                break;

            default :
                ExceptionRecord1.ExceptionCode = STATUS_INVALID_DISPOSITION;
                ExceptionRecord1.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                ExceptionRecord1.ExceptionRecord = ExceptionRecord;
                ExceptionRecord1.NumberParameters = 0;
                RtlRaiseException(&ExceptionRecord1);
                break;
            }

            // 取SEH鏈的下一個結點
            RegistrationPointer = RegistrationPointer->Next;           // Next
        }
    }

DispatchExit:

    // 調用VEH的ContinueHandler
    // 只要RtlDispatchException函數正常返回,那么ContinueHandler總會在SEH執行完畢后被調用
    RtlCallVectoredContinueHandlers(pExcptRec, pContext);
    return Completion;
}
RtlDispatchException()

函數 RtlExecuteHandlerForException 的返回值 _Exception_Disposition 有:

(1)  ExceptionContinueExecution

異常已處理,根據 Contex 結構體的相關信息恢復線程的執行

(2)  ExceptionContinueSearch

回調函數不能處理該異常,需要交由鏈表中的下一個函數來處理

(3)  ExceptionNestedException

回調函數在處理該異常時也發生了異常,即嵌套異常。在內核中發生將會停止系統的運行

(4)  ExceptionCollidedUnwind

回調函數在進行異常展開操作時發生了異常。展開操作可以理解為恢復發生異常的第一現場,並在恢復過程中的系統資源進行回收。展開操作由系統在處理異常時進行,用戶自定義的回調函數通常不返回這個值。

  后兩個返回值一般只見於系統內部的處理過程,用戶自定義的回調函數只返回前兩個值。

由書作者給出的程序,放入 X64 Dbg 中可知,之前在 ReverseCore 中看到的 SEH 的例子也是由匯編代碼寫出來,即從系統層面執行 SEH,並沒有使用高級語言

SEH 鏈的最后一個函數是系統設置的終結處理函數

 

異常處理函數的棧展開(RtlUnwind函數)

ExceptionRecord 結構的 ExceptionFlags 成員有三個值 012,分別代表 可修復異常不可修復異常棧展開

  棧展開:所有回調函數都不處理異常時,系統在終結程序之前會調用發生異常的線程中所有注冊的回調函數(被調用的函數執行自定義的操作,調用順序是:從Fs:[0]開始依次到目前正在執行的異常處理函數之前,不包括當前回調函數)。在開始調用前,ExceptionFlags 會被置為 2,ExceptionCode 會被置為 STATUS_UNWIND(0x0C0000027),回調的目的是給程序一個清理占用的資源以及保存關鍵變量的值等等操作。利用匯編代碼寫 SEH 時可以不執行這個函數。 

  棧展開的另一個目的是,如果在調用了多重函數並在內層函數遇到異常,返回到最外層函數后,經過一系列的 Push 操作,而再次遇到異常時,由於 Fs:[0] 的值指向的還是原來的棧地址,會造成程序出現問題。

 

MSC 編譯器對線程異常處理的增強

  上面那些例子都是用匯編語言寫的。但是,這個操作過程是極其不便的,尤其對高級語言來說,直接操作寄存器、讀寫fs[0]並不合適且非常煩瑣。為此,各主流編譯器都對 SEH 機制進行了擴充和增強,使程序員能更簡便地使用異常處理機制。所以,在現實程序設計中,除了保護殼、反調試等特殊用途,基本上沒有直接使用系統的SEH 機制,而是使用編譯器提供的增強版本。C語言是Windows操作系統的開發語言,下面就看看微軟的 C 編譯器(MSC)提供的增強版本異常處理機制。

typedef struct _EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION;
//異常回調函數原型 typedef EXCEPTION_DISPOSITION
(__cdecl
*PEXCEPTION_ROUTINE)(       struct _EXCEPTION_RECORD *__ExceptionRecord,
      void *_EstablisherFrame,       struct _cONTEXT *_ContextRecord,
      void *_DispatcherContext );
//C/C++運行庫使用的_SCOPETABLE_ENTRY 結構
typdef struct_SCOPETABLE_ ENTRY {   DWORD EnclosingLevel;
//上一層__try塊   PVOID FilterFunc;   //過濾表達式   PVOID HandlerFunc;   //_except 塊代碼或 _finally 塊代碼 }SCOPETABLE_ENTRY, *PSCOPETABLE_ENTRY;
struct _EH3_EXCEPTION_REGISTRATION {
  struct
_EH3_EXCEPTION_REGISTRATION *Next;
  PVOID ExceptionHandler;
  PSCOPETABLE_ENTRY scopeTable;
  DWORD TryLevel;
};

// C/C++編譯器擴展 SEH 的異常幀結構

struct CPPEH_RECORD
{
  DWORD old_esp:
  EXCEPTION_POINTERS *exc_ptr;
  struct_EH3_EXCEPTION_REGISTRATION registration;

};

  編譯器真正使用的結構是 CPPEH_RECORD,其成員 _EH3_EXCEPTION_RECISTRATION 結構是對原始的 SEH結構 _EXCEPTION_RECISTRATION_RECORD 的擴充。該結構在原始版本的基礎上增加了4個域成員(ScopeTableTrylevelold_espexc_ptr )來支持它的增強功能。

__try
{
    /*可能產生異常的代碼*/
}
__except(  /*異常篩選代碼*/ FilterFunc() }
{
    /*異常處理代碼*/ ExceptionHandler();
}

__try
{
  /*可能產生異常的代碼*/
}
__fina1ly

{
  /*終結處理代碼*/

  FinallyHandler();

}

  __try / __finally 暫時可以看成 __try /__except 模型的一個特例,它本身不能處理異常,但是不管有沒有發生異常,__finally 塊總會被執行,通常用來進行一些收尾和清理工作
現在程序員再進行異常處理就非常簡單了,只要把可能產生異常的代碼用 __try 包裹起來,然后在 FilterFunc 中判斷是不是預料之中的異常即可。如果是,在 ExceptionHandler 中進行處理就可以了。而且,__try / __except ( __finally ) 結構可以進行多層嵌套,每層相應地只處理自己關心的異常。

  這里的 FilterFunc 稱為異常篩選器,它實際上是一個逗號表達式(注意,逗號表達式最后的值取決於最右邊的表達式值,因為逗號表達式從左至右計算)。我們可以在這里完成任何工作——哪怕是計算圓周率,只要最后返回符合要求的結果就可以了。

  正常來說__except 的表達式內的函數通過調用 GetExceptionCode 或 GetExceptionInformation 函數獲取異常的詳細信息,對特定的異常進行處理返回不同的值。通常有3種返回值,示例如下。

#define  EXCEPTION_EXECUTE_HANDLER  1    表示該異常在預料之中,請直接執行下面的 Exception
#define  EXCEPTION_CONTINUE_SEARCH  0    表示不處理該異常,請繼續尋找其他處理程序
#define  EXCEPTION_CONTINUE_EXECUTION  -1  表示該異常已被修復,請回到異常現場再次執行

后面給的示例代碼直接在 Printf 函數后面跟上了返回值,省去了進一步的調用和返回,_except 根據表達式的值判斷進行什么操作,所以有點不太好理解。

 

編譯器的 SEH 增強設計

  按照原始版本的設計,每一對“觸發異常/處理異常”都有一個注冊信息,即 EXCEPTION_REGISTRATION_RECORD (以下簡稱“ERR結構”)。也就是說,按照原始設計,每一個__tryl_except(_finally)都應該對應於一個 ERR 結構。但是,而 MSC 編譯器的實現是:每個使用_tryl_except (_finally)的函數,不管其內部嵌套或反復使用多少_tryl_exeept ( _finally),都只注冊1遍,即只將1個ERR 結構掛入當前線程的異常鏈表中。每對 __try / _except(_finally) 稱為一個 Try 塊。對於遞歸函數,每次調用都會創建一個ERR結構,並將其掛入線程的異常鏈表。

  對於多個 Try 塊而只有一個 Handler MSC 提供一個代理函數,即 _EH3_EXCEPTION_RECISTRATION:ExceptionHandler 被設置為 MSC 的某個庫函數。開發人員提供的多個 __exeept 塊被存儲在 _EH3_EXCEPTION_REGISTRATION::ScopeTable 數組中。

  在由 VC6.0 編譯生成的程序中,這個由編譯器提供的異常處理函數叫作 _except_handler3 ,它實際上是公共庫的一部分系統的每個 DLL 里都有這個函數的實現。根據編譯設置的不同,它的具體實現可能出現在 msvcrt.dl、kernel32.dll 這樣的公共庫中,也可能直接內聯exe 本身,但具體代碼都是一樣的。這樣,當異常發生時,系統會根據 SEH 鏈表找到_except_handler3 函數並執行它,_except_handler3 再根據編譯時生成的 ScopeTable 執行開發人員提供的 FilterFuncHandlerFunc ,也就是說,編譯器提供的異常處理函數實現了一層代理的工作

 

編譯器的實際工作

① 把 FilterFuncHandlerFunc 編譯成單獨的函數FilterFunc 代碼塊和 Finally 代碼塊的尾部都有 retn 指令;而 HandlerFunc 尾部有 Jmp 指令,它跳轉到 Try 塊的結束;它們都是以函數的形式被調用的),並按照 Try 塊出現的順序及嵌套關系生成正確的 ScopeTable。

編譯器對 SCOPETABLE_ENTRY 是這樣定義的:對 __try / __except 組合來說,其中的 FilterFunc 就是過濾表達式代碼塊,HandlerFunc 就是異常處理代碼塊;對 __try / __finally 組合來說,FilterFunc 被置為 “NULL”, HandlerFunc 就是終結處理代碼塊。

對於函數中的每一個 Try 塊,編譯器都會生成一個 SCOPETABLE_ENTRY ,並按照 Try 塊的出現順序確定各個 Try 塊的索引,索引值遵從C語言的習慣(從0開始)。在執行對應的 Try 塊之前,編譯器會把當前 Try 塊的索引保存到 _EH3_EXCEPTION_RECISTRATION 結構的 TryLevel 成員中,它是函數中當前 Try 塊的索引。每個 SCOPETABLE_ENTRY 結構中的 EnclosingLevel 則表示:如果當前 Try 塊未能處理異常,那么要尋找下一級 Try 塊(也就是包含當前 Try 塊的父 Try 塊)的索引。如果它的值是 -1,表示當前塊已經沒有父塊了,當前函數已經不能處理該異常了。同時,-1也是 TryLevel 的默認值。

② 在函數開頭布置 CPPEH_RECORD 結構,並安裝 SEH 異常處理函數。在由 VC 6.0 編譯的程序中,這個函數的名字是 _except_handler3。如果想詳細分析 _except_handler3 函數,了解棧中的數據布局是非常有必要的。

③ 在進入每個 Try 塊之后,先設置當前 Try 塊的 TryLevel 值,再執行 Try 塊內的代碼。退出 Try 塊保護區域后,恢復為原來的 TryLevel.

④ 在函數返回前,卸載 SEH 異常處理函數。

 

VOID TwoException()
{
    int *pValue = NULL ;
    __try
    {
        printf("In Try0.\n");
        __try
        {
            printf("In Try1.\n");
            *pValue = 0x55555555;
        }
        __except(printf("In Filter1\n"),EXCEPTION_CONTINUE_SEARCH)
        {
            printf("In Handler1.\n");
        }
    }
    __except(printf("In Filter0\n"),EXCEPTION_EXECUTE_HANDLER)
    {
        printf("In Handler0.\n");
    }
    
    printf("After All Trys.\n");
}
Two Exceptions
.text:00401200                   sub_401200 proc near          ; CODE XREF: _main+F↑p
.text:00401200
.text:00401200                   ms_exc= CPPEH_RECORD ptr -18h
.text:00401200
.text:00401200                   ; __unwind { // __except_handler3
.text:00401200 55                push    ebp
.text:00401201 8B EC             mov     ebp, esp
.text:00401203 6A FF             push    0FFFFFFFFh
.text:00401205 68 10 71 40 00    push    offset stru_407110
.text:0040120A 68 30 17 40 00    push    offset __except_handler3
.text:0040120F 64 A1 00 00 00 00 mov     eax, large fs:0
.text:00401215 50                push    eax
.text:00401216 64 89 25 00 00 00+mov     large fs:0, esp
.text:0040121D 83 EC 0C          sub     esp, 0Ch
.text:00401220 53                push    ebx
.text:00401221 56                push    esi
.text:00401222 57                push    edi
.text:00401223 89 65 E8          mov     [ebp+ms_exc.old_esp], esp
.text:00401226 33 F6             xor     esi, esi
.text:00401228                   ;   __try { // __except at loc_401293
.text:00401228 89 75 FC          mov     [ebp+ms_exc.registration.TryLevel], esi
.text:0040122B 68 B0 82 40 00    push    offset aInTry0        ; "In Try0.\n"
.text:00401230 E8 D1 03 00 00    call    sub_401606
.text:00401235 83 C4 04          add     esp, 4
.text:00401235                   ;   } // starts at 401228
.text:00401238                   ;   __try { // __except at loc_401267
.text:00401238                   ;     __try { // __except at loc_401293
.text:00401238 C7 45 FC 01 00 00+mov     [ebp+ms_exc.registration.TryLevel], 1
.text:0040123F 68 A4 82 40 00    push    offset aInTry1        ; "In Try1.\n"
.text:00401244 E8 BD 03 00 00    call    sub_401606
.text:00401249 83 C4 04          add     esp, 4
.text:0040124C C7 06 55 55 55 55 mov     dword ptr [esi], 55555555h
.text:0040124C                   ;     } // starts at 401238
.text:0040124C                   ;   } // starts at 401238
.text:00401252                   ;   __try { // __except at loc_401293
.text:00401252 89 75 FC          mov     [ebp+ms_exc.registration.TryLevel], esi
.text:00401255 EB 4C             jmp     short loc_4012A3
.text:00401257                   ; ---------------------------------------------------------------------------
.text:00401257
.text:00401257                   loc_401257:                   ; DATA XREF: .rdata:stru_407110↓o
.text:00401257                   ;   __except filter // owned by 401238
.text:00401257 68 98 82 40 00    push    offset aInFilter1     ; "In Filter1\n"
.text:0040125C E8 A5 03 00 00    call    sub_401606
.text:00401261 83 C4 04          add     esp, 4
.text:00401264 33 C0             xor     eax, eax
.text:00401266 C3                retn
.text:00401267                   ; ---------------------------------------------------------------------------
.text:00401267
.text:00401267                   loc_401267:                   ; DATA XREF: .rdata:stru_407110↓o
.text:00401267                   ;   __except(loc_401257) // owned by 401238
.text:00401267 8B 65 E8          mov     esp, [ebp+ms_exc.old_esp]
.text:0040126A 68 88 82 40 00    push    offset aInHandler1    ; "In Handler1.\n"
.text:0040126F E8 92 03 00 00    call    sub_401606
.text:00401274 83 C4 04          add     esp, 4
.text:00401274                   ;   } // starts at 401252
.text:00401277                   ;   __try { // __except at loc_401293
.text:00401277 C7 45 FC 00 00 00+mov     [ebp+ms_exc.registration.TryLevel], 0
.text:0040127E EB 23             jmp     short loc_4012A3
.text:00401280                   ; ---------------------------------------------------------------------------
.text:00401280
.text:00401280                   loc_401280:                   ; DATA XREF: .rdata:stru_407110↓o
.text:00401280                   ;   __except filter // owned by 401228
.text:00401280                   ;   __except filter // owned by 401238
.text:00401280                   ;   __except filter // owned by 401252
.text:00401280                   ;   __except filter // owned by 401277
.text:00401280 68 7C 82 40 00    push    offset aInFilter0     ; "In Filter0\n"
.text:00401285 E8 7C 03 00 00    call    sub_401606
.text:0040128A 83 C4 04          add     esp, 4
.text:0040128D B8 01 00 00 00    mov     eax, 1
.text:00401292 C3                retn
.text:00401293                   ; ---------------------------------------------------------------------------
.text:00401293
.text:00401293                   loc_401293:                   ; DATA XREF: .rdata:stru_407110↓o
.text:00401293                   ;   __except(loc_401280) // owned by 401228
.text:00401293                   ;   __except(loc_401280) // owned by 401238
.text:00401293                   ;   __except(loc_401280) // owned by 401252
.text:00401293                   ;   __except(loc_401280) // owned by 401277
.text:00401293 8B 65 E8          mov     esp, [ebp+ms_exc.old_esp]
.text:00401296 68 6C 82 40 00    push    offset aInHandler0    ; "In Handler0.\n"
.text:0040129B E8 66 03 00 00    call    sub_401606
.text:004012A0 83 C4 04          add     esp, 4
.text:004012A0                   ;   } // starts at 401277
.text:004012A3
.text:004012A3                   loc_4012A3:                   ; CODE XREF: sub_401200+55↑j
.text:004012A3                                                 ; sub_401200+7E↑j
.text:004012A3 C7 45 FC FF FF FF+mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:004012AA 68 58 82 40 00    push    offset aAfterAllTrys  ; "After All Trys.\n"
.text:004012AF E8 52 03 00 00    call    sub_401606
.text:004012B4 83 C4 04          add     esp, 4
.text:004012B7 8B 4D F0          mov     ecx, [ebp+ms_exc.registration.Next]
.text:004012BA 64 89 0D 00 00 00+mov     large fs:0, ecx
.text:004012C1 5F                pop     edi
.text:004012C2 5E                pop     esi
.text:004012C3 5B                pop     ebx
.text:004012C4 8B E5             mov     esp, ebp
.text:004012C6 5D                pop     ebp
.text:004012C7 C3                retn
.text:004012C7                   ; } // starts at 401200
.text:004012C7                   sub_401200 endp
反編譯得到的代碼

需要注意,最外層的異常篩選器以及處理函數從屬的代碼段

他們分別覆蓋了__except 代碼段和編譯器自己加的代碼,對 TryLevel 進行賦值時也會檢測其是否產生異常

 

內層的異常篩選器以及處理函數

只包含了 __except 代碼段:

 

__except_handler3 函數流程解析

__except_handler3函數主要按以下流程工作。

① 在棧上生成一個EXCEPTION_POINTES結構,並將其保存到 [ebp-10] 處。
② 獲取當前的 TryLevel,判斷其值是否等於 -1。若等於,則表示當前不在 Try 塊中(嵌套的里面也沒有能處理的),返回 ExceptionContinueSearch,繼續尋找其他異常處理程序。
③ 若 TryLevel 的值不等於-1,並根據 TryLevel 在 ScopeTable 中找到相應的 SCOPTETABLE_ENTRY,判斷 FilterFunc 是否為“NULL”。若為“NULL”,說明是 __try / __finally組合。因為該組合不直接處理異常,所以也返回 ExceptionContinueSearch
④ 若FilterFunc 不為“NULL”,說明是  __try / __except 組合,那么執行 FilterFunc,然后判斷其返回值(也就是前面講到的3種返回值),根據返回值的不同執行不同的動作。EXCEPTION_CONTINUE_SEARCH 和 EXCEPTION_CONTINUE_EXECUTION 的意義比較明確,就不多介紹了。若返回值是 EXCEPTION_EXECUTE_HANDLER,就是去執行  HandlerFunc,執行完畢會跳轉到當前 Try 塊的結束位置,同時表示本次異常處理結束,此時 _except_handler3 將不返回
⑤ 如果異常沒有被處理,最后會由系統默認的異常處理函數進行處理,它在展開時會調用 finally 塊的代碼

  因為幾乎所有由 MSC 編譯生成的 sys、dll 、exe文件都需要使用 _except_handler3 異常處理函數,並且都需要進行 SEH 的安裝和卸載,所以編譯器把這部分代碼提取出來,形成了兩個獨立的函數,分別叫作 _SEH_prolog 和 _SEH_epilog。它們的主要作用就是把 _except_handler3 安裝為 SEH 處理函數及卸載,這也是在反匯編那些使用了 SEH 的系統 API 時總會看到如下代碼的原因。

更新一點版本的編譯器增加了 SecurityCookie ,防止緩沖區溢出而設置的棧驗證機制(即GS 保護機制),在函數開頭會對棧中的 ScopeTable 使用 Cookie 進行加密,異常函數也變成了 _except_handler4。

 

C++ 的異常處理

 

多個 Catch 用於捕獲多個異常

上面代碼中的 stru_40DE48 實際指向了一個非常復雜的結構,它主要包含各個 try 塊、 catch 塊的位置信息、異常類型信息等,而 _CxxFrameHandler 的工作與 _except_handler3有很多相似之處,也需要定位發生異常的 try 塊、匹配異常類型並執行相應的 catch 塊。C++ 涉及各種對象的操作,復雜度比 C 要高很多。

 

頂層異常處理

頂層(Top-level)異常處理是系統設置的一個默認異常處理程序,所有在線程中發生的異常,只要沒有被線程異常處理過程或調試器處理最終均交由頂層異常回調函數處理

Win XP 系統中:

進程的實際啟動位置是 kemel32!BaseProcessStartTunk ,然后才跳轉到 kernel32!BaseProcessStart ,它的反匯編結果如下。

.text:70817054 ; int __stdcall BaseProcessstart (PVOID ThreadStartAddress)
.text:7C817054  uExitCode    dword ptr -1Ch
.text:7C817054  ms_exc = CPPEH_RECORD ptr -18h
.text:7C817054  ThreadStartAddress = dword ptr 8
.text:7C817054                    push      0ch
.text:7C817056                    push     offset stru_7C817080
.text:7C81705B                    call__SEH_prolog
.text:7C817060                    and      [ebp+ ms_exc.registration.TryLevel],0
.text:7C817064                    push       4   ; ThreadInformationLength
.text:7C817066                    lea       eax, [ebp+ ThreadstartAddress]
.text:7C817069                    push       eax  ; ThreadInformation
.text:7C81706A                    push       9   ; ThreadInformationclass
.text:7C81706C            push      0FFFFFFEEh  ; ThreadHandle
.text:7C81706E            call      ds: NtSetInformationThread(x, x, x, x)
.text:7C817074            call   [ebp+ThreadStartAddress]
.text:7C817077            push     eax    ; dwExitcode
.text:7C817078            call     ExitThread (x)
.text:7C817078            __stdcall   BaseProcessStart(x) endp

等同於下面的 C 代碼

在使用 CreatThread 函數創建線程時,線程運行的起點是 kernel!BaseStartThunk ,而后跳轉到 kernel32!BaseThreadStart,並由該函數執行 ThreadProcBaseThreadStart 也包含上述的異常處理代碼,並且二者的 FilterFunc 都是 kernel!UnhandledExceptionFilter 。操作系統在執行任意一個用戶線程(不管是不是主線程)之前,都已經為它安裝了一個默認的 SEH 處理程序,這是該線程的第1個 SEH 處理程序,即頂層異常處理程序

 

(1)對預定錯誤的預處理

①檢測當前異常中是否是嵌套了異常,即異常處理的過程中是否又產生了異常。由於在這種情況下已經很難恢復現場和執行后續的異常處理過程了,UEF 函數會直接調用 NtTerminateProcess 結束當前進程。這大概解釋了為什么明明設置了錯誤報告但是某些程序在出錯退出時卻依然悄無聲息這個問題。

② 檢測異常代碼是不是 EXCEPTION_ACCESS_VIOLATION (0xc0000005),以及引起異常的操作是不是寫操作。如果是,會進一步檢測要寫入的內存位置是否在資源段中,然后通過改變頁屬性來嘗試修復該錯誤。
③ 檢測當前進程是否正在被調試,這是通過查詢當前進程的 DebugPort 實現的。如果進程正在被調試,那么 UEF 函數會打印一些調試信息並返回 EXCEPTION_CONTINUE_ SEARCH,也就是不進行后續的終結處理。由於這已經是最后一個異常處理程序了,該返回值會導致異常進行第2次分發。如果想調試后面的代碼,在這里必須通過調試器干預 UEF 的查詢結果,使它認為調試器不存在。例如,使 NtQueryInformationProcess 函數返回失敗,或者使查詢到的 ProcessDebugPort 值為0。

 

(2)調用用戶設置的回調函數

  為了在 UEF 階段給用戶一個干預的機會、微軟提供了一個API函數 SetUnhandledExceptionFilter 。用戶設置一個頂層異常過濾回調函數,在 kernel32!UnhandledExceptionFilter 中會調用它並根據它的返回值進行相應的操作,平時所說的“頂層異常回調函數”指的就是這個回調函數,而不是 UEF 函數。該API原型及參數類型定義如下。

API 函數 kernel32!SetUnhandledExceptionFilter 實際上把用戶設置的回調函數地址加密並保存在一個全局變量 kernel32!BasepCurrentTopLevelFilter 中,因此:
① 不管調用這個 API 多少次,只有最后一次設置的結果才是有效的,所以在同一時刻每個進程只可能有一個有效的頂層回調函數。有些程序為了保證自己設置的異常處理過濾函數不會被其他模塊覆蓋,會在調用該函數后對其入口進行Patch,使它不再執行實際功能,這樣就保證了不會有其他模塊能夠修改這個回調函數。

② 因為系統在創建用戶線程時總會安裝頂層異常處理過程,並把 UEF 函數作為異常過濾函數,所以該全局變量不僅對所有已經創建了的線程有效,對那些尚未“出生”的線程同樣有效。這就為什么頂層異常處理是基於 SEH 和線程的,而它的有效范圍卻是整個進程。

UEF 函數會判斷用戶有沒有設置回調函數,如果設置了就會進行調用。由於實際的異常過濾函數是 UEF 函數,用戶設置的回調函數只是它的一個子函數調用,回調函數的返回值只在某些情況下等於UEF 函數的返回值。異常過濾函數有3種有效的返回值。

• EXCEPTION_EXECUTE_HANDLER:表示異常已經被頂層異常處理過程處理了,這會使異常處理程序執行 HandlerFunc,也就是退出當前線程(服務程序)或進程(非服務進程),操作系統不會出現非法操作框。如果在回調函數中已經做了必要的收尾工作,可利用返回該值來優雅地結束程序。
 EXCEPTION_CONTINUE_EXECUTION:表示頂層異常處理過程處理了異常,程序應該從原異常發生的指令處繼續執行(Contex 結構體)。如果回調函數要這么做,那么在返回之前應該做出必要的修復異常現場的動作,這一點與普通的SEH處理程序是一樣的。

 EXCEPTION_CONTINUE_SEARCH:表示頂層異常處理過程不能處理異常,需要將異常交給其他異常處理過程繼續處理,這一般會導致調用操作系統默認的異常處理過程,也就是第3階段的終結處理過程。

 

(3)終結處理如何進行嚴重依賴用戶的設置

①  檢查應用程序是否使用 API SetErrorMode() 設置了 SEM_NOGPFAULTERRORBOX 標志。如果設置了,就不會出現任何錯誤提示,直接返回 EXCEPTION_EXECUTE_HANDLER 以結束進程。

② 判斷當前進程是否在 Job 中。如果在且設置了有未處理異常時結束,將直接結束進程

③ 讀取用戶關於JIT調試器(Just-In-Time,即時調試器)的設置,它保存在注冊表中。設置相對應的值可以帶命令行參數的將調試器附加到出錯的進程上

④ 如果經過查詢不需要,會加載 faultrep.dll,以異常信息為參數調用 ReportFault 函數,根據組策略的設置的不同,彈出不同類型的提示窗口。如果根據設置,不需要啟動錯誤報告程序,ReportFault 會直接返回。這時,會調用系統服務 NtRaiseHardError,由子系統進程 csrss.exe 進行相關操作

 

 

看下作者寫的測試代碼:

// NoSEH.cpp : Defines the entry point for the console application.
//
/*-----------------------------------------------------------------------
第8章  Windows下的異常處理
《加密與解密(第四版)》
(c)  看雪學院 www.kanxue.com 2000-2018
-----------------------------------------------------------------------*/
#include "stdafx.h"
#pragma comment(linker,"/Entry:main")
#pragma comment(linker,"/subsystem:windows")

#pragma comment(linker,"/entry:main")

// 需要手動添加這三個庫文件,本來在 代碼生成 -> 運行庫中可以選擇
// 但經過測試,發現都不行,手動添加反而可以
#pragma comment(lib, "msvcrtd.lib")
#pragma comment(lib, "vcruntimed.lib")
#pragma comment(lib, "ucrtd.lib")

__declspec(naked) void main(void)
{
    __asm
    {
        mov dword ptr fs:[0],-1
        xor eax,eax
        mov [eax],5 //向0地址寫入數據,引發內存訪問異常
        retn
    }
}
NoSEH

注意:若沒有連接微軟的符號服務器,將無法顯示出正確的符號

我的 Win XP sp3 中顯示出如下的代碼

下面的調試代碼在 Win7 x64 中得到:

可以看到,程序會先將異常篩選器指針地址解碼,然后通過 call eax 調用此函數。從 Windows Vista 開始,線程的實際入口點變成了 ntdll!RtlUserThreadStart (不再位於 kernel32.dlI 中)。該函數直接跳轉到了 ntdll!_RtIUserThreadStart,其內部調用了 RtIInitializeExceptionChain 函數(W),該函數與 SEHOP 保護機制有關。

  從以上代碼可以看到:從Windows Vista開始,UEF 的設置變成了兩級設置。相應地就有兩級接口,分別是 kernel32!SetUnhandledExceptionFilter 和ntdlI!RtlSetUnhandledExceptionFilter。當然,ntdll.dll 的接口不是公開的,用戶仍然只能使用 kernel32!SetUnhandledExceptionFilter 這個 API 來設置頂層回調函數。但是, kernel32.dll 在加載的時候會調用 ntdll!RtlSetUnhandledExceptionFilter 把回調函數設置成自己模塊內的 UnhandledExceptionFilter 函數。因此,異常到達頂層異常處理程序后,會先執行ntdll.dll 中的FilterFunc,而它會繼續調用 kernel32!UnhandledExceptionFilter (到這里就與 Windows XP 中一樣了), kernel32!UnhandledExceptionFilter 會再調用用戶設置的頂層異常回調函數。有一種特殊的情況是,如果某個 Native 程序並沒有加載 kernel32.dll,那么 ntdll.dll 本身仍然會提供一個與 kernel32!UnhandledExceptionFilter 功能相似的 ntdll!RtlUnhandledExceptionFilter 函數來完成終結處理功能。

 

頂層異常處理的典型應用模式
在程序設計中,開發人員普遍使用 SEH 機制來捕獲可能產生的異常。但 SEH 不是萬能的,總會有一些無法預料的情況發生,卻不能被 SEH 處理。所以,一般的使用模式是:使用 SEH 捕獲異常,並對那些預料之中的異常進行處理,其他無法處理的異常都會到達 UEF 函數處,由用戶設置的回調函數進行收尾處理。

於是可以把異常現場的所有信息保存下來,形成一個快照文件,作為分析異常的依據(Dump文件)。用戶可以手動把這個文件發送給開發人員,或者將文件自動上傳到專門用於收集 Dump 的服務器中(通常還會生成一個文本類的文件用於保存本次異常的一些概要信息和那些無法保存到 Dump 文件中的信息)。“拍照”時的參數不同,生成的 Dump 文件所包含信息的豐富。

// WriteDump.cpp : Defines the entry point for the console application.
// Author:achillis
// 本程序用於演示頂層異常處理的使用及生成Dump技術
//
/*-----------------------------------------------------------------------
第8章  Windows下的異常處理
《加密與解密(第四版)》
(c)  看雪學院 www.kanxue.com 2000-2018
-----------------------------------------------------------------------*/
#include "stdafx.h"
#include <WINDOWS.H>
#include <DbgHelp.h>

#pragma comment(lib,"Dbghelp.lib")

LONG WINAPI TopLevelExceptionFilter(
    struct _EXCEPTION_POINTERS* ExceptionInfo
    );

int main(int argc, char* argv[])
{
    //安裝頂層異常處理回調
    SetUnhandledExceptionFilter(TopLevelExceptionFilter);
    int *pValue = NULL;
    *pValue = 5 ; //引發內存訪問異常
    return 0;
}

LONG WINAPI TopLevelExceptionFilter(
    struct _EXCEPTION_POINTERS* ExceptionInfo
    )
{
    printf("Exception Catched, Code = 0x%08X EIP = 0x%p\n",
        ExceptionInfo->ExceptionRecord->ExceptionCode,
        ExceptionInfo->ExceptionRecord->ExceptionAddress);
    printf(".exr = 0x%p\n",ExceptionInfo->ExceptionRecord);
    printf(".cxr = 0x%p\n",ExceptionInfo->ContextRecord);

    HANDLE hDumpFile = CreateFile("Dump.dmp",
        GENERIC_WRITE,
        FILE_SHARE_READ,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    if (hDumpFile == INVALID_HANDLE_VALUE)
    {
        return EXCEPTION_CONTINUE_SEARCH;
    }

    MINIDUMP_EXCEPTION_INFORMATION MinidumpExpInfo;
    ZeroMemory(&MinidumpExpInfo,sizeof(MINIDUMP_EXCEPTION_INFORMATION));
    MinidumpExpInfo.ThreadId = GetCurrentThreadId();
    MinidumpExpInfo.ExceptionPointers = ExceptionInfo;
    MinidumpExpInfo.ClientPointers = TRUE ;

    BOOL bResult = MiniDumpWriteDump(GetCurrentProcess(),
        GetCurrentProcessId(),
        hDumpFile,
        MiniDumpWithProcessThreadData,
        &MinidumpExpInfo,
        NULL,
        NULL
        );
    
    printf("Write Dump File %s .\n",bResult ? "Success":"Failed");
    CloseHandle(hDumpFile);
    
    //Dump文件生成完畢,可以結束進程了
    return EXCEPTION_EXECUTE_HANDLER;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM