《Undocumented Windows 2000 Secrets》翻譯 --- 第二章(1)
本章對于 Windows 2000 Native API 的討論,主要集中在這些 API 和系統模塊之間的關系,將重點介紹 Windows 2000 采用的中斷機制。 Windows 2000 利用此機制將對內核服務的請求從用戶模式向內核模式傳遞。另外, Win32K 接口和一些與 Native API 相關的主要運行時庫也會被提及,同時還將介紹一些經常使用的數據類型。
有關 Windows 2000 架構的詳細討論已經很多。許多有關 Windows NT 的討論同樣適用于 Windows 2000 。《 Inside Windows NT 》( Custer 1993, Solomon 1998 )的第一、二版都是有關此方面的好書,同樣的還有《 Inside Windows 2000 》( Solomon and Russinovich 2000 )。
NT*() 和 Zw*() 函數集
有關 Windows 2000 架構的一個有趣的事實是:它模擬了多個操作系統。 Windows 2000 內置三個子系統來支持 Win32 、 POSIX 和 OS/2 應用程序。 Win32 子系統是最流行的一個,因此它更多的被開發人員和操作系統所關照。在 Windows 9x 中, Win32 接口實際上是作為整個系統的基礎結構來實現的。但是, Windows 2000 的設計卻有很大不同。盡管 Win32 子系統包含一個名為 kernel32.dll 的系統模塊,但這并不是實際的操作系統內核。它僅僅是 Win32 子系統的一個基本組件。在很多編程書籍中, Windows NT/2000 的軟件開發被簡化為與 Win32 API 打交道的工作, NT 平臺暴露出的一個隱藏的事實是存在另一個更為基礎的調用接口: Native API 。相信編寫 kernel-mode driver 或 file system driver 的開發人員已經對 Native API 非常熟悉了,因為 kernel-mode 模塊位于更低的系統層,在那里子系統是不可見的。然而,你并不需要到驅動程序一層才能訪問此接口 ---- 即使一個普通的 Win32 應用程序也可在任何時候調用 Native API 。這沒什么技術上的限制 ---- 僅僅是微軟不支持此種應用程序開發模式而已。因此,有關此話題的信息并不是很多, neither SDK nor the DDK make the Native API available to Win32 Application.
未文檔化的級別
本書中的多數東西都來自被稱為未文檔化的信息。這通常意味著微軟沒有公開發布這些信息。然而,未文檔化也存在幾個級別,這是因為可能被公布的有關龐大的操作系統(如 Windows 2000 )的信息非常的多。我個人的系統分類如下:
l 正式文檔 :這些信息來自微軟出版的書、文件或者開發工具。大多數重要信息來自 SDK 、 DDK 和 MSDN 。
l 半文檔化的( Semidocumented ) :盡管不是正式文檔,但這些信息還是可以從微軟正式發布的文件中挖掘出來的。例如, Windows 2000 的很多函數和結構體并沒有在 SDK 或 DDK 文檔中提到,但出現在一些頭文件或示列程序中。以 Windows 2000 為例,很多重要的半文檔化信息都源自頭文件 ntddk.h 和 ntdef.h ,這兩個文件都是 DDK 的一部分。
l 未文檔化,但并沒有隱藏 :這些信息不能在任何官方文檔和開發文檔中找到,但其中的一部分對調試工具是可用的。可執行文件或符號文件中的所有符號化信息都屬于這一部分。最好的例子是內核調試器的 !processfIElds 和 !threadfields 命令,這兩個命令會給出兩個未文檔化的結構: EPROCESS 和 ETHREAD 的成員名稱及其偏移量。
l 完全未文檔化的 :微軟很好的隱藏了某些信息,要獲得它們只能通過逆向工程和推理。此類信息包含很多實現細節的信息,沒有人認為 Windows 2000 開發人員需要關注它們,但是這些信息對于系統開發人員和開發調試軟件的人來說卻非常寶貴。挖掘系統內部的信息是非常困難的,但同樣是非常有趣的。
本書討論的 Windows 2000 的內部細節覆蓋了上述系統分類的后三個。
系統服務分配器( System Service Dispatcher )
Win32 子系統和 Native API 之間的關系可以由 Win32 核心模塊與 Windows 2000 內核模塊之間的依賴關系很好的解釋。 圖 2-1 展示了模塊間的依賴關系,方框表示系統模塊,箭頭表示模塊間的依賴關系。如果一個箭頭從模塊 A 指向模塊 B ,這表示 A 依賴于 B ,即,模塊 A 調用 B 中的函數。模塊由雙向箭頭連接,表示二者之間相互依賴。在 圖 2-1 中,模塊: user32.dll 、 advapi32.dll 、 gdi32.dll 、 rpcrt4.dll 以及 kernel32.dll 實現了基本的 Win32 API 。當然,還有其他的 DLL (如 version.dll 、 shell32.dll 和 comctl32.dll )也為 Win32 API 提供支持,為了更清晰些,我省略了它們。 圖 2-1 表現出的一個特性非常有趣,所有的 Win32 API 調用最后都轉移到了 ntdll.dll ,而 ntdll.dll 又將其轉移到了 ntoskrnl.exe 。
Ntdll.dll 是一個操作系統組件,它為 Native API 準確地提供服務, ntdll.dll 是 Native API 在用戶模式下的前端。 Native API 真正的接口在 ntoskrnl.exe 中實現。從其文件名可以猜出它就是 NT 操作系統內核。事實上,內核模式驅動程序對系統服務的請求多數時候都會進入該模塊。 Ntdll.dll 的主要任務就是為運行于用戶模式的程序提供一個確定的內核函數的子集,這其中就包括 Win32 子系統 DLLs 。在 圖 2-1 中,從 ntdll.dll 指向 ntoskrnl.exe 的箭頭旁標注的 INT 2eh 表示 Windows 2000 使用此中斷將 CPU 特權級從用戶模式切換到內核模式。開發內核( kernel-mode )模式程序的人員認為用戶模式的代碼是具有攻擊性的、充滿錯誤的和危險的。因此,必須讓這些代碼遠離內核函數。而通過在調用 API 的過程中將特權級別從用戶模式切換到內核模式是一種可控制這些問題的方式。調用程序從來不可能觸及內核,它只能察看它們。
例如,由 kernel32.dll 導出的 Win32 API 函數 DeviceIoControl() 最終會調用由 ntdll.dll 導出的 NtDeviceIoControlFile() 。通過反編譯該函數會發現此函數令人驚訝的實現方式 — 它是如此的簡單! 示列 2-1 展示了這些。首先, CPU 寄存器 EAX 被裝入了一個“魔術”數字 0x38 ,這是一個分派 ID 。接下來,寄存器 EDX 被設置指向堆棧中的某處,其地址為堆棧指針 ESP 加上 4 ,因此, EDX 將指向堆棧中返回地址的后面,該返回地址在進入 NtDeviceIoControlFile() 時將被立即保存下來。顯而易見, EDX 指向的位置是用來臨時存放傳遞進來的參數的。接下來的指令是一個簡單的 INT 2eh ,該指令將跳轉到中斷描述符表( Interrupt Descriptor Table,IDT )的 0x2e 位置上存放的中斷處理例程( interrupt handler )中。這看上去是不是很熟悉?事實上,這有些像 Dos 下的 INT 21h 調用。然而, Windows 2000 的 INT 2eh 接口要遠比一個簡單的 API 調用有用,分配器( dispatcher )利用它從用戶模式進入內核模式。請注意,這種模式切換方式是 x86 處理器特有的。在 Alpha 平臺上,有不同的方式來實現此種功能。
NtDeviceIoControlFile:
mov eax, 38h
lea edx, [esp+4]
int 2Eh
ret 28h
示列 2-1. ntdll.NtDeviceIoControlFile() 的實現方式
Windows 2000 Native API 由 248 個函數組成,這些函數都采用上述方式進入內核。與 Windows NT 4.0 相比多出了 37 個。你很容易在 ntdll.dll 的導出列表中通過 Nt 前綴來認出它們。 Ntdll.dll 總共導出了 249 個這樣的符號。多出的那個函數是 NtCurrentTeb() ,該函數是一個純粹的用戶模式函數,它無需進入內核。 附錄 B 中的 表 B-1 列出了所有可用的 Native API 。該表同時還指出那個函數是由 ntoskrnl.exe 導出的。令人奇怪的是,在處于內核模式的模塊中,只能調用 Native API 的一個子集。另一方面, ntoskrnl.exe 導出了兩個 ntdll.dll 沒有提供的 Nt* 符號(指以 Nt 開頭的符號): NtBuildNumber 和 NtGlobalFlag 。這兩個符號都沒有指向函數的入口地址,而是指向 ntoskrnl.exe 中的變量。驅動模塊( driver module )可以使用 C 編譯器的 extern 關鍵字來導入這些變量。 Window 2000 采用此種方式導出了很多變量,稍后我將給出一個示例代碼來使用其中的幾個。
你可能會奇怪為什么 表 B-1 (位于附錄 B 中)分別為 ntdll.dll 和 ntoskrnl.exe 提供了兩列,其名稱分別為: ntdll.Nt* 、 ntdll.Zw* 和 ntoskrnl.Nt* 、 ntoskrnl.Zw* 。原因是,這兩個模塊導出了兩組相互關聯的 Native API 符號。在 表 B-1 (位于附錄 B 中)的最左列給出了所有名字中包含 Nt 前綴的符號。另一個集合包含相似的名字,不過由 Zw 前綴代替了 Nt 。反編譯 ndll.dll 可看出每對符號都指向相同的代碼。這看起來似乎是浪費內存。然而,如果你反編譯 ntoskrnl.exe ,你就會發現 Nt* 符號指向實際的代碼而 Zw* 指向 INT 2eh stubs (如 示列 2-1 列出的)。這意味著 Zw* 函數集合將從用戶模式轉入內核模式,而 Nt* 符號直接指向的代碼會在模式切換后被執行。
表 B-1 (位于附錄 B 中)中有兩件事需要特別注意。首先, NtCurrentTeb() 函數沒有對應的 Zw* 函數。這不是什么大問題,因為 ntdll.dll 以相似的方式導出 Nt* 和 Zw* 函數。其次, ntoskrnl.exe 不再一貫的成對的導出 Nt/Zw 函數。其中的一些僅以 Nt* 或 Zw* 的形式出現。我不知道為什么會這樣,我猜測 ntoskrnl.exe 僅導出了在 Windows 2000 DDK 中有文檔記錄的函數以及其它系統模塊必須的那些函數。注意,保留的 Native API 函數仍然實現于 ntoskrnl.exe 的內部。這些函數并沒有公開的進入點,但可通過 INT 2eh 到達他們。
服務描述符表( The Service Descriptor Tables )
從 示例 2-1 給出的反編譯代碼可看出, INT 2eh 隨同傳入 CPU 寄存器 EAX 和 EDX 的兩個參數一起被調用。我已經提到過 EAX 中的“魔術”數字是一個分派 ID 。除 NtCurrentTeb() 之外的所有 Native API 都采用此種方式,處理 INT 2eh 的代碼必須確定每個調用將被分配到那個函數。這就是提供分派 ID 的原因。位于 ntoskrnl.exe 中的中斷處理例程將 EAX 中的數值作為一個索引來查詢一個特定的表。這個表被稱作系統服務表( System Service Table, SST )該表對應的 C 結構體 ---SYSTEM_SERVICE_TABLE 的定義在 列表 2-1 中給出。在該列表中還包含 SERVICE_DESCRIPTOR_TABLE 結構的定義,該結構共有四個 SST 類型的數組,其中的前兩個用于特定目的。
盡管上述的兩個表是系統基本的數據類型,但他們在 Windows 2000 DDK 中 并沒有相應的文檔記載,本書中出現的許多代碼片斷都包含未文檔化的數據類型和函數。因此,不能保證這些信息是完全真實可信的。所有符號化的信息,如結構名 稱、結構成員和參數都是如此。在創建這些符號時,我試圖使用適當的名稱,這些名稱基于從已知符號的一個很小的子集(包括從符號文件中得到的那些)中得出的 命名方案。然而,在很多場合這種啟發式方法并不成功。只有在原始的代碼中包含所有的信息,但我無法得到它們。實際上,我并不打算閱讀這些源代碼,因為這需 要和微軟簽訂一個 NDA ( Non-Disclosure Agreement, ,不可泄漏協議),由于該 NDA 的限制,將很難寫出一本有關非文檔化信息的書。
typedef NTSTATUS (NTAPI*NTPROC)();
typedef NTPROC* PNTPROC;
#define NTPROC_ sizeof(NTPROC)
typedef struct _SYSTEM_SERVICE_TABLE
{
PNTPROC ServiceTable; // array of entry points
PDOWRD CounterTable; // array of usage counters
DWord ServiceLimit; // number of table entries
PBYTE ArgumentTable; // array of byte counts
}
SYSTEM_SERVICE_TABLE,
*PSYSTEM_SERVICE_TABLE,
**PPSYSTEM_SERVICE_TABLE;
//-----------------------------------------------------------------------------------------------------------
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
SYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe ( native api )
SYSTEM_SERVICE_TABLE win32k; // win32k.sys (gdi/user support)
SYSTEM_SERVICE_TABLE Table3; // not used
SYSTEM_SERVICE_TABLE Table4; // not used
}
SYSTEM_DESCRIPTOR_TABLE,
*PSYSTEM_DESCRIPTOR_TABLE,
**PPSYSTEM_DESCRIPTOR_TABLE;
列表 2-1 系統服務描述符表的結構定義
現在,回到 SDT ( Service Descriptor Table )的秘密上來。從 列表 2-1 給出的該結構的定義可看出該結構的頭兩個數組保留給了 ntoskrnl.exe 和 Win32 子系統(位于 win32k.sys )中的內核模式( kernel-mode )部分。來自 gdi32.dll 和 user32.dll 的調用都通過 Win32k 的系統服務表( SST )進行分派。 Ntolkrnl.exe 導出了一個指針(符號為 KeServiceDescriptorTable )指向其主服務描述符表( Main SDT )。內核還維護了一個替代的 SDT ,其名稱為: KeServiceDescriptorTableShadow ,但這個 SDT 并沒有被導出。從處于內核模式的模塊中訪問主服務描述符表( SDT )非常容易,你只需要兩個 C 指令,如 列表 2-2 所示。首先是由 extern 關鍵字修飾的變量說明,這告訴鏈接器該變量并不包含在此模塊中,而且不需要在鏈接時解析相應的符號名稱。當該模塊被加載到進程的地址空間后,針對該符號的引用才會動態連接到相應的模塊中。 列表 2-2 中第二個 C 指令就是這樣的一個引用。將類型為 PSERVER_DESCRIPTOR_TABLE 的變量賦值為 KeServiceDescriptorTable 時,就會和 ntoskrnl.exe 建立一個動態連接。這很像調用一個 DLL 中的 API 函數。
// Import SDT pointer
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
// Create SDT reference
PSERVICE_DESCRIPTOR_TABLE psdt = KeServiceDescriptorTable;
列表 2-2 訪問系統服務描述符表
SDT 中的每個 SST 的 ServiceTable 成員都是一個指針,指向一個由函數指針構成的數組,此函數指針的類型為: NTPROC ,這為 Native API 提供了占位符,這種方式和在 Win32 編程中使用的 PROC 類型很相似。 NTPROC 的定義在前面的 列表 2-1 中給出。 Native API 函數通常返回一個 NTSTATUS 類型的代碼并且使用 NTAPI 調用方式, NTAPI 實際上就是 _stdcall 。 ServiceLimit 成員保存在 ServieTable 數組中發現的入口地址的個數。在 Windows 2000 中,其默認值為 248 。 ArgumentTable 成員是一個 BTYE 類型的數組,它和 ServiceTable 所指的數組一一對應,并給出其中每個函數指針所需的參數在調用者的堆棧中的字節數。此信息隨 EDX 寄存器提供的指針一起使用。當內核從調用者的堆棧中復制參數到自己的堆棧時就需要這些信息。 CounterTable 成員在 Windows 2000 的 Free Build 版中不被使用。在 Debug Build 版中,該成員指向一個 DWORD 類型的數組,作為每個函數的使用計數器( usage counters )。 This information can be used for profiling purposes.
使用 Windows 2000 的內核調試器可方便的顯示 SDT 中的內容。如果你還沒有設置好這個有用的程序,那請參考第一章。在 示列 2-2 中,我首次使用了 dd KeServiceDescriptorTable 命令。調試器會將此公開符號解析為 0x8046AB80 ,同時顯示該地址之后的 32 個 DWORD 的 16 進制轉儲。不過僅有前面的四行才是有意義的,它們分別對應 列表 2-1 中的四個 SDT 成員。為了更清晰些,它們都將以黑體顯示。如果你仔細觀察,你會發現第五行與第一行十分相像,這是另一個 SDT 嗎?這是測試內核調試器的 ln 命令的好機會。在示列 2-2 中,在顯示完 KeServiceDescriptorTable 的十六進制 dump 之后,我輸入 ln 8046abc0 命令。顯然,調試器知道地址 0x8046abc0 ,它將此地址轉化為對應的符號 KeServiceDescriptorTableShadow 可以看出,這是內核維護的第二個 SDT 。二者之間的顯著區別是:第二個 SDT 包含 Win32k.sys 的入口地址。這兩個表的的第三和第四個成員都是空的。 Ntoskrnl.exe 提供了一個函數 KeAddSystemServiceTabel() 來填充這兩個成員。
注意,我截斷了 ln 命令的輸出信息,僅保留了基本的信息。
從地址 0x8046ab88 開始,是 KeServiceDescriptorTable 的十六進制轉儲,在那兒可以找到 ServiceLimit 成員,可看到其值為 0xF8 (十進制 248 ),這和我們預期的一樣。 ServiceTable 和 ArgumentTable 的值分別指向地址 0x804704d8 和 0x804708bc 。用 ln 命令察看著兩個地址,可得到其符號: KiServiceTable 和 KiArgumentTable 。這兩個符號都沒有從 ntoskrnl.exe 中導出,但是調試器可通過察看 Windows 2000 的符號文件識別它們。 ln 命令還可應用到 Win32k SST 指針上,針對其 ServiceTable 和 ArgumentTable 成員,調試器分別給出了其對應的符號 w32pServiceTable 和 W32pArgumenTable 。這兩個符號都來自 Win32k.sys 的符號文件。如果調試器無法解析這些地址,可使用 .reload 命令強制重新加載所有可用符號文件,然后再進行解析。
示例 2-2 的剩余部分是 KiServiceTable 和 KiArgumentTable 最前面的 128 個字節的十六進制轉儲。到目前為止,如果我說的有關 Native API 的東西都是正確的,那么 NtClose() 函數的地址應位于 KiServiceTable 數組的第 24 個位置上,其地址為 0x80470538 。在該地址處,可發現其值為 0x8044c422 ,在 dd KiServiceTable 的輸出中,該地址以黑體標記。用 ln 察看 0x8044c422 ,會看到其對應的符號正是 NtClose() 。
kd> dd KeServiceDescriptorTable
8046ab80 804704d8 00000000 000000f8 804708bc
8046ab90 00000000 00000000 00000000 00000000
8046aba0 00000000 00000000 00000000 00000000
8046abb0 00000000 00000000 00000000 00000000
8046abc0 804704d8 00000000 000000f8 804708bc
8046abd0 a0186bc0 00000000 0000027f a0187840
8046abe0 00000000 00000000 00000000 00000000
8046abf0 00000000 00000000 00000000 00000000
kd> ln 8046abc0
(8046abc0) nt!KeServiceDescriptorTableShadow
kd> ln 804704d8
(804704d8) nt!KiServiceTable
kd> ln 804708bc
(804708bc) nt!KiArgumentTable
kd> ln a0186bc0
(a0186bc0) win32k!W32pServiceTable
kd> ln a0187840
(a0187840) win32k!W32pArgumentTable
kd> dd KiServiceTable
804704d8 804ab3bf 804ae86b 804bdef3 8050b034
804704e8 804c11f4 80459214 8050c2ff 8050c33f
804704f8 804b581c 80508874 8049860a 804fc7e2
80470508 804955f7 8049c8a6 80448472 804a8d50
80470518 804b6bfb 804f0cef 804fcb95 8040189a
80470528 804d06cb 80418f66 804f69d4 8049e0cc
80470538 8044c422 80496f58 804ab849 804aa9da
80470548 80465250 804f4bd5 8049bc80 804ca7a5
kd> db KiArgumentTable
804708bc 18 20 2c 2c 40 2c 40 44-0c 18 18 08 04 04 0c 10 . ,,@,@D........
804708cc 18 08 08 0c 08 08 04 04-04 0c 04 20 08 0c 14 0c ........... ....
804708dc 2c 10 0c 1c 20 10 38 10-14 20 24 1c 14 10 20 10 ,... .8.. $... .
804708ec 34 14 08 04 04 04 0c 08-28 04 1c 18 18 18 08 18 4.......(.......
804708fc 0c 08 0c 04 10 00 0c 10-28 08 08 10 00 1c 04 08 ........(.......
8047090c 0c 04 10 00 08 04 08 0c-28 10 04 0c 0c 28 24 28 ........(....($(
8047091c 30 0c 0c 0c 18 0c 0c 0c-0c 30 10 0c 0c 0c 0c 10 0........0......
8047092c 10 0c 0c 14 0c 14 18 14-08 14 08 08 04 2c 1c 24 .............,.$
kd> ln 8044c422
(8044c422) nt!NtClose
示例 2-2 檢查服務描述符表
譯注:
在 Windows XP 中, KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow 和 Windows 2000 有所區別。在 XP 中,后者位于前者的前面,而在 W2K 中,后者位于前者的后面。
INT 2eh 系統服務處理例程( System Service Handler )
隱藏在內核模式中的 INT 2eh 中斷處理例程為 KiSystemService() 。再強調一次,這是一個內部符號, ntoskrnl.exe 并沒有導出該符號,不過,它卻包含在 Windows 2000 的符號文件中。因此,內核調試器可以正確的解析該符號。從本質上來看, KiSystemService() 將執行如下操作:
1. 從當前線程的控制塊( thread's control block )中檢索 SDT 指針。
2. 通過測試 EAX 寄存器中的分派 ID 的第 12 、 14 位來確定使用 SDT 中的那個 SST ( SDT 中有四個 SST )。如果分派 ID 位于 0x0000-0x0FFF ,將選擇 ntoskrnl 表;位于 0x1000-0x1FFF 則選擇 Win32k 表。 0x2000-0x2FFF 和 0x3000-0x3FFF 由 SDT 的 Table3 和 Table4 保留。如果分配 ID 超過了 0x3FFF ,在分派前多余的位將被屏蔽掉。
3. 通過檢查分派 ID 的 0 到 11 位來確定該 ID 在所選 SST 中對應的 ServiceLimit 成員。如果 ID 超出了范圍,將返回錯誤代碼: STATUS_INVALID_SYSTEM_SERVICE 。在一個未使用的 SST 中, ServiceLimit 成員始終是 0 ,從而為所有可能的分派 ID 產生一個錯誤代碼。
4. 通過檢查 EDX 中保存的參數堆棧指針,來取得 MmUserProbeAddress 的值。這是由 ntoskrnl.exe 導出的一個公開變量。參數指針通常會與 0x7FFF0000 進行比較。如果沒有低于該地址,那么將返回 STATUS_ACCESS_VIOLATION 。
5. 根據在 SST 的 ArgumentTable 中查找到的參數堆棧的字節數,將所有函數參數從調用者堆棧中復制到當前的內核堆棧中。
6. 在從服務調用( Service Call )中返回后,將控制權傳遞給內部函數 KiServiceExit()
非常有趣的是 INT 2eh 中斷處理例程并不使用全局 SDT (即 KeServiceDescriptorTable ),而是使用線程專屬的指針替代之。顯然,每個線程可以擁有不同的 SDT 。在線程初始化時, KeInitializeThread() 會將 KeServiceDescriptorTable 的指針寫入線程控制塊( Thread Control Block )中。不過,此默認值在稍后可能會改變,如改為指向 KeServiceDescriptorTableShadow 。
Win32 內核模式接口( Win32 Kernel-mode Interface )
從前面對 SDT 的討論,可看出存在著與 Native API 相關的第二個主內核模式接口( main Kernel-mode Interface )。該接口將 Win32 子系統的圖形設備接口( Graphics Device Interface, GDI )、窗口管理器(即 User 模塊)連接至內核組件 ---Win32K (即 Win32k.sys ) . ,該組件隨同 Windows NT 4.0 引入。引入該組件是為了克服 Win32 圖形引擎固有的性能限制(由于 Windows NT 子系統的最初設計導致)。在 Windows NT 3.x 中, Win32 子系統采用的是客戶 - 服務器模式( Client-Server model ),這樣就必須從用戶模式切換到內核模式才能進行內核調用( Kernel Involved )。通過將圖形引擎的絕大部分移至內核組件 ---Win32k.sys ,從而避免了大部分因內核切換導致的性能損失。
Win32K 分派 ID ( Win32K Dispatch IDs )
現在該介紹 Win32k.sys 了,也是該更新 圖 2-1 的時候了。 圖 2-2 基于 圖 2-1 ,但在 ntoskrnl.exe 左面加入了 Win32k.sys 。同時我還加入了從 GDI32.DLL 和 USER32.DLL 指向 Win32k.sys 的箭頭。當然,這不是百分之百正確,因為這些模塊中的 INT 2eh 調用實際上指向 ntoskrnl.exe ,在 ntoskrnl.exe 中才有該中斷的處理例程。然而,調用最后還是由 Win32k.sys 管理,這也是箭頭這樣指的原因。
稍早提到過, Win32K 接口同樣基于 INT 2eh 分派器( INT 2eh Dispatcher ),這與 Native API 非常相似。僅有的區別在于 Win32K 使用另一區段的分派 ID 。盡管與所有 Native API 調用相關的分派 ID 都位于 0x0000----0x0FFF ,而 Win32K 分派 ID 位于 0x1000---0x1FFF 之間。如 圖 2-2 所示, Win32K 的主要客戶端是 GDI32.DLL 和 USER32.DLL 。因此,通過反編譯這些模塊(指 gdi32.dll 和 user32.dll )可能會找到與 Win32K 分派 ID 相關的符號化名稱。通過反編譯可發現在這些模塊( gdi32.dll 和 user32.dll )的導出節( export sections )中僅包含 INT 2eh 調用的一個很小的子集,看來是時候再次使用內核調試器了。如 示例 2-3 所示,我通過使用 dd W32pServiceTable 命令,來確定 Win32k.sys 的符號是可用的,在此之前請先使用 .reload 命令以加載所有可用符號文件。
在 示例 2-3 的最后三行中,我使用 ln 命令顯示與 W32pServiceTable 的第一個入口地址相關的符號。顯然,可看到分派 ID 為 0 的 Win32K 函數為 NtGdiAbortDoc() 。你可以針對所有 639 個 ID 來重復此過程,但是最好能自動進行符號的查找。現在,我已經為你完成了這項工作,所有分派 ID 對應的符號名稱都收錄在 附錄 B 的 表 B-2 中。符號從 gdi32.dll 和 user32.dll 映射到 win32k.sys 十分簡單: GDI 符號可通過在其前面添加 NtGdi 前綴就可轉換為 Win32K 符號, USER 符號則添加 NtUser 前綴。然而,有一少部分例外。例如,如果一個 GDI 符號以 Gdi 開始,那么其前綴就減少為 Nt ,這可能是為了避免出現 NtGdiGdi 這樣的字符序列。在其他的一些例子中,字符的大小寫會有些不同(比如 EnableEUDC() 轉化后則變成了 NtGdiEnableEudc() ),或者用符號名稱尾部的 W 來表示沒有對應的 Unicode 函數(如, CopyAcceleratorTableW() 轉化后成為 NtUserCopyAcceleratorTable() )。
提供 Win32K API 的詳細文檔需要很大的努力。這些函數幾乎是 Native API 的三倍。或許某天有人會為這些 API 編寫一本不錯的參考手冊,就像 Gary Nebbett 編寫的 Native API 手冊。不過,在本書范圍內,有關這些 API 的信息已經足夠了。
