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等處理細節。