Windows異常處理核心原理理論篇
前言
這裏簡單說一下此篇文章應該怎麼看,Windows下的異常處理相對來說不是很複雜,但是內容還是蠻多的。也因此呢這個文章也應該是一個系列的開頭;在這一篇文章中,我會大概的講述Windows下異常的處理流程,是一個大概的內容,更詳盡的內容留在後面的章節慢慢來說。
理論篇篇幅應該較長,想要詳細瞭解流程的同學還請慢慢看。
異常是什麼?
怎麼解釋異常是什麼之前,得先了解我們寫程序的時候在什麼地方用到了異常;應用層使用Windows的異常處理機制主要通過了三種方式:
- 使用AddVectoredExceptionHandler函數來添加一個VEH的向量化異常處理函數;
- 使用SetUnhandledExceptionFilter函數來添加一個未處理異常的異常處理函數;
- 在C/C++中使用關鍵字__try {...} __except(...) {...} (後面我直接用__try來代替前面這一長串)的方式來實現的SEH異常處理,當然這裏並不是說SEH只能使用_try來實現的,這裏是指平時用到的SEH異常捕獲一般使用__try關鍵字;
那麼,即便是現在知道了我們常用的使用異常捕獲的方式有這些,但是這對於我們來講,對於我們來說也是使用而已;平時代碼裏面我使用__try的,Windows自己的VEH以及未處理異常的處理一般情況下不會用到。__try非常好用,我們可以在我們代碼的任何位置給我們的代碼套上這個關鍵字。
那麼,到底是什麼異常?異常是CPU或者程序發生的某種錯誤,異常處理就是異常產生之後Windows對於產生的錯誤的一段處理程序;在這裏異常分爲CPU異常和程序拋出的異常(也稱之爲模擬異常),我分開講,
CPU異常就是由CPU發現的異常,比如說,除零異常,內存訪問異常,經常我們看到的錯誤信息,類似於““0xXXXXXXX指令引用的“0xXXXXXXXX內存”,該內存不能爲“read””
看着是不是很熟悉? 那麼這類錯誤就是CPU產生的異常,這一類異常的異常代碼定義在Windows裏面可以找到。這一類異常的產生是由CPU觸發的,所以叫CPU異常;
程序拋出的異常也稱之爲模擬異常,意思就是這個異常不是由CPU觸發的,而是由程序觸發的異常。比如C++關鍵字throw,以及Windows API函數RaiseException,當然throw最終仍然會調用RaiseException;也就是這個異常是由程序觸發的,當然這個程序也包括C++庫裏面的函數。此處沒圖.jpg。
CPU異常和程序拋出異常的區別在於,他們產生異常的方式不同;CPU的異常觸發是在CPU檢測到錯誤而發出的異常;比如,如果當前正在執行應用層的代碼,但是該代碼產生了異常,那麼這個時候CPU會立刻中止執行,並且換到內核然後進入對應的異常分發函數;而程序拋出的異常則是通過ntdll!NtRaiseException函數進入內核,然後在nt!NtRaiseException的流程中調用異常處理函數。除開這一點兒,其他的都是一樣的。
異常的產生
雖然看起來這裏分成了兩種異常,但是實際上這兩種異常的最終處理方式是一模一樣的,只是異常的觸發點不一樣;
CPU異常:該異常的產生是當CPU嘗試執行指令時檢查到的問題;當異常產生時,CPU查詢中斷處理表,找到異常處理的函數(_KiTrapXX之類)-> KiTrapXX函數裏面調用CommonDipatchException-> 調用KiDispatchException進行異常分發;
模擬異常的產生:該異常的產生調用RaiseException->包裝異常->NtRaiseException->進入內核-> 調用nt!NtRaiseException-> 調用KiDispatchException進行異常分發; 其中包裝異常的意思就是,因爲這個異常是模擬的,所以這個異常的具體信息需要由程序自己來填充。
異常的分發
說起異常的分發,那麼這裏故事線就稍稍有一點兒長了。因爲Windows分爲內核(R0)和應用層(R3),Windows的異常處理機制是內核和應用層都可以使用的。所以這裏就需要進行一些區分了。因爲如果內核產生了異常,那麼自然內核的異常處理函數也在內核裏面那麼這個事情就好辦了,我出現異常了我直接處理就好了。但是應用層如果出現了異常,那麼一定他的處理函數是在應用層的,當產生異常的時候,異常的分發總是在內核,那麼如果要處理應用層的異常,那麼需要先切換到應用層然後才能去執行應用層的異常分發處理。 這裏注意了,如果覺得沒有看懂,那麼這個地方多看幾次。
內核異常的分發
內核裏面的異常處理比起應用層的異常處理來說要簡單的多。當異常產生的時候,這時候流程進入到KiDispatchException函數,在該函數內備份當前線程R3的TrapFrame(注意當處理完畢R3異常的時再次調用NtContinue需要此備份的數據),但是因爲異常是在內核觸發的,所以這個過程其實並沒有什麼用。它的用處在後面再講;首先判斷這是不是第一次異常,判斷是否存在內核調試器,如果有內核調試器,則把當前的異常信息發送給內核調試器;如果沒有內核調試器或者內核調試器沒有處理該異常,那麼便調用RtlDispatchException函數進行異常處理;但是如果RtlDispatchException函數沒有處理該異常,那麼將再次嘗試將異常發送到內核調試器,如果此時內核調試器仍然不存在或者沒有處理該異常,那麼此時系統會直接藍屏。
RtlDispatchException函數內部通過fs:[0]來獲取關於當前線程的_KPCR,它的成員_NT_TIB::ExceptionList裏面存放的是當前線程的異常處理函數鏈表;原型如下(代碼來自ReactOS):
typedef struct _KPCR {
union {
NT_TIB NtTib;
struct {
struct _EXCEPTION_REGISTRATION_RECORD *Used_ExceptionList;
PVOID Used_StackBase;
PVOID Spare2;
PVOID TssCopy;
ULONG ContextSwitches;
KAFFINITY SetMemberCopy;
PVOID Used_Self;
};
};
struct _KPCR *SelfPcr;
struct _KPRCB *Prcb;
KIRQL Irql;
ULONG IRR;
ULONG IrrActive;
ULONG IDR;
PVOID KdVersionBlock;
struct _KIDTENTRY *IDT;
struct _KGDTENTRY *GDT;
struct _KTSS *TSS;
USHORT MajorVersion;
USHORT MinorVersion;
KAFFINITY SetMember;
ULONG StallScaleFactor;
UCHAR SpareUnused;
UCHAR Number;
UCHAR Spare0;
UCHAR SecondLevelCacheAssociativity;
ULONG VdmAlert;
ULONG KernelReserved[14];
ULONG SecondLevelCacheSize;
ULONG HalReserved[16];
} KPCR, *PKPCR;
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
_ANONYMOUS_UNION union {
PVOID FiberData;
ULONG Version;
} DUMMYUNIONNAME;
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB, *PNT_TIB;
// 異常處理函數結構體
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一個異常處理結構
PEXCEPTION_ROUTINE Handler; // 異常處理函數
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
注意:這裏不同的系統會不一樣:不同的操作系統版本會出現不一樣的情況。Windows XP SP3下RtlDispatchException函數只遍歷了SEH, 但是在ReactOS裏面,內核異常先遍歷了VEH鏈表然後再遍歷SEH鏈表。
用戶態異常的分發
內核態的異常處理函數自然在內核中,那麼用戶態的異常處理自然也是在用戶態中,當用戶態異常經過KiDispatchException時,那麼便需要切換到用戶態的代碼進行異常的分發和處理。
用戶態異常的分發這裏稍微複雜一些,最主要的原因是處理此異常需要先切換到用戶空間,然後交由用戶層的異常代碼再次進行分發。詳細流程如下:
- 檢查當前是不是第一次分發該異常,爲什麼有這個標誌,因爲異常第一次如果沒有被處理,那麼第二次就不會再次調用異常處理程序,而是直接嘗試將異常發給調試器;
- 如果當前是第一次分發該異常,那麼便嘗試將異常發給內核調試器(注意:這裏是內核調試器,不是用戶調試器);
- 如果內核調試器不存在或者沒有對該異常進行處理,那麼便嘗試將異常發送給用戶態調試器;
- 如果用戶態調試不存在或者也沒有處理該異常,那麼此時邊準備一個返回ntdll!KiUserExceptionDispatcher函數的應用層調用棧,準備產生的異常的數據,然後結束本次KiDispatchException函數的運行;因爲當函數結束之後,會調用KiServiceExit返回用戶層,此時當前的TrapFrame就是準備好的用於執行ntdll!KiUserExceptionDispatcher的環境,所以當從內核退出時,用戶態線程便會從執行ntdll!KiUserExceptionDispatcher開始執行;
- ntdll!KiUserExceptionDispatcher調用ntdll!RtlDispatchException進行異常的分發,此處的流程和內核態nt!RtlDispatchException流程基本一致;
- 通過RtlCallVectoredExceptionHandlers遍歷VEH鏈表嘗試查找異常處理函數;
- 如果VEH沒有處理函數處理該異常,那麼便從fs[0]讀取ExceptionList並開始執行SEH的函數處理;
- 如果到最後仍然沒有處理該異常,這是便會再次主動調用NtRaiseException將該異常重新拋出來,但是此時就不是第一次機會了,此時NtRaiseException流程重新調用了nt!KiDispatchException,並再次進入用戶態異常的處理分支,但是此時不再是第一次異常處理,所以此次異常不會再次發給用戶態進行分發,而是再次嘗試將異常發給用戶調試器(注意:此時不會再次將異常發送給內核調試器),此時有兩次機會可以讓用戶態調試器進行異常的處理,最後如果此異常仍然沒有被用戶態調試器處理,那麼nt!KiDispatchException便會調用ZeTerminateProcess直接結束該進程。
- 如果在這一步有了異常處理程序處理了該異常,那麼這時候便會調用NtContinue,將之前保存的TrapFrame還原;(注意看內核分發裏面的標紅文本)
- 當函數從NtContinue返回時,這時候就會根據上面函數的處理結果繼續執行。
注意:這部分的描述應該藉助於源碼進行觀察。
始終都有異常處理函數的用戶層代碼
應用層異常的分發雖然看起來有可能存在找不到異常處理方案,但是應用層和內核最大的區別在於,如果內核層發生的異常沒有被正確執行,那麼此時就會產生藍屏,但是用戶層的異常沒有被處理,那麼肯定不會也是藍屏吧。當然不會了,如果最終實在是沒有異常處理程序處理該異常,那麼最終Windows會根據當前系統的設置,調用UnhandledExpctionFilter的函數,這個函數被調用之後,要麼彈出一個錯誤框,要麼啓動一個調試器等等等。也就是說,幾乎是無論如何都會有一個異常處理函數來接管最後的異常,哪怕是彈出一個框框(““0xXXXXXXX指令引用的“0xXXXXXXXX內存”,該內存不能爲“read””)。參見圖1
VEH和SEH
VEH是一個全局鏈表,全名爲Vectored Exception Handler, 這個全局鏈表裏面存放的異常處理函數可以過濾所有線程產生的異常;其處理函數的原型如下:
typedef LONG
(NTAPI *PVECTORED_EXCEPTION_HANDLER)(
struct _EXCEPTION_POINTERS *ExceptionInfo
);
VEH的註冊是通過API函數AddVectoredExceptionHandler進行註冊的;他比SEH擁有更優先的級別過濾異常;更多有關於VEH的說明請參見MSDN。
SEH是比較特殊的異常處理鏈表,全名爲Structured Exception Handler,SEH的註冊結構體只能作爲局部變量存在於當前線程的調用棧中,如果一旦結構體的地址不在當前調用棧範圍中,那麼在進行異常分發時,將不會進入該函數。SEH描述結構的註冊隨着函數的調用而註冊,隨着函數的結束而註銷。當前有關於SEH的部分還有很多,這裏就不專門針對SEH的異常處理做介紹,接下來專門再開一篇來介紹SEH,以及C++編譯器用__try來實現的SEH。
和內核態的讀取ExceptionList一樣,用戶態也是從fs[0]出讀取當前的異常處理函數鏈表。不過此時的fs[0]村裏面存放的就不是_KPCR結構,而是_TEB,不過_TEB結構的第一個成員仍然是_NT_TIB, 所以這裏就無所謂到底是哪個結構了,結構如下:
typedef struct _TEB
{
NT_TIB Tib; /* 000 */
PVOID EnvironmentPointer; /* 01c */
CLIENT_ID ClientId; /* 020 */
PVOID ActiveRpcHandle; /* 028 */
PVOID ThreadLocalStoragePointer; /* 02c */
PVOID Peb; /* 030 */
ULONG LastErrorValue; /* 034 */
ULONG CountOfOwnedCriticalSections;/* 038 */
PVOID CsrClientThread; /* 03c */
PVOID Win32ThreadInfo; /* 040 */
ULONG Win32ClientInfo[31]; /* 044 used for user32 private data in Wine */
PVOID WOW32Reserved; /* 0c0 */
ULONG CurrentLocale; /* 0c4 */
ULONG FpSoftwareStatusRegister; /* 0c8 */
PVOID SystemReserved1[54]; /* 0cc used for kernel32 private data in Wine */
PVOID Spare1; /* 1a4 */
LONG ExceptionCode; /* 1a8 */
PVOID ActivationContextStackPointer; /* 1a8/02c8 */
BYTE SpareBytes1[36]; /* 1ac */
PVOID SystemReserved2[10]; /* 1d4 used for ntdll private data in Wine */
GDI_TEB_BATCH GdiTebBatch; /* 1fc */
ULONG gdiRgn; /* 6dc */
ULONG gdiPen; /* 6e0 */
ULONG gdiBrush; /* 6e4 */
CLIENT_ID RealClientId; /* 6e8 */
HANDLE GdiCachedProcessHandle; /* 6f0 */
ULONG GdiClientPID; /* 6f4 */
ULONG GdiClientTID; /* 6f8 */
PVOID GdiThreadLocaleInfo; /* 6fc */
PVOID UserReserved[5]; /* 700 */
PVOID glDispatchTable[280]; /* 714 */
ULONG glReserved1[26]; /* b74 */
PVOID glReserved2; /* bdc */
PVOID glSectionInfo; /* be0 */
PVOID glSection; /* be4 */
PVOID glTable; /* be8 */
PVOID glCurrentRC; /* bec */
PVOID glContext; /* bf0 */
ULONG LastStatusValue; /* bf4 */
UNICODE_STRING StaticUnicodeString; /* bf8 used by advapi32 */
WCHAR StaticUnicodeBuffer[261]; /* c00 used by advapi32 */
PVOID DeallocationStack; /* e0c */
PVOID TlsSlots[64]; /* e10 */
LIST_ENTRY TlsLinks; /* f10 */
PVOID Vdm; /* f18 */
PVOID ReservedForNtRpc; /* f1c */
PVOID DbgSsReserved[2]; /* f20 */
ULONG HardErrorDisabled; /* f28 */
PVOID Instrumentation[16]; /* f2c */
PVOID WinSockData; /* f6c */
ULONG GdiBatchCount; /* f70 */
ULONG Spare2; /* f74 */
ULONG Spare3; /* f78 */
ULONG Spare4; /* f7c */
PVOID ReservedForOle; /* f80 */
ULONG WaitingOnLoaderLock; /* f84 */
PVOID Reserved5[3]; /* f88 */
PVOID *TlsExpansionSlots; /* f94 */
} TEB, *PTEB;
所以,這裏的話就是在RtlDispatchException中對SEH鏈表進行處理。SEH結構的聲明見本篇上面的結構原型EXCEPTION_REGISTRATION_RECORD;
注意:EXCEPTION_REGISTRATION_RECORD這個結構體只是一個基本的結構體,裏面第一個參數Next指向了下一個異常處理器的結構,第二個參數Handler則存放的是處理函數的指針。C++編譯器的擴展了這個結構以使__try{...}__except(...){...}可以嵌套使用。
異常分發的理論這裏暫時結束,後續我還會繼續進行補全。
如果有說錯的地方,還請指正,我會第一時間進行修正,歡迎討論。