Windows 異常處理原理

Windows運行過程中,不可避免會產生各種異常(由內核或應用程序),系統提供了一套強大的異常處理機制,靈活的使用它,可以讓我們的應用程序變的更健壯。

瞭解涉及異常處理的數據結構

IDT 系統中斷表

有異常產生時,處理器根據IDT的中斷號,找到對應的處理函數 KiTrapxx,異常處理函數會將異常封裝到一個數據結構。

typedef struct _EXCEPTION_RECORD32 {
	DWORD    ExceptionCode; //異常代碼
	DWORD ExceptionFlags;  //異常標誌
	DWORD ExceptionRecord;  //EXCEPTION_RECORD32指針
	DWORD ExceptionAddress; //產生異常時的地址
	DWORD NumberParameters; //異常附加信息數量
	DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //異常附加信息,指針指向一個數組
} EXCEPTION_RECORD32, *PEXCEPTION_RECORD32;

CONTEXT異常上下文 ,不太嚴謹,姑且這樣認爲吧,它包含了異常產生時,執行環境的詳細信息

typedef struct _CONTEXT {

	//標識哪些成員是有效的
	DWORD ContextFlags;


	//調試寄存器 
	DWORD   Dr0;
	DWORD   Dr1;
	DWORD   Dr2;
	DWORD   Dr3;
	DWORD   Dr6;
	DWORD   Dr7;


	FLOATING_SAVE_AREA FloatSave;

	//
	// This section is specified/returned if the
	// ContextFlags word contians the flag CONTEXT_SEGMENTS.
	//

	DWORD   SegGs;
	DWORD   SegFs;
	DWORD   SegEs;
	DWORD   SegDs;

	//
	// This section is specified/returned if the
	// ContextFlags word contians the flag CONTEXT_INTEGER.
	//

	DWORD   Edi;
	DWORD   Esi;
	DWORD   Ebx;
	DWORD   Edx;
	DWORD   Ecx;
	DWORD   Eax;

	//
	// This section is specified/returned if the
	// ContextFlags word contians the flag CONTEXT_CONTROL.
	//

	DWORD   Ebp;
	DWORD   Eip;
	DWORD   SegCs;              // MUST BE SANITIZED
	DWORD   EFlags;             // MUST BE SANITIZED
	DWORD   Esp;
	DWORD   SegSs;

	//
	// This section is specified/returned if the ContextFlags word
	// contains the flag CONTEXT_EXTENDED_REGISTERS.
	// The format and contexts are processor specific
	//

	BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

當KiTrapxxx處理完成並將異常信息封裝好後,轉而調用系統內核nt!KiDispatchException函數。

KiDispatchException 該函數是異常處理的核心函數,該函數代碼比較長,這裏貼出函數頭的部分代碼。

該函數會根據異常類型及是否存在調試器,來進行不同的處理。

  • 內核態

    1.如果存在內核調試器,並設置處理進度爲第一次處理,內核調試器如果不能處理異常則會中斷,重新把控制權交給用戶,如果調試器處理了異常,那麼會從發生異常的地方繼續執行指令。

    2.不存在內核調試器,或者內核調試器不能處理異常,內核就會調用RtlDispatchException函數,依次調用註冊的異常處理函數。(SEH)

    3.如果RtlDispatchException也沒有能夠處理異常,內核會重新將異常交給調試器。

    4.如果調試器仍然處理不了,此時就可以見到久違的藍屏錯誤了。(0x0000008e)

 

  • 用戶態

1.如果存在內核調試器,將異常交給調試器處理。

2.如果存在用戶態調試器,則交給調試器處理,調試器未處理該異常或者不存在調試器,KiDispatchException會壓入2個數據結構,exception_record、context,讓後將控制權返回給用戶態函數,位於ntdll!RtlDispatchException。

3.如果RtlDispatchException未能處理異常,此時會再次將異常返回給內核KiDispatchException,它會將異常重新轉給用戶態調試器,如果仍然無法處理異常,就結束進程。

4.結束進程前,系統會再次調用註冊的異常處理函數,然後會結束異常進程。

 

SEH (結構化異常處理)

前面講到的是內核異常處理流程,下面說下用戶態的異常處理。

SEH是一種異常處理機制,發生異常時,系統通過它找到處理函數,處理該異常。

涉及到的數據結構

TEB  線程環境塊(翻譯過來是這樣的)

typedef struct _TEB {
    PVOID Reserved1[12];
    PPEB ProcessEnvironmentBlock;
    PVOID Reserved2[399];
    BYTE Reserved3[1952];
    PVOID TlsSlots[64];
    BYTE Reserved4[8];
    PVOID Reserved5[26];
    PVOID ReservedForOle;  // Windows 2000 only
    PVOID Reserved6[4];
    PVOID TlsExpansionSlots;
} TEB, *PTEB;

TIB 線程信息塊

typedef struct _NT_TIB32 {
    DWORD ExceptionList;
    DWORD StackBase;
    DWORD StackLimit;
    DWORD SubSystemTib;

#if defined(_MSC_EXTENSIONS)
    union {
        DWORD FiberData;
        DWORD Version;
    };
#else
    DWORD FiberData;
#endif

    DWORD ArbitraryUserPointer;
    DWORD Self;
} NT_TIB32, *PNT_TIB32;

異常處理結構( ExceptionList成員)

typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next; //下一個處理
    PEXCEPTION_ROUTINE Handler; //異常處理函數
} EXCEPTION_REGISTRATION_RECORD;

異常處理函數聲明

typedef
_IRQL_requires_same_
_Function_class_(EXCEPTION_ROUTINE)
EXCEPTION_DISPOSITION
NTAPI
EXCEPTION_ROUTINE (
    _Inout_ struct _EXCEPTION_RECORD *ExceptionRecord, //異常信息
    _In_ PVOID EstablisherFrame, //陷阱幀
    _Inout_ struct _CONTEXT *ContextRecord, //異常上下文
    _In_ PVOID DispatcherContext // 未知
    );

typedef EXCEPTION_ROUTINE *PEXCEPTION_ROUTINE;

從XP到現在的win10,這些數據結構都沒有大的變動。

從以上流程知道,要處理異常,需要註冊SEH處理函數,SEH位於TIB頭,是一個單項鍊表,通過next指向下一個處理函數。

SEH異常處理代碼(此處代碼來自網上,做了部分調整)

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <winternl.h>


//異常處理函數
DWORD dwTest;
EXCEPTION_DISPOSITION NTAPI ExceptHandler(
	_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
	_In_ PVOID EstablisherFrame,
	_Inout_ struct _CONTEXT *ContextRecord,
	_In_ PVOID DispatcherContext) {
	printf("進入異常處理\n");
	printf("異常地址:%X<異常代碼:%X>\n", ExceptionRecord->ExceptionAddress,
		ExceptionRecord->ExceptionCode);
	ContextRecord->Eax = (DWORD)(&dwTest);
	return ExceptionContinueExecution;
}



int main()
{
	PTEB teb=NULL;

	_asm
	{
		mov eax,fs:[0]
		mov teb,eax
	}

	printf("TEB %X \n", teb);


	printf("註冊SEH\n");
	__asm {
		lea eax, ExceptHandler  //將異常處理函數地址裝入eax
		push eax //異常處理函數入棧
		push fs : [0] //構造EXCEPTION_REGISTRATION_RECORD
		mov dword ptr fs : [0], esp //將構造好的處理函數加入SEH鏈表頭部
	}

	__asm {
		xor eax, eax
		mov dword ptr[eax], 1234h  //內存訪問無效異常
	}
	printf("刪除SEH\n");

	//卸載異常處理函數並恢復堆棧
	__asm {
		pop dword ptr fs : [0]
		add esp, 4
	}

	printf("dwTest=%X\n", dwTest);

	system("pause");
}

下面在調試器中看下系統的異常處理流程,首先在內核的KiDispatchException函數下斷,運行exe。

bp 0x83ef7dc1虛擬機有問題,觸發異常後關閉,就在用戶模式下調試了。

註冊SEH前的異常處理列表

0:000> dd fs:[0]
0053:00000000  0019ff60 001a0000 0019d000 00000000
0053:00000010  00001e00 00000000 00260000 00000000
0053:00000020  00004d5c 00000308 00000000 0026002c
0053:00000030  0025d000 00000000 00000000 00000000
0053:00000040  00000000 00000000 00000000 00000000
0053:00000050  00000000 00000000 00000000 00000000
0053:00000060  00000000 00000000 00000000 00000000
0053:00000070  00000000 00000000 00000000 00000000

0x19ff60 ExceptionList地址,繼續查看 dd 0x19ff60

0:000> dd 0x19ff60
0019ff60  0019ffcc 00434bb0 af181fd2 00000000
0019ff70  0019ff80 74b50419 0025d000 74b50400
0019ff80  0019ffdc 7750662d 0025d000 6bd86c90
0019ff90  00000000 00000000 0025d000 00000000
0019ffa0  00000000 00000000 00000000 00000000
0019ffb0  00000000 00000000 00000000 00000000
0019ffc0  00000000 0019ff8c 00000000 0019ffe4
0019ffd0  775186d0 1c9b01fc 00000000 0019ffec

19ffcc 指向下一個異常處理函數,434bb0地址爲當前異常處理函數地址。

我們看下,下一個異常處理函數

0:000> dd 0x19ffcc
0019ffcc  0019ffe4 775186d0 1c9b01fc 00000000
0019ffdc  0019ffec 775065fd ffffffff 775251d2
0019ffec  00000000 00000000 0042c6e5 0025d000
0019fffc  00000000 78746341 00000020 00000001
001a000c  00003318 000000dc 00000000 00000020
001a001c  00000000 00000014 00000001 00000007
001a002c  00000034 0000017c 00000001 00000000
001a003c  00000000 00000000 00000000 00000000

0x775186d0處函數 ,這是MSVC編譯器對系統SEH異常的增強,後面會陸續講到。

 

接着安裝我們的SEH異常處理函數。

004325e6 8d05cfc34200   lea     eax, [targetexe!ILT+970(?ExceptHandlerYG?AW4_EXCEPTION_DISPOSITIONPAU_EXCEPTION_RECORDPAXPAU_CONTEXT (0042c3cf)]
004325ec 50             push    eax
004325ed 64ff3500000000 push    dword ptr fs:[0]
004325f4 64892500000000 mov     dword ptr fs:[0], esp fs:0053:00000000=0019ff60  //安裝異常處理函數
004325fb 33c0           xor     eax, eax
004325fd c70034120000   mov     dword ptr [eax], 1234h //產生內存寫入異常

查看安裝SEH後的異常處理列表

0:000> dd 19fe44
0019fe44  0019ff60 0042c3cf 00646110 00499d2c
0019fe54  0025d000 cccccccc cccccccc cccccccc
0019fe64  cccccccc cccccccc cccccccc cccccccc
0019fe74  cccccccc cccccccc cccccccc cccccccc
0019fe84  cccccccc cccccccc cccccccc cccccccc
0019fe94  cccccccc cccccccc cccccccc cccccccc
0019fea4  cccccccc cccccccc cccccccc cccccccc
0019feb4  cccccccc cccccccc cccccccc cccccccc

0x42c3cf 這裏是我們註冊的SEH處理函數地址。

內核會將異常轉到用戶態,然後調用我們註冊的處理函數,這就是windows異常處理的整體流程,後面會陸續分享涉及SEH、VEH等處理細節。

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