解讀Windows 2000/XP分層驅動模型
可擴充性是Windows NT/2000/XP設計的目標之一,其分層驅動模型是可擴充性的最好體現。實現分層依賴於IO管理器的兩個重要的設計:1、Windows中的任何一個驅動程式都被設計成Client/Server模式。對於用戶端驅動,通過IoGetDeviceObjectPointer之類的擷取服務端驅動匯出的Device對象,通過IO管理器的IoCallDriver請求服務端的服務。IoCallDriver實際上根據用戶端的調用參數(通過IRP)調用服務端的派遣入口(回呼函數)接受用戶端的請求。2、IO管理器實現一個分層的資料結構,在DEVICE_OBJECT對象中儲存某種關係,自動將請求IRP發給裝置棧中的最高的一個裝置,由其決定如何處理,或是自身處理,或是向下傳遞,達到分層的目的。鑒於這種能力,分層驅動模型可以實現很多應用,如檔案監控,加密,防病毒等等,由於PNP的引入,這種應用將更加廣泛。實際上這種分層模型在Windows NT/2000/XP中無處不在,不信的話,請執行如下命令看看:
findstr /M IoAttachDevice %SYSTEMROOT%/system32/drivers/*.sys
所有列出的Driver幾乎都可以看作分層驅動的例子。對於分層驅動的介紹幾乎充斥著所有介紹Windows驅動的任何一本書中。本文不想過多於重複這些內容,旨在從底層資料結構的實現上說明這種分層的實現。我們首先從DEVICE_OBJECT開始說明。下面是這個結構的部分定義:
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT {
.
.
.
struct _DRIVER_OBJECT *DriverObject;
struct _DEVICE_OBJECT *NextDevice;
struct _DEVICE_OBJECT *AttachedDevice;
.
.
.
PVOID DeviceExtension;
.
.
.
CCHAR StackSize;
.
.
.
struct _DEVOBJ_EXTENSION *DeviceObjectExtension;
.
.
.
} DEVICE_OBJECT;
成員DriverObject是DeviceObject對應的DRIVER_OBJECT,通過這個對象,IO管理器可以知道如何為這個裝置提供服務(通過調用DriverObject提供的Dispatch入口)。通常一個DRIVER_OBJECT可以為一至多個DEVICE_OBJECT提供服務,各個DEVICE服務不同的同一類型的物理或邏輯裝置。她們也有可能扮演不同的角色,如對於PNP引入的PDO與FDO,下面我會詳細介紹。正因為這樣NextDevice成員即用於連結這些DEVICE_OBJECT。所以可以得到這樣的結論,像前一句所描述的,對於同一個Driver匯出的PDO與FDO則通過NextDevice成員邏輯上建立關係。而對於AttachedDevice成員對於Legacy的Driver(Windows NT 4.0之前,在之後的版本中也可以正常使用),主要通過這一欄位來實現本文開頭提到的IO管理器提供的第二項功能。如下所示,AttacheDevice1附加至Device1之上, 這種情況下AttachDevice1與Device1通常不是由同一個Driver服務的,即他們沒有通過NextDevice成員連結在一起的,這樣我們可以通過書寫另一個Driver,通過附加一個Device至一個已存在的裝置上改變或監視這個裝置的行為。當然這時候Device1的AttachedDevice成員即指向AttachDevice1。而AttachedDevice1並沒有被任何裝置附接過,所以其AttachedDevice成員指向NULL。通過調用IoCallDriver請求Device1服務時,IO管理器內部會調用IoGetAttachedDevice之類的,獲得附接在Device1之上的最高層裝置。這裡是AttachedDevice1,而對於Device2,即是AttachedDevice3,如果沒有附接任何裝置,當然就是Device1了。這樣我們附接的裝置就有機會執行相應的操作了。另外對於Device1與Device2的情況,很明顯,Device1位於Device2之上,而這時就是我們開頭介紹的第一種情況,Device1通過IoCallDriver請求Device2,邏輯上建立一種關係(我們不能通過任何資料結構標識這種關係,通常這是驅動開發人員設計成的邏輯關係,對於Windows 2000/XP上如何知道這種關係,當然最好的出入就是DDK文檔了)。
|-------------|____AttachDevice1
| Device1 |
|-------------|
------AttachDevice3
|-------------|____AttachDevice2---|
| Device2 |
|-------------|
DeviceExtension成員通常是驅動開發人員自已定義的結構,其存放裝置相關的內容,例如用於區別PDO與FDO等等。在調用IoCreateDevice時指定其大小,IO管理器分配sizeof(DEVICE_OBJECT)+DeviceExtensionSize的非分頁式記憶體用於裝置對象,這樣這兩個結構在物理上是連續的,所以我們在一些檔案系統驅動程式中經常看過VOLUME_DEVICE_OBJECT這樣的定義(緊隨標準的DEVICE_OBJECT後即是專用的用於VOLUME的定義,避免在Dispatch入口每次都要引用這個成員)。
StackSize指當前裝置棧的裝置個數(AttachedDevice個數加上裝置本身),用於IO管理器分配IRP時指定STACK_LOCATION個數。
上面的所有敘述即構成了Windows NT 4.0之前的Windows分層驅動模型。這也是遺留的(Legacy)的層次驅動程式的主要工作思路。AttachedDevice指出了Attached了的裝置,Microsoft在Windows 2000中的DeviceObjectExtension結構成員中引入了一個AttachedTo指出被當前裝置Attached的低層裝置,這在Windows NT是沒有實現的。DeviceObjectExtension是一個很重要的結構,下面要介紹的支援pnp的WDM驅動的AddDevice入口也在這兒。她是由系統定義的結構(區別於DeviceExtension)。對於DRIVER_OBJECT也有個類似的稱為DriverExtenion的結構,後者有一個ClientDriverExtension結構,由IoAllocateDriverObjectExtension分配,IoGetDriverObjectExtension獲得。classpnp.sys即是通過這一方法,實現對disk.sys、tape.sys與cdrom.sys的管理;NDIS.SYS也通過這一方法實現各個Miniport/IM/Protocol Driver的管理。
Windows 2000及其以後的NT系列引入了WDM,支援PNP、Power管理及WMI,為了讓作業系統本身就對此支援,ntoskrnl.exe中匯出了幾個置有DRVO_BUILTIN_DRIVER(ntddk.h中定義)標誌的Driver,分別為pnpmanager、WMIxWDM及ACPI_HAL,後者是支援ACPI的HAL(這是在我Windows XP Professional台式機上的情況,不知道不支援ACPI的機子啥模樣,我的機子上ntoskrnl.exe還匯出/FileSystem/Raw驅動用於檔案系統的支援,我想這幾個裝置名稱可能會隨機器配置及Windows版本不同而不同,實際上我在我Windows 2000 Server SP0筆記本上ntoskrnl.exe產生如下四個裝置:Pnpmanager,PCI_HAL,WDM與RAW,我底下的敘述都是基本我台式機上的,至於出現的一些不同我可能會另外指出,也有可能沒有),前兩個driver都是為了支援WDM而引入的。WMIxWDM匯出WMIDataDevice用作WMI的支援,這與本文討論分層驅動沒有關係,以下我重點介紹Pnpmanager。
在介紹之前,我們來看一下devmgmt.msc“依串連排序裝置”視圖:
TSU00(機器名,由pnpmanager實現的虛擬Root匯流排枚舉)
|
|+ Advanced Configuration and Power Interface(ACPI) PC(ntoskrnl中的ACPI_HAL驅動實現)
|
|+Micrsoft ACPI-Compliant System(由acpi.sys枚舉)
|
|+PCI bus(由pci.sys實現)
|
|+(串連的裝置)
這是我機子上的情況,pnpmanager是一個匯流排驅動程式(在ntoskrnl.exe內部實現,如果你有checked build的ntoskrnl.exe,你可以很容易的發現其由base/ntos/io/pnpmgr/pnpdd.c實現,看過Sysinternals匯出的Windows XP Source Tree了嗎?),她實現一個稱為Root的虛擬匯流排。所有Legacy裝置,都是串連至這個虛擬匯流排上的,不信的話,你在devmgmt.msc的上面列出的草圖上繼續選“顯示隱藏的裝置看看”。從這種意義上看她要為每一個串連到她上面的裝置建立一個PDO。對於這些PDO通常是以00000001開始的十六進位命名,如在我機子我實驗某一時刻裝置名稱一直至00000050,共80個PDO(在我的Windows 2000 Server的機子上並不是這樣命名的,雖然也是基於十六進位的,但卻是從挺大的一個數值開始的)。我非常喜歡隨Windows XP DDK一些發行的OSR的DeviceTree,但為了更好的理解,還是以windbg作個實驗吧:
找出上面草圖ACPI_HAL附接的由pnpmanager實現的PDO,通常這是pnpmanager實現的第一個PDO,即命名為00000001(如果你的Pnpmanager產生的裝置不是這樣命名的,請使用!drvobj pnpmanager找出產生的對應的PDO,我的Windows 2000 Server SP0的筆記本上,pnpmanager的第一個PDO用於服務ESS音效卡,而並不是我原以為的PCI_HAL,你可能另需要使用!devstack或是下面要介紹的!devnoe命令,方法不詳述):
kd> !object /Device/00000001
Object: 812b4410 Type: (812b4048) Device
ObjectHeader: 812b43f8
HandleCount: 0 PointerCount: 5
Directory Object: e10011b0 Name: 00000001
kd> !devobj 812b4410
Device object (812b4410) is for:
00000001 /Driver/PnpManager DriverObject 812b4980
Current Irp 00000000 RefCount 0 Type 00000004 Flags 00001040
Dacl e1518a6c DevExt 812b44c8 DevObjExt 812b44d0 DevNode 812b42b8
ExtensionFlags (0000000000)
AttachedDevice (Upper) 812f6bb0 /Driver/ACPI_HAL
Device queue is not busy.
我們很容易通過上面的輸出含用AttachedDevice的一行發現串連至裝置00000001的是ACPI_HAL,與我們devmgmt.msc上看到的一致。實際上Attached至這一PDO的是由ACPI_HAL服務的一個稱為FDO的裝置。PDO與FDO本身在內部都仍是由DEVICE_OBJECT來表示的,正像前面提及的對於匯流排驅動開發人員通常使用DeviceExtension中的一個標誌區分同個driver服務的裝置是PDO還是FDO。FDO通常由driver的AddDevice入口建立,建立後使用IoAttachDeviceToDeviceStack附接至下層匯流排驅動提供的PDO,正像上面windbg中我們看到的一樣。這個Attached操作與我們開頭討論的Legacy裝置是一樣的,同樣是使用DEVICE_OBJECT的AttachedDevice成員。
你可能會困惹pnpmanager的AddDevice入口應該是怎樣實現的。我們知道pnpmanager實現稱為Root的匯流排驅動程式,既然是Root,肯定也就不存在Attached上誰提供的PDO。實際上你可以使用windbg看看其實現:在Free Build XP中,其只實現return STATUS_SUCCESS(xor eax,eax/ret)了。我在用checked build的時候,其只是對IRQL進行檢查:RtlAssert(KeGetCurrentIrql()<=APC_LEVEL)。
當然對於ACPI_HAL或是PCI匯流排,其AddDevice與pnpmanager實現肯定不同,她們肯定要IoCreateDevice進立FDO,Attached至pnpmanager提供的PDO或是上層匯流排提供的PDO,實現層次關係。我們繼續使用windbg驗證我這種思路:
kd> !drvobj /Driver/ACPI_HAL
Driver object (812f6ce8) is for:
/Driver/ACPI_HAL
Driver Extension List: (id , addr)
Device Object list:
812f6a90 812f6bb0
ACPI_HAL匯出的兩個裝置哪個是PDO,哪個是FDO呢(你應該會明白至少有一個FDO吧)。我們知道pnp管理器在發現匯流排後,首先會調用匯流排驅動程式的AddDevice入口,然後才會發各種各樣的IRP_MJ_PNP的各種MinorFunction指示匯流排驅動程式枚舉匯流排,對串連至上面的裝置各建立PDO等等。這兒我只是描述通常情況,對於如DDK中附帶的Toaster這樣的虛擬匯流排,其枚舉匯流排上的裝置是通過應用程式發相應的IOCTL來指示PDO的建立(通過IoInvalidateDeviceRelations讓pnp管理器發IRP_MJ_PNP)。當然就算是Toaster實現的這樣虛擬匯流排,及AddDevice入口,即FDO總是先於PDO的建立。因為對於同一個DRIVER_OBJECT服務的裝置,其是通過DRIVER_OBJECT的DeviceObject形成單向鏈表,這個DeviceObject指示鏈表頭,由DEVICE_OBJECT的NextDevice聯結成鏈表。而對於IoCreateDevice建立的裝置,後建立的裝置,總是插入表頭,而FDO基本上總是最先建立的,所以總是在表尾。有了這些分析,對於ACPI_HAL上面的輸出812f6bb0即是FDO,而812f6a90則是PDO。OK,這樣這個PDO,則又是上層acpi.sys實現的匯流排驅動程式的下層PDO了。
kd> !devobj 812f6a90
Device object (812f6a90) is for:
00000052 /Driver/ACPI_HAL DriverObject 812f6ce8
Current Irp 00000000 RefCount 0 Type 0000002a Flags 00001040
Dacl e1518a6c DevExt 812f6b48 DevObjExt 812f6b60 DevNode 812f63a8
ExtensionFlags (0000000000)
AttachedDevice (Upper) 812ad960 /Driver/ACPI
Device queue is not busy.
你看看上層是不是/Driver/ACPI(AttachedDevice行)。而PCI匯流排又是Attached至acpi.sys實現的Micrsoft ACPI-Compliant System上的。注意acpi可不像acpi_hal一樣,只有一個PDO,而PCI匯流排也不是第一個PDO。有了這些知識,我想你也應該可以比較容易的發現pci匯流排附接至哪個pdo吧。一個更簡單的辦法是使用!devstack命令dump PCI匯流排的FDO了。
kd> !drvobj pci
Driver object (812ef850) is for:
/Driver/PCI
Driver Extension List: (id , addr)
Device Object list:
812f39e8 812f3d58 812f4e40 812f4038
812f0710 812f0908 812f0c58 812f14e8
812f0e38
kd> !devstack 812f0e38
!DevObj !DrvObj !DevExt ObjectName
> 812f0e38 /Driver/PCI 812f0ef0
812dc8c0 /Driver/ACPI 812f5660 00000058
!DevNode 812f1008 :
DeviceInst is "ACPI/PNP0A03/2&daba3ff&0"
ServiceName is "pci"
devstack命令實際上使用DEVICE_OBJECT的AttachedDevice與存於DeviceObjectExtension(注意這兒是DeviceObjectExtension而不是DeviceExtension)結構成員中的AttachedTo來顯示裝置棧的。當然devstack命令還顯示裝置對應的DEVICE_NODE。DEVICE_NODE是為了支援pnp而引入的一個系統資料結構,完整的DEVICE_NODE定義是非常複雜且非常龐大的,我就不列出來了,幾個重要的成員如Sibling(兄弟DEVICE_NODE),Child(子DEVICE_NODE),Parent(父DEVICE_NODE),裝置狀態PNP_DEVNODE_STATE,資源使用方式CM_RESOURCE_LIST,介面類型INTERFACE_TYPE,裝置標識、服務名ServiceName等等。
從我的提示DeviceNode含有Sibling、Child、Parent等成員,我們很容易想到系統可能會將所有DeviceNode組成一個樹(與檔案系統的分類樹類似),實際上正是這樣的,核心變數IopRootDeviceNode指示這顆樹的根。!devnode命令可以看出這個根節點的情況,如果你使用!devnode 0 1命令的話,你將活生生的看到一個windbg的devmgmt.msc版。實際上系統的SetupDi(setupapi.dll匯出的用於裝置安裝的API)就是通過這個來dump出所有的裝置的。devmgmt.msc間接的使用這些API(你可別像我一樣訝異.msc檔案只是一個由MMC.EXE解析的XML檔案)。同樣OSR的DeviceTree肯定也使用了這些API。
到現在為止你可能更加困惹,啥是啥的PDO,系統如何知道哪個匯流排附接至哪兒,以形成裝置層次。秘密在於註冊表,裝置安裝時通過.inf檔案等向註冊表加入內容指示系統的載入順序。早先的.inf檔案真的是好複雜,在我看來絕不亞於perl指令碼。Windows 2000為你自動做了太多太多了(我真想知道到底做了什麼,嘻嘻)。
註冊表中HKLM/SYSTEM/CurrentControlSet/Enum與HKLM/SYSTEM/CurrentControlSet/Control/Class共同協作來完成這樣的任務,當然與Legacy驅動程式一樣離不開HKLM/SYSTEM/CurrentControlSet/Services了。為了完整的傳述Windows 2000/XP分層驅動模型,有必要在最後提及一下Filter驅動程式,這是附加在匯流排驅動程式或是其他驅動上或下的一類驅動,用於增強,改變原有裝置的某些功能。由剛提及的註冊表中的Enum與Class項的UpperFilters與LowerFilters的值提供。
最後,說明一下,由Windows 2000/XP的這個分層驅動區分出很多概念,如中介層驅動程式,故名思義,如Windows 2000/XP中隨處可見的類驅動程式。類驅動程式實現某一類裝置的共同功能,沒有牽涉到實際硬體的訪問。如磁碟類驅動程式,在Windows 2000/XP中有disk.sys,tape.sys,cdrom.sys,他們均藉助於classpnp.sys實作類別驅動程式,以disk.sys為例,她根本不管是IDE介面或是SCSI介面,由底層的atapi.sys或是scsiport.sys這些miniport/port驅動實現與特定硬體的互動。
另外再提及一點,可以說FileSystem Filter是一個Legacy的分層驅動(只使用DEVICE_OBJECT的AttachedDevice成員。而對於網路驅動程式,如NDIS Intermediate Drivers(含NDIS Filter Intermediate Drivers與NDIS MUX Intermediate Drivers)也可以看作是分層驅動的應用,只不過在Windows 2000/XP中由Ndis Wrapper Library(ndis.sys)隱藏了太多的資訊(隱藏了使用IRP的真正面目),也可以這麼說ndis.sys使用其內部自身的結構定義,如NDIS_M_DRIVER_BLOCK、NDIS_MINIPORT_BLOCK、NDIS_PROTOCOL_BLOCK、NDIS_OPEN_BLOCK這些定義之間的微妙關係,自身實現了一個層次化的結構。
基本上講了個大概,不知道說得清楚不清楚。期待你的反饋指導交流(tsu00@263.net)。