编写WDM驱动,如何写AddDevice例程

AddDevice例程


在前一节中,我讲述了当WDM驱动程序被第一次装入时如何初始化。通常,一个驱动程序可以被多个设备利用。WDM驱动程序有一个特殊的AddDevice函数,PnP管理器为每个设备实例调用该函数。该函数的原型如下:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
{
}

DriverObject参数指向一个驱动程序对象,就是你在DriverEntry例程中初始化的那个驱动程序对象。pdo参数指向设备堆栈底部的物理设备对象。

对于功能驱动程序,其AddDevice函数的基本职责是创建一个设备对象并把它连接到以pdo为底的设备堆栈中。相关步骤如下:

  1. 调用IoCreateDevice创建设备对象,并建立一个私有的设备扩展对象。
  2. 寄存一个或多个设备接口,以便应用程序能知道设备的存在。另外,还可以给出设备名并创建符号连接。
  3. 初始化设备扩展和设备对象的Flag成员。
  4. 调用IoAttachDeviceToDeviceStack函数把新设备对象放到堆栈上。

下面我将详细解释这些步骤。

创建设备对象

调用IoCreateDevice函数创建设备对象,例如:

PDEVICE_OBJECT fdo;
NTSTATUS status = IoCreateDevice(DriverObject,
                                 sizeof(DEVICE_EXTENSION),
                                 NULL,
                                 FILE_DEVICE_UNKNOWN,
                                 FILE_DEVICE_SECURE_OPEN,
                                 FALSE,
                                 &fdo);

第一个参数(DriverObject)就是AddDevice的第一个参数。该参数用于在驱动程序和新设备对象之间建立连接,这样I/O管理器就可以向设备发送指定的IRP。

第二个参数是设备扩展结构的大小。正如我在本章前面讲到的,I/O管理器自动分配这个内存,并把设备对象中的DeviceExtension指针指向这块内存。

第三个参数在本例中为NULL。它可以是命名该设备对象的UNICODE_STRING串的地址。决定是否命名设备对象以及以什么名字命名还需要仔细考虑,我将在本节后面深入讨论这个问题。

第四个参数(FILE_DEVICE_UNKNOWN)是表2-4中列出的设备类型。这个值可以被设备硬件键或类键中的超越值所替代,如果这两个键都含有该参数的超越值,那么硬件键中的超越值具有更高的优先权。对于属于某个已存在类的设备,必须在这些地方指定正确的值,因为驱动程序与外围系统的交互需要依靠这个值。另外,设备对象的默认安全设置也依靠这个设备类型值。

第五个参数(FILE_DEVICE_SECURE_OPEN)为设备对象提供Characteristics标志(见表2-3)。这些标志主要关系到块存储设备(如软盘、CDROM、Jaz等等)。未公开标志位FILE_AUTOGENERATED_DEVICE_NAME仅用于内部使用,并不是DDK文档忘记提到该标志。这个参数同样也能被硬件键或类键中的对应值超越,如果两个值都存在,那么硬件键中的超越值具有更高的优先权。

第六个参数(FALSE)指出设备是否是排斥的。通常,对于排斥设备,I/O管理器仅允许打开该设备的一个句柄。这个值同样也能被注册表中硬件键和类键中的值超越,如果两个超越值都存在,硬件键中的超越值具有更高的优先权。

注意
排斥属性仅关系到打开请求的目标是命名设备对象。如果你遵守Microsoft推荐的WDM驱动程序设计方针,没有为设备对象命名,那么打开请求将直接指向PDO。PDO通常不能被标记为排斥,因为总线驱动程序没有办法知道设备是否需要排斥特征。把PDO标为排斥的唯一的机会在注册表中,即设备硬件键或类键的Properties子键含有 Exclusive超越值。为了完全避免依赖排斥属性,你应该利用IRP_MJ_CREAT例程弹出任何有违规行为的打开请求。

第七个参数(&fdo)是存放设备对象指针的地址,IoCreateDevice函数使用该变量保存刚创建设备对象的地址。

如果IoCreateDevice由于某种原因失败,则它返回一个错误代码,不改变fdo中的值。如果IoCreateDevice函数返回成功代码,那么它同时也设置了fdo指针。然后我们进行到下一步,初始化设备扩展,做与创建新设备对象相关的其它工作,如果在这之后又发现了错误,那么在返回前应先释放刚创建的设备对象并返回状态码。见下面例子代码:

NTSTATUS status = IoCreateDevice(...);
if (!NT_SUCCESS(status))
  return status;
...
if ()
{
  IoDeleteDevice(fdo);
  return status;
}

NTSTATUS状态代码和NT_SUCCESS宏的解释见下一章。

为设备命名

Windows NT使用对象管理器集中管理大量的内部数据结构,包括我们讨论过的驱动程序对象和设备对象。DavidSolomon在《Inside Windows NT, SecondEdition (Microsoft Press, 1998)》的第三章“SystemMechanisms”中给出了关于WindowsNT对象管理器和命名空间的一个比较完整的阐述。对象都有名称,对象管理器用一个层次化的命名空间来管理这些名称。图2-16是DevView显示的顶层对象名。图中以文件夹形式显示的对象是目录对象,它可以包含子目录或常规对象,其它图标则代表正常对象。(从这一点上看,DevView与平台SDK中的WINOBJ工具相类似,但WINOBJ不能给出设备对象和驱动程序的相关信息)

图2-16. 用DevView观察命名空间

通常设备对象都把自己的名字放到/Device目录中。在Windows2000中,设备的名称有两个用途。第一个用途,设备命名后,其它内核模式部件可以通过调用IoGetDeviceObjectPointer函数找到该设备,找到设备对象后,就可以向该设备的驱动程序发送IRP。

另一个用途,允许应用程序打开命名设备的句柄,这样它们就可以向驱动程序发送IRP。应用程序可以使用标准的CreateFile API打开命名设备句柄,然后用ReadFileWriteFile,和DeviceIoControl向驱动程序发出请求。应用程序打开设备句柄时使用//./路径前缀而不是标准的UNC(统一命名约定)名称,如C:/MYFILE.CPP或//FRED/C-Drive/HISFILE.CPP。在内部,I/O管理器在执行名称搜索前自动把//./转换成/??/。为了把/??目录中的名字与名字在其它目录(例如,在/Device目录)中的对象相连接,对象管理器实现了一种称为符号连接(symboliclink)的对象。

符号连接

符号连接有点象桌面上的快捷方式,符号连接在WindowsNT中的主要用途是把处于列表前面的DOS形式的名称连接到设备上。图2-17显示了/??目录的部分内容,这里就有一些符号名,例如,“C:”和其它一些用DOS命名方案命名的驱动器名称,它们被连接到/Device目录中,而这些设备对象的真正名称就放在/Device目录中。符号连接可以使对象管理器在分析一个名称时能跳到命名空间的某个地方。例如,如果我用CreateFile打开名称为“C:/MYFILE.CPP”的对象,对象管理器将以下面过程打开该文件:

  1. 内核模式代码最开始看到的名称是/??/C:/MYFILE.CPP。对象管理器在根目录中查找“??”。
  2. 找到/??目录后,对象管理器在其中查找“C:”。它发现找到的对象是一个符号连接,所以它就用这个符号连接组成一个新的内核模式路径名:/Device/HarddiskVolume1/MYFILE.CPP,然后析取它。
  3. 使用新路径名后,对象管理器重新在根目录中查找“Device”。
  4. 找到/Device目录后,对象管理器在其中查找“HarddiskVolume1”,最后它找到一个以该名字命名的设备。

图2-17. /??目录和部分符号连接

现在,对象管理器要创建一个IRP,然后把它发到HarddiskVolume1设备的驱动程序。该IRP最终将使某个文件系统驱动程序或其它驱动程序定位并打开一个磁盘文件。描述文件系统驱动程序的工作过程已经超出了本书的范围。如果我们使用设备名COM1,那么最终收到该IRP的将是/Device/Serial0的驱动程序。

用户模式程序可以调用DefineDosDevice创建一个符号连接,如下例:

BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "//Device//SECTEST_0");

图2-17中显示了上面调用的结果。

如果你需要在WDM驱动程序中创建一个符号连接,可以调用IoCreateSymbolicLink函数:

IoCreateSymbolicLink(linkname, targname);

linkname是要创建的符号连接名,targname是要连接的名字。顺便说一下,对象管理器并不关心targname是否是已存在对象的名字,如果连接到一个未定义的符号名,那么访问该符号连接将简单地收到一个错误。如果你想允许用户模式程序能超越这个连接而转到其它地方,应使用IoCreateUnprotectedSymbolicLink函数替代上面函数。

ARC名字

在ARC(Advanced RISC Computing)架构的计算机中,Windows2000需要依赖一个称为ARC命名的概念。你可以在BOOT.INI文件中看到使用中的ARC名字,BOOT.INI文件位于引导驱动器的根目录。下面是该文件的一个例子:

[boot loader]
timeout=30
default=c:/
[operating systems]
C:/="Microsoft Windows 98"
scsi(0)disk(1)rdisk(0)partition(1)/BETA2F="Win2k Beta-2 (Free Build)"  /fastdetect /noguiboot
scsi(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管理器基于这些分类来选择安全描述符。(参考表2-4中的第二列)

  • 大部分文件系统设备对象(磁盘、CD-ROM、文件、磁带)将得到“public defaultunrestricted”ACL(访问控制表)。该表对系统(SYSTEM)和管理员(administrator)之外的所有账户给予了SYNCHRONIZE、READ_CONTROL、FILE_READ_ATTRIBUTES、FILE_TRAVERSE访问权限。顺便说一下,文件系统设备对象就是作为CreateFile函数的目标而存在,CreateFile函数将打开一个由文件系统管理的文件。
  • 磁盘设备和网络文件系统对象将得到与文件系统对象相同的ACL,但做了一些修改。例如,任何人对命名软磁盘设备对象都有全部访问权,管理员有足够的权限运行ScanDisk。(用户模式的网络支持DLL需要更大的权限来访问其对应文件系统驱动程序的设备对象,这就是网络文件系统需要与其它文件系统区别对待的原因)
  • 所有其它的设备对象将得到“public openunrestricted”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。该标志将使Windows2000在额外名称部件存在的情况下仍检查调用者是否有权限打开设备句柄。

设备名称

如果你决定命名设备对象,通常应该把对象名放在名称空间的/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仍可以使用,但它本身是/??目录的符号连接。这种改变将使经常查找的用户模式目录名能位于字母排序的目录列表前面。如果你要在命名中使用/??,应该先参考本章“Windows98兼容问题”节中的注意事项。

注意,上面提到的把设备对象名放到/??目录中可能不适用于Windows 2000的TerminalServer版本。由于设备对象不能复制到控制台事务的外边,而符号连接可以,因此你应该在/Device目录中保存设备命名,而在/DosDevices目录中放一个符号连接。

在以前版本的WindowsNT中,某些种类设备(特别是磁盘、磁带、串行口,和并行口)的驱动程序通过调用IoGetConfigurationInformation来获得一个全局表的指针,该表包含这些类中的设备计数。驱动程序应使用当前计数值来合成设备名称,例如Harddisk0、Tape1,等等,并同时增加该计数器的值。然而,WDM驱动程序并不需要使用这个服务函数以及它返回的计数器表,为这些类中的设备构造名称现在是Microsoft专有的类驱动程序的责任,如DISK.SYS。

设备接口

用旧的命名方法命名设备对象,并创建一个应用程序能够使用的符号连接,存在着两个主要问题。命名设备对象所带来的潜在安全问题我们已经讨论过。此外,访问设备的应用程序需要先知道设备采用的命名方案。如果你的硬件仅由你的应用程序访问,那么不会有什么问题。但是,如果有其它公司想为你的硬件写应用程序,并且有许多硬件公司想制作相似的设备,那么设计一个合适的命名方案是困难的。最后,许多命名方案将依赖于程序员所说的自然语言,这不是一个好的选择。

为了解决这些问题,WDM引入了一个新的设备命名方案,该方案是语言中立的、易于扩展的、可用于许多硬件和软件厂商,并且易于文档化。该方案依靠一个设备接口(deviceinterface)的概念,它基本上是软件如何访问硬件的一个说明。一个设备接口被一个128位的GUID唯一标识。你可以用平台SDK中的UUIDGEN工具或者GUIDGEN工具生成GUID,这两个工具输出同一种数,但格式不同。这个想法就象某些工业组织联合起来共同制定某种硬件的标准访问方法一样。在标准制作过程中,产生了一些GUID,这些GUID将永远关联到某些接口上。

关于GUID

GUID用于标识软件接口,它与COM(部件对象模型)中用于标识COM接口的标识符相同,它还用于OSF(开放软件基金)的DCE(分布式计算环境)中,标识RPC(远程过程调用)目标。如果你想了解GUID如何生成以及为什么能在统计意义上唯一,请参考KraigBrockschmidt的《Inside OLE, SecondEdition (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                                                        <--1
#include "guids.h"                                                      <--2
...
NTSTATUS AddDevice(...)
{
  ...
  IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname);     <--3
  ...
}
  1. 我们包含了GUIDS.H头文件,那里定义了DEFINE_GUID宏。DEFINE_GUID通常声明一个外部变量。在驱动程序的某些地方,我们不得不为将要引用的每个GUID保留初始化的存储空间。系统头文件INITGUID.H利用某些预编译指令使DEFINE_GUID宏在已经定义的情况下仍能保留该存储空间。
  2. 我使用单独的头文件来保存我要引用的GUID定义。这是一个好的想法,因为用户模式的代码也需要包含这些定义,但它们不需要那些仅与内核模式驱动程序有关的声明。
  3. IoRegisterDeviceInterface的第一个参数必须是设备PDO的地址。第二个参数指出与接口关联的GUID,第三个参数指出额外的接口细分类名。只有Microsoft的代码才使用名称细分类方案。第四个参数是一个UNICODE_STRING串的地址,该串用于接收设备对象的符号连接名。

IoRegisterDeviceInterface的返回值是一个Unicode串,这样在不知道驱动程序编码的情况下,应用程序能用该串确定并打开设备句柄。顺便说一下,这个名字比较丑陋;后面例子是我在Windows98中为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的安全属性。

枚举设备接口 内核模式代码和用户模式代码都能定位含有支持它们感兴趣接口的设备。下面我将解释如何在用户模式中枚举所有含有特定接口的设备。枚举代码写起来十分冗长,最后我不得不写一个C++类来实现。你可以在DEVICELIST.CPP和DEVICELIST.H文件中找到这些代码,这些文件是第八章“电源管理”中WDMIDLE例子的一部分。它们声明并实现了一个CDeviceList类,该类包含一个CDeviceListEntry对象数组。这两个类声明如下:

class CDeviceListEntry
{
public:
  CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname);
  CDeviceListEntry(){}
  CString m_linkname;
  CString m_friendlyname;
};

class CDeviceList
{
public:
  CDeviceList(const GUID& guid);
  ~CDeviceList();
  GUID m_guid;
  CArray m_list;
  int Initialize();
};

该类使用了CString类和CArray模板,它们都是MFC的一部分。这两个类的构造函数仅简单地把它们的参数复制到数据成员中:

CDeviceList::CDeviceList(const GUID& guid)
{
  m_guid = guid;
}

CDeviceListEntry::CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname)
{
  m_linkname = linkname;
  m_friendlyname = friendlyname;
}

所有实际的工作都发生在CDeviceList::Initialize函数中。其执行过程大致是这样:先枚举所有接口GUID与构造函数得到的GUID相同的设备,然后确定一个“友好”名,我们希望向最终用户显示这个名字。最后返回找到的设备号。下面是这个函数的代码:

int CDeviceList::Initialize()
{
  HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);      <--1
  if (info == INVALID_HANDLE_VALUE)
    return 0;
  SP_INTERFACE_DEVICE_DATA ifdata;
  ifdata.cbSize = sizeof(ifdata);
  DWORD devindex;
  for (devindex = 0; SetupDiEnumDeviceInterfaces(info, NULL, &m_guid, devindex, &ifdata); ++devindex)   <--2
  {
    DWORD needed;
    SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL);                             <--3

    PSP_INTERFACE_DEVICE_DETAIL_DATA detail = (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed);
    detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
    SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};
    SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did));

    TCHAR fname[256];                                                                                   <--4
    if (!SetupDiGetDeviceRegistryProperty(info,
                                          &did,
                                          SPDRP_FRIENDLYNAME,
                                          NULL,
                                          (PBYTE) fname,
                                          sizeof(fname),
                                          NULL)
      && !SetupDiGetDeviceRegistryProperty(info,
                                           &did,
                                           SPDRP_DEVICEDESC,
                                           NULL,
                                           (PBYTE) fname,
                                           sizeof(fname),
                                           NULL)
        )
      _tcsncpy(fname, detail->DevicePath, 256);

    CDeviceListEntry e(detail->DevicePath, fname);                                                      <--5
    free((PVOID) detail);

    m_list.Add(e);
  }

  SetupDiDestroyDeviceInfoList(info);
  return m_list.GetSize();
}
  1. 该语句打开一个枚举句柄,我们用它寻找寄存了指定GUID接口的所有设备。
  2. 循环调用SetupDiEnumDeviceInterfaces以寻找每个匹配的设备。
  3. 有两项信息是我们需要的,接口的“细节”信息和设备实例信息。这个“细节”信息就是设备的符号名。因为它的长度可变,所以我们两次调用了SetupDiGetDeviceInterfaceDetail。第一次调用确定了长度,第二次调用获得了名字。
  4. 通过询问注册表中的FriendlyName键或DeviceDesc键,我们获得了设备的“友好”名称。
  5. 我们用设备符号名同时作为连接名和友好名创建了类CDeviceListEntry的一个临时实例e
友好名

你可能会疑惑,注册表怎么会有设备的FriendlyName名。安装设备驱动程序的INF文件中有一个指定设备参数的HW段,这些参数将被添加到注册表中。通常我们可以在这里为设备提供一个FriendlyName名。

其它全局性的设备初始化操作

在AddDevice中还需要加入其它一些步骤来初始化设备对象,下面我将按顺序描述这些步骤。

初始化设备扩展

设备扩展的内容和管理全部由用户决定。该结构中的数据成员应直接反映硬件的专有细节以及对设备的编程方式。大多数驱动程序都会在这里放入一些数据项,下面代码声明了一个设备扩展结构:

typedef struct _DEVICE_EXTENSION {                                <--1
  PDEVICE_OBJECT DeviceObject;                                  <--2
  PDEVICE_OBJECT LowerDeviceObject;                             <--3
  PDEVICE_OBJECT Pdo;                                           <--4
  UNICODE_STRING ifname;                                        <--5
  IO_REMOVE_LOCK RemoveLock;                                    <--6
  DEVSTATE devstate;                                            <--7
  DEVSTATE prevstate;
  DEVICE_POWER_STATE devpower;
  SYSTEM_POWER_STATE syspower;
  DEVICE_CAPABILITIES devcaps;                                  <--8
  ...
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
  1. 我模仿DDK中官方的结构声明模式声明了这个结构。
  2. 我们可以用设备对象中的DeviceExtension指针定位自己的设备扩展。同样,我们有时也需要在给定设备扩展时能定位设备对象。因为某些函数的逻辑参数就是设备扩展本身(这里有设备每个实例的全部信息)。所以,我认为这里应该有一个DeviceObject指针。
  3. 我在一些地方曾提到过,在调用IoAttachDeviceToDeviceStack函数时,应该把紧接着你下面的设备对象的地址保存起来。LowerDeviceObject成员用于保存这个地址。
  4. 有一些服务例程需要PDO的地址,而不是堆栈中某个高层设备对象的地址。由于定位PDO非常困难,所以最好的办法是在AddDevice执行时在设备扩展中保存一个PDO地址。
  5. 无论你用什么方法(符号连接或设备接口)命名你的设备,都希望能容易地获得这个名字。所以,这里我用一个Unicode串成员ifname来保存设备接口名。如果你使用一个符号连接名而不是设备接口,应该使用一个有相关含义的成员名,例如“linkname”。
  6. 当你调用IoDeleteDevice删除这个设备对象时,需要使用一个自旋锁来解决同步安全问题,我将在第六章中讨论同步问题。因此,需要在设备扩展中分配一个IO_REMOVE_LOCK对象。AddDevice有责任初始化这个对象。
  7. 你可能需要一个成员来记录设备当前的PnP状态和电源状态。DEVSTATE和POWERSTATE是枚举类型变量,我假设事先已经在头文件中声明了这些变量类型。我将在后面章节中讨论这些状态变量的用途。
  8. 电源管理的另一个部分涉及电源能力设置的恢复,设备扩展中的devcaps结构用于保存这些设置。

下面是AddDevice中的初始化语句(着重设备扩展部分的初始化):

NTSTATUS AddDevice(...)
{
  PDEVICE_OBJECT fdo;
  IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo);
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
  pdx->DeviceObject = fdo;
  pdx->Pdo = pdo;
  IoInitializeRemoveLock(&pdx->RemoveLock, ...);
  pdx->devstate = STOPPED;
  pdx->devpower = PowerDeviceD0;
  pdx->syspower = PowerSystemWorking;
  IoRegisterDeviceInterface(..., &pdx->ifname);
  pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...);
}

初始化默认的DPC对象

许多设备使用中断来报告操作完成。我将在第七章“读写数据”中讨论中断处理,其中对中断服务例程(ISR)能做什么做了严格的限定。特别是ISR不能调用用于报告IRP完成的例程(IoCompleteRequest)。利用DPC(推迟过程调用)可以绕过这个限制。你的设备对象中应包含一个辅助DPC对象,它可以调度你的DPC例程,该对象应该在设备对象创建后不久被初始化。

NTSTATUS AddDevice(...)
{
  IoCreateDevice(...);
  IoInitializeDpcRequest(fdo, DpcForIsr);
}

设置缓冲区对齐掩码

执行DMA传输的设备直接使用内存中的数据缓冲区工作。HAL要求DMA传输中使用的缓冲区必须按某个特定界限对齐,而且设备也可能有更严格的对齐需求。设备对象中的AlignmentRequirement域表达了这个约束,它是一个位掩码,等于要求的地址边界减一。下面语句可以把任何地址圈入这个界限:

PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) ((SIZE_T) address & ~ar);

还可以把任意地址圈入下一个对齐边界:

PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) (((SIZE_T) address + ar) & ~ar);

在这两段代码中,我使用了SIZE_T把指针类型(它可以是32位也可以是64位,这取决于编译的目标平台)转化成一个整型,该整型与原指针有同样的跨度范围。

IoCreateDevice把新设备对象中的AlignmentRequirement域设置成HAL要求的值。例如,Intel的x86芯片没有对齐需求,所以AlignmentRequirement的默认值为0。如果设备需要更严格的缓冲区对齐(例如设备有总线主控的DMA能力,要求对齐数据缓冲区),应该修改这个默认值,如下:

if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)
  fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;

我假设你在驱动程序某处已定义了一个名为MYDEVICE_ALIGNMENT的常量,它是2的幂,代表设备的数据缓冲区对齐需求。

其它对象

设备可能还有其它一些需要在AddDevice中初始化的对象。这些对象可能包括各种同步对象,各种队列头(queueanchors),聚集/分散列表缓冲区,等等。事实上,在本书的其它地方讨论这些对象的初始化更合适。

初始化设备标志

设备对象中有两个标志位需要在AddDevice中初始化,并且它们在以后也不会改变,它们是DO_BUFFERED_IO和DO_DIRECT_IO标志。你只能设置并使用其中一个标志,它将决定你以何种方式处理来自用户模式的内存缓冲区。(我将在第七章中讨论这两种缓冲模式的不同,以及你如何选择)由于任何在后面装入的上层过滤器驱动程序将复制你的标志设置,所以在AddDevice中做这个选择十分重要。如果你在过滤器驱动程序装入后改变了设置,它们可能会不知道。

设备对象中有两个标志位属于电源管理范畴。与前两个缓冲区标志不同,这两个标志在任何时间都可以被改变。我将在第八章中详细讨论它们,但这里我先介绍一下。DO_POWER_PAGABLE意味着电源管理器将在PASSIVE_LEVEL级上向你发送IRP_MJ_POWER请求。DO_POWER_INRUSH意味着你的设备在上电时将汲取大量电流,因此,电源管理器将确保没有其它INRUSH设备同时上电。

设置初始电源状态

大部分设备一开始就进入全供电状态。如果你知道你的设备的初始电源状态,应该告诉电源管理器:

POWER_STATE state;
state.DeviceState = PowerDeviceD0;
PoSetPowerState(fdo, DevicePowerState, state);

电源管理的细节请见第八章。

建立设备堆

每个过滤器驱动程序和功能驱动程序都有责任把设备对象放到设备堆栈上,从PDO开始一直向上。你可以调用IoAttachDeviceToDeviceStack完成你那部分工作:

NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo)
{
  PDEVICE_OBJECT fdo;
  IoCreateDevice(..., &fdo);
  pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);
}

IoAttachDeviceToDeviceStack的第一个参数是新创建的设备对象的地址。第二个参数是PDO地址。AddDevice的第二个参数也是这个地址。返回值是紧接着你下面的任何设备对象的地址,它可以是PDO,也可以是其它低级过滤器设备对象。如果该函数失败则返回一个NULL指针,因此你的AddDevice函数也是失败的,应返回STATUS_DEVICE_REMOVED。

清除DO_DEVICE_INITIALIZING标志

在AddDevice中最后一件需要做的事是清除设备对象中的DO_DEVICE_INITIALIZING标志:

fdo->Flags &= ~DO_DEVICE_INITIALIZING;

当这个标志设置时,I/O管理器将拒绝任何打开该设备句柄的请求或向该设备对象上附着其它设备对象的请求。在驱动程序完成初始化后,必须清除这个标志。在以前版本的WindowsNT中,大部分驱动程序在DriverEntry中创建所有需要的设备对象。当DriverEntry返回时,I/O管理器自动遍历设备对象列表并清除该标志。但在WDM驱动程序中,设备对象在DriverEntry返回后才创建,所以I/O管理器不会自动清除这个标志,驱动程序必须自己清除它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值