为设备命名 Windows NT使用对象管理器集中管理大量的内部数据结构,包括我们讨论过的驱动程序对象和设备对象。David Solomon在《Inside Windows NT, Second Edition (Microsoft Press, 1998)》的第三章“System Mechanisms”中给出了关于Windows NT对象管理器和命名空间的一个比较完整的阐述。对象都有名称,对象管理器用一个层次化的命名空间来管理这些名称。图2-16是DevView显示的顶层对象名。图中以文件夹形式显示的对象是目录对象,它可以包含子目录或常规对象,其它图标则代表正常对象。(从这一点上看,DevView与平台SDK中的WINOBJ工具相类似,但WINOBJ不能给出设备对象和驱动程序的相关信息)。
通常设备对象都把自己的名字放到/Device目录中。在Windows 2000中,设备的名称有两个用途。第一个用途,设备命名后,其它内核模式部件可以通过调用IoGetDeviceObjectPointer函数找到该设备,找到设备对象后,就可以向该设备的驱动程序发送IRP。
另一个用途,允许应用程序打开命名设备的句柄,这样它们就可以向驱动程序发送IRP。应用程序可以使用标准的CreateFile API打开命名设备句柄,然后用ReadFile、WriteFile,和DeviceIoControl向驱动程序发出请求。应用程序打开设备句柄时使用//./路径前缀而不是标准的UNC(统一命名约定)名称,如C:/MYFILE.CPP或//FRED/C-Drive/HISFILE.CPP。在内部,I/O管理器在执行名称搜索前自动把//./转换成/??/。为了把/??目录中的名字与名字在其它目录(例如,在/Device目录)中的对象相连接,对象管理器实现了一种称为符号连接(symbolic link)的对象。
符号连接 符号连接有点象桌面上的快捷方式,符号连接在Windows NT中的主要用途是把处于列表前面的DOS形式的名称连接到设备上。图2-17显示了/??目录的部分内容,这里就有一些符号名,例如,“C:”和其它一些用DOS命名方案命名的驱动器名称,它们被连接到/Device目录中,而这些设备对象的真正名称就放在/Device目录中。符号连接可以使对象管理器在分析一个名称时能跳到命名空间的某个地方。例如,如果我用CreateFile打开名称为“C:/MYFILE.CPP”的对象,对象管理器将以下面过程打开该文件:
内核模式代码最开始看到的名称是/??/C:/MYFILE.CPP。对象管理器在根目录中查找“??”。 找到/??目录后,对象管理器在其中查找“C:”。它发现找到的对象是一个符号连接,所以它就用这个符号连接组成一个新的内核模式路径名:/Device/HarddiskVolume1/MYFILE.CPP,然后析取它。 使用新路径名后,对象管理器重新在根目录中查找“Device”。 找到/Device目录后,对象管理器在其中查找“HarddiskVolume1”,最后它找到一个以该名字命名的设备。
现在,对象管理器要创建一个IRP,然后把它发到HarddiskVolume1设备的驱动程序。该IRP最终将使某个文件系统驱动程序或其它驱动程序定位并打开一个磁盘文件。描述文件系统驱动程序的工作过程已经超出了本书的范围。如果我们使用设备名COM1,那么最终收到该IRP的将是/Device/Serial0的驱动程序。
用户模式程序可以调用DefineDosDevice创建一个符号连接,如下例:
BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "//Device//SECTEST_0");
如果你需要在WDM驱动程序中创建一个符号连接,可以调用IoCreateSymbolicLink函数:
IoCreateSymbolicLink(linkname, targname);
linkname是要创建的符号连接名,targname是要连接的名字。顺便说一下,对象管理器并不关心targname是否是已存在对象的名字,如果连接到一个未定义的符号名,那么访问该符号连接将简单地收到一个错误。如果你想允许用户模式程序能超越这个连接而转到其它地方,应使用IoCreateUnprotectedSymbolicLink函数替代上面函数。
ARC名字 在ARC(Advanced RISC Computing)架构的计算机中,Windows 2000需要依赖一个称为ARC命名的概念。你可以在BOOT.INI文件中看到使用中的ARC名字,BOOT.INI文件位于引导驱动器的根目录。下面是该文件的一个例子:
[boot loader]timeout=30default=c:/[operating systems]C:/="Microsoft Windows 98"scsi(0)disk(1)rdisk(0)partition(1)/BETA2F="Win2k Beta-2 (Free Build)" /fastdetect /noguibootscsi(0)disk(1)rdisk(0)partition(1)/WINNT ="Win2K Beta-3 (Free Build)" /fastdetect /noguiboot
在Intel平台上,象scsi(0)disk(1)rdisk(0)partition(1)的ARC名其实是符号连接,它们存在于内核的/ArcName目录中。
除了硬盘之外的块存储设备,它们的驱动程序在初始化时需要调用IoAssignArcName建立一个这样的符号连接。由于系统引导的需要,I/O管理器自动为硬盘设备建立ARC名。
应该命名设备对象吗? 决定为设备对象命名之前,你应该多想一想。如果命名了设备对象,那么任何内核模式程序都可以打开该设备的句柄。另外,任何内核模式或用户模式程序都能创建连接到该设备的符号连接,并可以使用这个符号连接打开设备的句柄。你可能允许也可能不允许这种事情发生。是否命名设备对象的主要考虑是安全问题。当有人打开一个命名对象的句柄时,对象管理器将检查他是否有权这样做。当IoCreateDevice为你创建设备对象时,它也为设备对象设置了一个默认安全描述符(基于第四个参数中的设备类型)。下面是三个基本分类,I/O管理器基于这些分类来选择安全描述符。大部分文件系统设备对象(磁盘、CD-ROM、文件、磁带)将得到“public default unrestricted”ACL(访问控制表)。该表对系统(SYSTEM)和管理员(administrator)之外的所有账户给予了SYNCHRONIZE、READ_CONTROL、FILE_READ_ATTRIBUTES、FILE_TRAVERSE访问权限。顺便说一下,文件系统设备对象就是作为CreateFile函数的目标而存在,CreateFile函数将打开一个由文件系统管理的文件。 磁盘设备和网络文件系统对象将得到与文件系统对象相同的ACL,但做了一些修改。例如,任何人对命名软磁盘设备对象都有全部访问权,管理员有足够的权限运行ScanDisk。(用户模式的网络支持DLL需要更大的权限来访问其对应文件系统驱动程序的设备对象,这就是网络文件系统需要与其它文件系统区别对待的原因) 。 所有其它的设备对象将得到“public open unrestricted”ACL,它允许任何有设备句柄的人不受限制地使用该设备。 可以看出,如果非磁盘设备的驱动程序在调用IoCreateDevice时给出设备对象名,那么任何人都可以读写这个设备,因为默认安全设置几乎允许用户有全部的访问权限,而且在创建符号连接时根本不进行安全检查。安全检查仅发生在对设备的打开操作上,基于命名对象的安全描述符。这对于在同一堆栈中的有更严格安全限制的其它设备对象也是这样。
DevView可以显示设备对象的安全属性。你可以通过测试一个文件系统、一个磁盘设备、或者任何其它随机存取设备了解到我刚描述过的默认操作规则。
PDO也得到一个默认安全描述符,但这个安全描述符可能被存储在硬件键或类键的Properties子键中的安全描述符超越(当两者都存在时,硬件键中的超越值有更高的优先权)。即使没有指定安全描述符超越,如果硬件键或类键的Properties子键中有设备类型或特征的超越值,那么I/O管理器也会基于新类型为对象构造一个新的默认安全描述符。但I/O管理器不会超越PDO上面的任何其它设备对象的安全设置。因此,由于超越的影响,你不应该命名你的设备对象。但不要失望,应用程序仍可以使用注册的接口(interface)访问你的设备。
关于安全问题的最后一点:当对象管理器析取对象名时,对于名字的中间部分仅需要具有FILE_TRAVERSE访问权,它仅在最终对象名上执行全部的安全检查。所以,假设某个设备对象可以通过/Device/SECTEST_0名或符号连接/??/SecurityTest_0名到达,那么,如果设备对象的安全描述符设置为拒绝写,则试图以写方式打开//./SecurityTest_0的用户模式应用程序将被阻塞。但如果应用程序试图打开名为//./SecurityTest_0/ExtraStuff的对象,那么打开请求(IRP_MJ_CREATE形式)将被允许,而此时用户对//./SecurityTest_0/仅有FILE_TRAVERSE权限。I/O管理器希望设备驱动程序自己去处理额外名称部件的安全检查。
为了避免涉及到我刚描述过的安全问题,你可以在调用IoCreateDevice时指定设备特征参数为FILE_DEVICE_SECURE_OPEN。该标志将使Windows 2000在额外名称部件存在的情况下仍检查调用者是否有权限打开设备句柄。
设备名称 如果你决定命名设备对象,通常应该把对象名放在名称空间的/Device分支中。为了命名设备对象,首先应该创建一个UNICODE_STRING结构来存放对象名,然后把该串作为调用IoCreateDevice的参数:
UNICODE_STRING devname;RtlInitUnicodeString(&devname, L"//Device//Simple0");IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...);
我将在下一章中讨论RtlInitUnicodeString的用法。
通常,驱动程序用设备类型串后加上一个以0开始的实例号作为设备对象名(如上面的Simple0)。一般,你不希望象我上面做的那样使用带有硬编码性质的名称。你希望用串操作函数动态地合成一个名字:
UNICODE_STRING devname;static LONG lastindex = -1;LONG devindex = InterlockedIncrement(&lastindex);WCHAR name[32];_snwprintf(name, arraysize(name), L"//Device//SIMPLE%2.2d", devindex);RtlInitUnicodeString(&devname, name);IoCreateDevice(...);
我将在后两章中解释上面代码中出现的服务函数。如上面代码所示,从私有设备类型得出的实例号应该是一个静态变量。
设备命名的注意事项 如果仅仅想在开发过程中为应用程序打开设备句柄提供一个快速方法,你应该在/??目录中为设备赋予一个名字。然而,对于一个产品级的驱动程序来说,最好把设备对象名放到/Device目录中。
/??目录以前叫做/DosDevices。实际上,/DosDevice仍可以使用,但它本身是/??目录的符号连接。这种改变将使经常查找的用户模式目录名能位于字母排序的目录列表前面。如果你要在命名中使用/??,应该先参考本章“Windows 98兼容问题”节中的注意事项。
注意,上面提到的把设备对象名放到/??目录中可能不适用于Windows 2000的Terminal Server版本。由于设备对象不能复制到控制台事务的外边,而符号连接可以,因此你应该在/Device目录中保存设备命名,而在/DosDevices目录中放一个符号连接。
在以前版本的Windows NT中,某些种类设备(特别是磁盘、磁带、串行口,和并行口)的驱动程序通过调用IoGetConfigurationInformation来获得一个全局表的指针,该表包含这些类中的设备计数。驱动程序应使用当前计数值来合成设备名称,例如Harddisk0、Tape1,等等,并同时增加该计数器的值。然而,WDM驱动程序并不需要使用这个服务函数以及它返回的计数器表,为这些类中的设备构造名称现在是Microsoft专有的类驱动程序的责任,如DISK.SYS。
设备接口用旧的命名方法命名设备对象,并创建一个应用程序能够使用的符号连接,存在着两个主要问题。命名设备对象所带来的潜在安全问题我们已经讨论过。此外,访问设备的应用程序需要先知道设备采用的命名方案。如果你的硬件仅由你的应用程序访问,那么不会有什么问题。但是,如果有其它公司想为你的硬件写应用程序,并且有许多硬件公司想制作相似的设备,那么设计一个合适的命名方案是困难的。最后,许多命名方案将依赖于程序员所说的自然语言,这不是一个好的选择。
为了解决这些问题,WDM引入了一个新的设备命名方案,该方案是语言中立的、易于扩展的、可用于许多硬件和软件厂商,并且易于文档化。该方案依靠一个设备接口(device interface)的概念,它基本上是软件如何访问硬件的一个说明。一个设备接口被一个128位的GUID唯一标识。你可以用平台SDK中的UUIDGEN工具或者GUIDGEN工具生成GUID,这两个工具输出同一种数,但格式不同。这个想法就象某些工业组织联合起来共同制定某种硬件的标准访问方法一样。在标准制作过程中,产生了一些GUID,这些GUID将永远关联到某些接口上。
关于GUID GUID用于标识软件接口,它与COM(部件对象模型)中用于标识COM接口的标识符相同,它还用于OSF(开放软件基金)的DCE(分布式计算环境)中,标识RPC(远程过程调用)目标。如果你想了解GUID如何生成以及为什么能在统计意义上唯一,请参考Kraig Brockschmidt的《Inside OLE, Second Edition (Microsoft Press, 1995)》第66页,原始算法规范由OSF制定,相关部分见http://www.opengroup.org/onlinepubs/9629399/apdxa.htm。
为了在设备驱动程序中使用GUID,首先需要使用UUIDGEN或者GUIDGEN生成GUID,然后把结果放到头文件中。GUIDGEN更易于使用,它允许选择GUID的输出格式,并把结果送到剪贴板。图2-18显示了GUIDGEN的运行窗口。你可以把它的输出粘贴到头文件中:
// {CAF53C68-A94C-11D2-BB4A-00C04FA330A6}DEFINE_GUID(<<name>>, 0xCAF53C68, 0xA94C, 0x11D2, 0xBB, 0x4A, 0x00, 0xC0, 0x4F, 0xA3, 0x30, 0xA6);
然后,用有意义的名字换掉<<name>>,如GUID_SIMPLE,并把这个定义包含到驱动程序或应用程序中。
图2-18. 使用GUIDGEN生成GUID
我想接口类似于蛋白质合成器,它能制作活细胞的细胞膜。访问特定种类设备的应用程序有自己的蛋白质合成器,它就象一把钥匙,可以插入到所有有匹配合成器的设备驱动程序中。如图2-19。
图2-19. 用设备接口匹配应用程序和设备
注册设备接口 调用IoRegisterDeviceInterface函数,功能驱动程序的AddDevice函数可以注册一个或多个设备接口:
#include <initguid.h> <--1#include "guids.h" <--2...NTSTATUS AddDevice(...){ ... IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname); <--3 ...}
我们包含了GUIDS.H头文件,那里定义了DEFINE_GUID宏。DEFINE_GUID通常声明一个外部变量。在驱动程序的某些地方,我们不得不为将要引用的每个GUID保留初始化的存储空间。系统头文件INITGUID.H利用某些预编译指令使DEFINE_GUID宏在已经定义的情况下仍能保留该存储空间。 我使用单独的头文件来保存我要引用的GUID定义。这是一个好的想法,因为用户模式的代码也需要包含这些定义,但它们不需要那些仅与内核模式驱动程序有关的声明。 IoRegisterDeviceInterface的第一个参数必须是设备PDO的地址。第二个参数指出与接口关联的GUID,第三个参数指出额外的接口细分类名。只有Microsoft的代码才使用名称细分类方案。第四个参数是一个UNICODE_STRING串的地址,该串用于接收设备对象的符号连接名。 IoRegisterDeviceInterface的返回值是一个Unicode串,这样在不知道驱动程序编码的情况下,应用程序能用该串确定并打开设备句柄。顺便说一下,这个名字比较丑陋;后面例子是我在Windows 98中为Sample设备生成的名字:/DosDevices/0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6}。
注册过程实际就是先创建一个符号连接名,然后再把它存入注册表。之后,当响应PnP请求IRP_MN_START_DEVICE时,驱动程序将调用IoSetDeviceInterfaceState函数“使能”该接口:
IoSetDeviceInterfaceState(&pdx->ifname, TRUE);
在响应这个调用过程中,I/O管理器将创建一个指向设备PDO的符号连接对象。以后,驱动程序会执行一个功能相反的调用禁止该接口(用FALSE做参数调用IoSetDeviceInterfaceState)。最后,I/O管理器删除符号连接对象,但它保留了注册表项,即这个名字将总与设备的这个实例关联;但符号连接对象与硬件一同到来或消失。
因为接口名最终指向PDO,所以PDO的安全描述符将最终控制设备的访问权限。这样比较好,因为只有管理员才可以通过控制台控制PDO的安全属性。
摘自: http://blog.csdn.net/bluesun777/archive/2008/03/05/2150960.aspx
