win10系統呼叫架構分析
1. 作業系統模型
大多數作業系統中,都會把應用程式和核心程式碼分離執行在不同的模式下。核心模式訪問系統資料和硬體,應用程式執行在沒有特權的模式下(使用者模式),只能使用有限的API,且不能直接訪問硬體。當用戶模式呼叫系統服務時,CPU執行一個特殊的指令以切換到核心模式(Ring0),當系統服務呼叫完成時,作業系統切換回使用者模式(Ring3)。
Windows與大多數UNIX系統類似,驅動程式程式碼共享核心模式的記憶體空間,意味著任何系統元件或驅動程式都可能訪問其他系統元件的資料。但是,Windows實現了一套核心保護機制,比如PatchGuard和核心模式程式碼簽名。
核心模式的元件雖然共享系統資源,但也不會互相訪問,而是通過傳引數的方式來訪問或修改資料結構。大多數系統程式碼用
C寫的是為了保證可移植性,C語言不是面向物件的語言結構,比如動態型別繫結,多型函式,型別繼承等。但是,基於C的實現借鑑了面向物件的概念,但並不依賴面向物件。
2. 系統架構
下圖是簡化版的Windows系統架構實現:
首先注意那條橫線將使用者模式和核心模式分開兩部分了。橫線之上是使用者模式的程序,下面是核心模式的系統服務。服務程序和使用者程式之下的“子系統DLL”。在Windows下,使用者程式不直接呼叫本地Windows服務,而是通過子系統DLL來呼叫。子系統DLL的角色是將文件化的函式翻譯成呼叫的非文件化的系統服務(未公開的)。
核心模式的幾個元件包括:
- Windows執行實體,包括基礎系統服務,比如記憶體管理器,程序和執行緒管理器,安全管理,I/O管理,網路,程序間通訊。
- Windows核心,包括底層系統函式,比如執行緒排程,中斷,異常分發,多核同步。也提供了一些routine和實現高層結構的基礎物件。
- 裝置驅動,包括硬體裝置驅動(翻譯使用者I/O到硬體I/O),軟體驅動(例如檔案和網路驅動)。
- 硬體抽象層,獨立於核心的一層程式碼,將裝置驅動與平臺的差異性分離開。
- 視窗和圖形系統,實現了GUI函式,處理使用者介面和繪圖。
下表中是Windoows系統核心元件的檔名:
檔名
元件
Ntoskrnl.exe
執行體和核心
Ntkrnlpa.exe(32位才有)
支援PAE
Hal.dll
硬體抽象層
Win32k.sys
子系統的核心模式部分
Ntdll.dll
內部函式
KERNEL32.DLL,KERNELBASE.dll,USER32.dll, GDI32.dll
核心子系統的元件
在一個安裝完成的Windows作業系統中可見並有效的核心實現檔案是:
C:\Windows\System32\ntoskrnl.exe
C:\Windows\System32\ntkrnlpa.exe
請注意有兩個核心檔案,其中第二個比第一個的名字少了os多了個pa,省去的os沒有任何意義,但是多出來的pa所代表的意思是PAE(實體地址擴充套件),這是X86CPU的一個硬體特性,Windows啟動之後根據當前系統設定是否開啟了PAE特性會自動選擇把其中一個作為核心載入到記憶體中。
為什麼加了這麼多限定詞,因為ntoskrnl.exe這個檔名並不一定是這個檔案的真實名稱,可以從檔案屬性中看到:
ntoskrnl.exe原始檔名為可能為ntoskrnl.exe或者ntkrnlmp.exe
ntkrnlpa.exe原始檔名為可能為ntkrnlpa.exe或者ntkrpamp.exe
可以發現其中的不同之處就是mp,mp就是Multi-processor(多處理器,也可以理解為多核,因為IA-32架構對多核處理器的程式設計和多處理器的程式設計是相似的機制)。為什麼會出現這中情況呢?因為這完全是由計算機硬體的不同配置導致的。當安裝Windows作業系統的時候,Windows安裝程式會自動檢測機器的CPU特性,根據CPU的核心數來確定使用哪一套核心。如果是單核心就只複製ntkrnlpa.exe和ntoskrnl.exe到系統目錄下,如果是多核心就複製ntkrnlpamp.exe和ntoskrnlmp.exe到系統目錄下,所以如果你有一臺單核心CPU的機器,有一天你換了雙核的CPU卻沒有重新安裝作業系統,那麼你就不會在看到熟悉的Windows啟動畫面了。類似這兩個檔案的還有一個檔案C:\Windows\System32\hal.dll,這是Windows的硬體抽象層程式檔案,這個就不做具體介紹了。
注意:由於在跟蹤分析系統核心呼叫的時候需要匯入相應的符號檔案以及對函式偏移位置等進行分析,因此需要知道自己系統上核心檔案的原始檔名。
3. 系統服務呼叫機制
對於應用程式程序來說,作業系統核心的作用體現在一組可供呼叫的函式,稱為系統呼叫(也成"系統服務")。
從程式執行的角度來看,程序是主動、活性的,是發出呼叫請求的一方;而核心是被動的,只是應程序要求而提供服務。從整個系統執行角度看,核心也有活性的一面,具體體現在程序排程。
系統呼叫所提供的服務(函式)是執行在核心中的,也就是說,在"系統空間"中。而應用軟體則都在使用者空間中,二者之間有著空間的間隔(CPU執行模式不同)。
綜上所述,應用軟體若想進行系統呼叫,則應用層和核心層之間,必須存在"系統呼叫介面",即一組介面函式,這組介面運行於使用者空間。對於windows來說,其系統呼叫介面並不公開,公開是的一組對系統呼叫介面的封裝函式,稱為windowsAPI。
使用者空間與系統空間所在的記憶體區間不一樣,同樣,對於這兩種區間,CPU的執行狀態也不一樣。
在使用者空間中,CPU處於"使用者態";在系統空間中,CPU處於"系統態"。
CPU從系統態進入使用者態是容易的,因為可以執行一些系統態特有的特權指令,從而進入使用者態。
而相反,使用者態進入系統態則不容易,因為使用者態是無法執行特權指令的。
所以,一般有三種手段,使CPU進入系統態(即轉入系統空間執行):
① 中斷:來自於外部裝置的中斷請求。當有中斷請求到來時,CPU自動進入系統態,並從某個預定地址開始執行指令。中斷只發生在兩條指令之間,不影響正在執行的指令。
② 異常:無論是在使用者空間或系統空間,執行指令失敗時都會引起異常,CPU會因此進入系統態(如果原先不在系統空間),從而在系統空間中對異常做出處理。異常發生在執行一條指令的過程中,所以當前執行的指令已經半途而廢了。
③ 自陷:以上兩種都是CPU被動進入系統態。而自陷是CPU通過自陷指令主動進入系統態。多數CPU都有自陷指令,系統呼叫函式一般都是靠自陷指令實現的。一條自陷指令的作用相當於一次子程式呼叫,子程式存在於系統空間。
4. Windows使用系統呼叫的方法
4.1. 通過自陷實現系統呼叫
Windows API如果設涉及到系統呼叫就要由RING3進入RING0,這就牽扯到了X86保護模式下有特權級變化的控制轉移。在早期的CPU中(Pentium II之前),沒有快速系統呼叫這個機制,所以能用來進行特權級變化的控制轉移的機制只有通過自陷實現(很多書或網路上也經常稱為中斷方式),保護模式下的中斷的實現方式是通過IDT表來實現,IDT表中存放的是一種特殊的X86段描述符——門描述符,門描述符的格式如下:
可以看到其中有一個Selector欄位和一個Offset欄位,並且是不連續的,這裡只介紹這兩個欄位的含義,其他欄位的含義這裡不再贅述,有興趣的話可以自己去看下保護模式相關資料。說到底這個門描述符的作用就是描述一個程式段,對我們來說重要的就是Selector和Offset欄位了,因為Selector可以幫我們找到它所描述的程式的【段】,Offset就是程式在【段】內的【偏移】,有了【段】和【偏移】就可以確定程式的線性地址。
在Win10 X64作業系統中IDT表的結構又有些不一樣,具體的結構可以用WinDbg獲得,具體指令及結果如下:
kd> dt_KIDTENTRY64
ACPI!_KIDTENTRY64
+0x000 OffsetLow : Uint2B
+0x002 Selector : Uint2B
+0x004 IstIndex : Pos 0, 3 Bits
+0x004 Reserved0 : Pos 3, 5 Bits
+0x004 Type : Pos 8, 5 Bits
+0x004 Dpl : Pos 13, 2 Bits
+0x004 Present : Pos 15, 1 Bit
+0x006 OffsetMiddle : Uint2B
+0x008 OffsetHigh : Uint4B
+0x00c Reserved1 : Uint4B
+0x000 Alignment : Uint8B
kd> r idtr
idtr=fffff801b88ca070
kd> dt_KIDTENTRY64 fffff801b88ca070
ACPI!_KIDTENTRY64
+0x000 OffsetLow : 0x7500
+0x002 Selector : 0x10
+0x004 IstIndex : 0y000
+0x004 Reserved0 : 0y00000 (0)
+0x004 Type : 0y01110 (0xe)
+0x004 Dpl : 0y00
+0x004 Present : 0y1
+0x006OffsetMiddle : 0xb6d5
+0x008 OffsetHigh : 0xfffff801
+0x00c Reserved1 : 0
+0x000 Alignment : 0xb6d58e00`00107500
在使用這種機制的windows系統中,系統呼叫2E號中斷,進入了系統核心。一般在中斷呼叫前都會初始化一個系統服務號;也叫做分發 ID,該 ID 需要在執行 int 2Eh 前,載入到EAX 暫存器,以便在切換到核心模式的時候呼叫相應的核心函式來完成相應的功能。
粗略地講,INT 指令在內部涉及如下幾個操作:
1) 清空陷阱標誌(TF),和中斷允許標誌(IF);
2) 依序把(E)FLAGS,CS,(E)IP 暫存器中的值壓入棧上;
3) 轉移到 IDT 中的中斷門描述符記載的相應 ISR(中斷服務例程)的起始地址;
4) 執行 ISR,直至遇到 IRET 返回。
最關鍵的第3步涉及“段間”轉移,通過中斷門描述符,能夠引用一個 Ring0 許可權程式碼段,該程式碼段對應的 64 位段描述符(儲存在 GDT 中)中的 DPL 位,即特權級位等於0(0=Ring0;3=Ring3,即便由 Intel 規定的段描述符的 DPL 位有4種取值,但 Windows 僅使用了其中的最高特權級 Ring0 與最低特權級 Ring3,總體而言,使用者模式應用程式位於 Ring3 程式碼或資料段;核心與裝置驅動程式則位於 Ring0 程式碼或資料段 ),再結合段描述符中的“基址”與中斷門描述符中的“偏移”,就能計算出 ISR在 Ring0 程式碼段中的起始地址。下表是64位段描述符的格式,取自 Intel 文件,自行添加了翻譯:
我們知道了系統呼叫了2E號中斷,從而進入了系統核心,知道了中斷號下面我們要做的就是找到這個中斷的服務程式,也就是RING3進入到RING0之後的第一條指令在哪裡。下面就進入核心除錯模式。由於IDT是由IDTR指定的,這裡用WINDBG進行手工分析:
1) 在X86模式下:
0: kd> r idtr
idtr=8003f400
這個IDT有多大呢?
0: kd> r idtl
idtl=000007ff
其實大小就是這個數加一。地址找到了,大小找到了,關鍵是這個是啥結構,IDT長啥樣呢?
0: kd> dt _KIDTENTRY
ntdll!_KIDTENTRY
+0x000Offset : Uint2B
+0x002Selector : Uint2B
+0x004Access : Uint2B
+0x006ExtendedOffset : Uint2B
就是這個結構的陣列。
下面看看第一個成員。
0: kd> dt _KIDTENTRY 8003f400
ntdll!_KIDTENTRY
+0x000Offset : 0x3360
+0x002Selector : 8
+0x004Access : 0x8e00
+0x006ExtendedOffset : 0x8054
這個結構的具體的含義,請看前面對中斷門描述符的解釋或檢視Intel的手冊及者相關的資料。經過計算得出地址是:0x80543360
驗證的方式之一:
|
|
看到了吧!顯示的是正確的。
另一個辦法是:
|
|
2) 在X64模式下:
首先檢視IDTR和IDTL
kd> r idtr
idtr=fffff801b88ca070
kd> r idtl
idtl=0fff
在64位系統中使用的結構是_KIDTENTRY64
kd> dt _KIDTENTRY64
ACPI!_KIDTENTRY64
+0x000 OffsetLow : Uint2B
+0x002 Selector : Uint2B
+0x004 IstIndex : Pos 0, 3Bits
+0x004 Reserved0 : Pos 3, 5Bits
+0x004 Type : Pos 8, 5Bits
+0x004 Dpl : Pos 13,2 Bits
+0x004 Present : Pos 15,1 Bit
+0x006 OffsetMiddle : Uint2B
+0x008 OffsetHigh : Uint4B
+0x00c Reserved1 : Uint4B
+0x000 Alignment : Uint8B
kd> dt _KIDTENTRY64 fffff801b88ca070
ACPI!_KIDTENTRY64
+0x000 OffsetLow : 0x7500
+0x002 Selector : 0x10
+0x004 IstIndex : 0y000
+0x004 Reserved0 : 0y00000(0)
+0x004 Type : 0y01110(0xe)
+0x004 Dpl : 0y00
+0x004 Present : 0y1
+0x006 OffsetMiddle : 0xb6d5
+0x008 OffsetHigh :0xfffff801
+0x00c Reserved1 : 0
+0x000 Alignment :0xb6d58e00`00107500
檢視IDT服務表
kd> !idt
Dumping IDT: fffff801b88ca070
00: fffff801b6d57500nt!KiDivideErrorFault
01: fffff801b6d57600nt!KiDebugTrapOrFault
02: fffff801b6d577c0nt!KiNmiInterrupt Stack =0xFFFFF801B88E5000
03: fffff801b6d57b40nt!KiBreakpointTrap
04: fffff801b6d57c40nt!KiOverflowTrap
05: fffff801b6d57d40nt!KiBoundFault
06: fffff801b6d57fc0nt!KiInvalidOpcodeFault
07: fffff801b6d58200nt!KiNpxNotAvailableFault
08: fffff801b6d582c0nt!KiDoubleFaultAbort Stack =0xFFFFF801B88E3000
09: fffff801b6d58380nt!KiNpxSegmentOverrunAbort
0a: fffff801b6d58440nt!KiInvalidTssFault
0b: fffff801b6d58500nt!KiSegmentNotPresentFault
0c: fffff801b6d58640nt!KiStackFault
0d: fffff801b6d58780nt!KiGeneralProtectionFault
0e: fffff801b6d58880nt!KiPageFault
10: fffff801b6d58c40nt!KiFloatingErrorFault
11: fffff801b6d58dc0nt!KiAlignmentFault
12: fffff801b6d58ec0nt!KiMcheckAbort Stack =0xFFFFF801B88E7000
13: fffff801b6d59540nt!KiXmmException
1f: fffff801b6d52890nt!KiApcInterrupt
20: fffff801b6d56c10nt!KiSwInterrupt
29: fffff801b6d59700nt!KiRaiseSecurityCheckFailure
2c: fffff801b6d59800nt!KiRaiseAssertion
2d: fffff801b6d59900nt!KiDebugServiceTrap
2f: fffff801b6d52b60nt!KiDpcInterrupt
30: fffff801b6d52d90nt!KiHvInterrupt
31: fffff801b6d530f0nt!KiVmbusInterrupt0
32: fffff801b6d53440nt!KiVmbusInterrupt1
33: fffff801b6d53790nt!KiVmbusInterrupt2
34: fffff801b6d53ae0nt!KiVmbusInterrupt3
35: fffff801b6d51718hal!HalpInterruptCmciService (KINTERRUPT fffff801b7425cb0)
50: fffff801b6d517f0USBPORT!USBPORT_InterruptService (KINTERRUPT ffffd001fee5c640)
60: fffff801b6d51870VBoxGuest+0x1290 (KINTERRUPT ffffd001fee5cb40)
70: fffff801b6d518f0storport!RaidpAdapterInterruptRoutine (KINTERRUPT ffffd001fee5cc80) HDAudBus!HdaController::Isr(KINTERRUPT ffffd001fee5c780)
80: fffff801b6d51970i8042prt!I8042MouseInterruptService (KINTERRUPT ffffd001fee5c8c0)
90: fffff801b6d519f0i8042prt!I8042KeyboardInterruptService (KINTERRUPT ffffd001fee5ca00)
a0: fffff801b6d51a70serial!SerialCIsrSw (KINTERRUPT ffffd001fee5c500)
b0: fffff801b6d51af0ACPI!ACPIInterruptServiceRoutine (KINTERRUPT ffffd001fee5cdc0)
b1: fffff801b6d51af8dxgkrnl!DpiFdoLineInterruptRoutine (KINTERRUPT ffffd001fee5c3c0)
ce: fffff801b6d51be0hal!HalpIommuInterruptRoutine (KINTERRUPT fffff801b74266b0)
d1: fffff801b6d51bf8hal!HalpTimerClockInterrupt (KINTERRUPT fffff801b74264b0)
d2: fffff801b6d51c00hal!HalpTimerClockIpiRoutine (KINTERRUPT fffff801b74263b0)
d7: fffff801b6d51c28hal!HalpInterruptRebootService (KINTERRUPT fffff801b74261b0)
d8: fffff801b6d51c30hal!HalpInterruptStubService (KINTERRUPT fffff801b7425fb0)
df: fffff801b6d51c68hal!HalpInterruptSpuriousService (KINTERRUPT fffff801b7425eb0)
e1: fffff801b6d53e30nt!KiIpiInterrupt
e2: fffff801b6d51c80hal!HalpInterruptLocalErrorService (KINTERRUPT fffff801b74260b0)
e3: fffff801b6d51c88hal!HalpInterruptDeferredRecoveryService (KINTERRUPT fffff801b7425db0)
fd: fffff801b6d51d58hal!HalpTimerProfileInterrupt (KINTERRUPT fffff801b74265b0)
fe: fffff801b6d51d60hal!HalpPerfInterrupt (KINTERRUPT fffff801b74262b0)
可以看到在X64環境下INT 2E中斷服務表已經沒有可以匯出的服務了,在使用!idt –a指令檢視,可以考到2E中斷服務表的內容為:
2e: fffff801b6d516e0nt!KiIsrThunk+0x170
通過下一個章