Windows核心讀書筆記——Windows異常分發處理機制

Ox9A82發表於2016-04-10

本篇讀書筆記主要參考自《深入解析Windows作業系統》和《軟體除錯》這兩本書。

IDT是處理異常,實現作業系統與CPU的互動的關口。

系統在初始化階段會去填寫這個結構。

IDT的每一個表項都成為門描述符,因為IDT的功能就像大門一樣,從一個空間跳到另一個空間去執行。

IDT中包含三種門描述符

  • 任務門描述符:用於任務切換
  • 中斷門描述符:用於描述中斷處理例程
  • 陷阱們描述符:用於描述異常處理例程

CPU如何使用IDT

cpu首先根據IDTR找到IDT,再利用向量號碼找到門描述符。再去判斷門描述符的型別,如果是任務描述符,CPU會執行硬體方式的任務切換,切換到描述符所定義的執行緒。

但是CPU的PCR結構中也會儲存有IDT的基地址,這個用處我沒搞懂,可能與核心初始化流程有關係,以後再來研究。

如果是陷阱或中斷描述符,那麼會去呼叫處理例程。X64架構不支援硬體方式的任務切換,也就不再存在任務門了。

 

在呼叫處理例程之前,CPU會把EFLAGS、CS、EIP壓入棧,如果發生的異常有錯誤程式碼則把錯誤程式碼也壓入堆疊。

這個過程就是所謂的構建陷阱幀(Context)和異常記錄(EXCEPTION_RECORD)

windows系統使用EXCEPTION_RECORD結構描述異常。

1 typedef struct _EXCEPTION_RECORD {
2   DWORD                    ExceptionCode;//異常程式碼
3   DWORD                    ExceptionFlags;//異常標誌
4   struct _EXCEPTION_RECORD  *ExceptionRecord;//相關的另一個異常
5   PVOID                    ExceptionAddress;//異常發生的地址
6   DWORD                    NumberParameters;//引數陣列中的引數個數
7   ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];//引數陣列
8 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;

異常程式碼,可以認為是異常的別稱,有的異常有異常程式碼有的則沒有。

在經過中斷或異常後會呼叫IDT中的處理例程比如KiTrap03,這個處理例程會呼叫CommonDispatchException函式。

CommonDispatchException函式會在棧中生成一個EXCEPTION_RECORD結構,把當前的資料情況寫入到這個結構中。

然後會以這個結構為引數呼叫KiDispatchException來分發異常。

 

上面說的是硬體異常,接下來看看軟體異常,軟體異常是通過直接或間接呼叫核心函式NtRaiseException產生的,就是說軟體異常不需要經過IDT分發等等這些東西只需要呼叫函式,而且使用者層也可呼叫這個函式,使用者層匯出了一個RaiseException函式在kernel32.dll中,供使用者產生一個自定義的異常。

RaiseException函式的實現原理是把相應的引數放入EXCEPTION_RECORD結構中,呼叫RtlRaiseException函式,這個函式把當前的執行緒上下文放入CONTEXT結構中,然後呼叫NtRaiseException。

NtRaiseException內部實現函式是KiRaiseException,我們看下面KiRaiseException的原始碼會發現這個函式還是呼叫 KiDispatchException來實現功能的。

 1   //來自WRK1.2
 2    NTSTATUS
 3    KiRaiseException (
 4        IN PEXCEPTION_RECORD ExceptionRecord,//異常記錄
 5        IN PCONTEXT ContextRecord,//執行緒上下文結構
 6        IN PKEXCEPTION_FRAME ExceptionFrame,//不使用,為空
 7        IN PKTRAP_FRAME TrapFrame,//棧幀基地址
 8        IN BOOLEAN FirstChance//表示是第一輪還是第二輪處理
 9       )
10   
11 {
12     //省略了部分內容
13     //把ContextRecord複製到當前執行緒的核心棧中
14     KeContextToKframes(TrapFrame,
15                        ExceptionFrame,
16                        ContextRecord,
17                        ContextRecord->ContextFlags,
18                        PreviousMode);
19     //把異常記錄中的異常程式碼最高位清零,這樣可以分辨出軟體異常和CPU異常
20     ExceptionRecord->ExceptionCode &= ~KI_EXCEPTION_INTERNAL;
21     //呼叫分發異常的函式
22     KiDispatchException(ExceptionRecord,
23                         ExceptionFrame,
24                         TrapFrame,
25                         PreviousMode,
26                         FirstChance);
27 
28     return STATUS_SUCCESS;
29 }

 所有說無論是CPU異常還是軟體異常都會呼叫KiDispatchException來分發異常。

 

異常分發過程

在Windows核心方面的聖經《Windows Internals》中有一部分章節是介紹Windows系統異常分發機制的,其實那一章節的內容就是在講述KiDispatchException函式的處理流程(雖然作者根本沒有提到這個函式,因為Windows核心是不開源的,微軟也沒有提供手冊)。

第一次看《Windows Internals》時,我還沒有讀過這個函式的實現。當時對書裡的各種概念如二次除錯、尋找偵錯程式等等一臉懵逼。但是看了這個函式之後會發現真正的處理流程是很清晰的。

首先異常有兩大類,發生在核心態下的異常和發生在使用者態下的異常。為什麼要分兩種,這兩者是不一樣的。

1    if (PreviousMode == KernelMode) 
2    {
3         //……
4    } 
5    else 
6    {
7         //……
8    }

函式一開始就把執行的流程完全的分成兩部分了,可見核心異常與使用者異常的分發是你走你的陽關道我走我的獨木橋,兩者是毫不關聯的。

至於為什麼要分為兩個完全不同的流程,我想了一下使用者態異常分發和核心態異常分發的相同點和不同點。

首先說說相同點:

1.都是基於二次除錯機制

沒有接觸過異常分發的讀者可能不明白什麼叫二次除錯機制。其實我們平時使用OD或是Windbg處理異常時就已經接觸到二次除錯機制了。比如,你用OD載入一個使用者態程式,你就會接收到兩次中斷到偵錯程式。其實這個是很常見的,尤其是以前玩CrackMe的時候經常可以碰到用觸發異常來進行反除錯的。這個時候如果你使用OD來忽略異常要連續忽略兩次,這就是二次除錯。

下面來做一個實驗。

很簡單的一個程式

1 #include "stdio.h"
2 
3 int main()
4 {
5     int num = 10 / 0;
6     printf("%d", num);
7     return 0;
8 }

觸發一個除零異常

VS2015編譯不了這個,因為把除零視為錯誤了,不知道該怎麼關這個保護機制。於是用C-Free 5編譯了一下。執行後就是標準的Windows異常視窗,問你要不要掛載偵錯程式啊什麼的,這個選項是可以在登錄檔裡面改的,下面也會研究一下這個標準的Windows異常視窗是哪裡來的。這裡先回歸主題說一下二次除錯的事情。

OD設定不忽略所有的異常,注意有SrongOD外掛的要把外掛關了。這樣OD就不會忽略異常了,如果你用shift+F8來跳過這個異常,你會發現你要按兩次shift+F8這個就是異常的二次除錯。其實用Windbg可以更直觀的看到二次除錯的現象,用Windbg載入這個示例程式,按G繼續,可以看到如圖的情況

這個就是二次除錯。正如我們所說的,這是核心態異常分發和使用者態異常分發的共同點,兩者都會有二次除錯。我個人認為這是一種保護機制,是一種“再給一次機會”的機制,就算沒有這個二次除錯機制也是無關大局的。

2.都在尋找偵錯程式

我們知道異常是無法被“自動”處理的,舉個例子,以上面那個例子程式來說,如果你不是手動指定異常處理程式碼(如__try)的話,那麼異常是不可能自己消失的。就是說系統不會自己改錯,當然也有例外的情況,比如說缺頁異常就是自己處理的,應該是在中斷處理例程中就處理完畢了,不會進行異常分發。但是那個是系統故意留的,根本不能稱作是真正意義上的異常。

那麼,所謂的異常分發就是一個尋找偵錯程式和異常處理程式碼的過程。這個才是本質,也是分發的目的,找不到就進行暴力結束機制。因為不可能帶著錯誤繼續執行下去。

 

相同點說完了,那麼重點就是不同點了。

不同點:

1.CPU模式的切換

我們前面說了,無論是在使用者態觸發異常還是在核心態觸發異常都會呼叫到這個KiDispatchException函式,就是說當使用者態觸發異常後需要轉入核心態。這好像是一句廢話,因為很多函式都是要從使用者態轉入核心態的啊。比如使用者層的CreateFile就會到核心層的NtCreateFile函式。但是對於使用者態異常分發來說卻不是這麼簡單,因為要執行使用者態的使用者指定的異常處理程式碼,所以進入這個KiDispatchException函式後還要再轉回使用者態去尋找使用者態的異常處理程式碼。而對於核心異常分發來說是不需要CPU執行狀態轉來轉去。

2.尋找偵錯程式的函式不同

一個是尋找核心偵錯程式的,一個是尋找使用者態偵錯程式的。這兩者本質不同。

 

這個就是一個完整的流程圖,來自《軟體除錯》

相關文章