WebCrazy(http://webcrazy.yeah.net)
可扩展性是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这些定义之间的微妙关系,自身实现了一个层次化的结构