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这些定义之间的微妙关系,自身实现了一个层次化的结构
文章整理:站长天空 网址:http://www.z6688.com/
以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢!




