织梦CMS - 轻松建站从此开始!

罗索

DDK开发介绍

jackyhwei 发布于 2011-01-28 09:59 点击:次 
限于篇幅,这类函数详细的用法可以参见DDK文档说明。这部分我们主要学习了内存管理部分。如我们前面所说,内存管理在内核编程中是非常重要的。通常我们在应用程序编写中的不良习惯都不应该带到驱动程序开发中。
TAG:

本篇我们介绍开发之前的准备工作,包括开发环境准备、预备知识。
开发环境准备
对于开发WDM驱动程序来说,我们有以下三个常用组合:
1.直接使用Windows DDK
2.使用DriverStudio
3.使用Windriver
下面我们分别比较三种方式的优缺点。
第一种:开发难度大一些,而且有很多烦琐的工作要作,大部分都是通用的基础性的工作。但如果选用这种方式的话你将对整个体系结构会有很好的理解和把握。
第二种:难度低一些,工具软件已经帮你作了很多基础性的工作。也封装了一些细节,你只要专心去作你需要的操作,但由于封装的问题,可能会带来一些bug。有可能导致项目的失败。
第三种:几乎没有难度(从开发驱动的角度)。很容易,但只能开发硬件相关的驱动,事实上你写的只是定制和调用它提供的通用驱动而已。效率上有问题。工作频率不是很高。但开发花费的时间很少。是上面的几 乃至几十分之一。 
建议: 
用windriver作驱动程序的原型,用driverstudio作最终发行的驱动程序,如果驱动程序很复杂的话,建议直接使用ddk开发。 
上面的几种情况都需要vc++作为辅助开发环境。(ddk也可以直接用命令行工具,但比较烦),前两种情况都需要ddk。开发时间上,第一种最长,第三种最短,第二种可以认为是前面两种方案的折衷。    
如果更具体一点的话,我们可以把以上三种形式比作三种开发工具,那就是 ms c,vc++,Vb。
如果SDK没bug的话,用ms c开发的纯sdk程序的bug是最少的。Vc++由于对sdk进行了封装,必然会引出一些新的bug。Vb开发程序虽然快了一些,但运行效率比前两种方式差了很多。
这样说明这三种方式的话,大家一定会明白了。
我们为了简便起见,使用ddk+VC的方式。
首 先,我们按正常方式安装好vc++ 6.0。不过据微软文档说ddk98只支持vc++ 5.0。我手里没有vc++5.0,在vc++6.0下试了一 下,证明可以使用,不过设置很困难的。当然,如果你不觉得烦的话,也可以直接用build工具即可。在安装好vc++后再安装ddk开发包,这样不容易出 错。
如果你使用DriverStudio开发包,请先安装好vc++6.0,然后再安装它,在安装softice时注意选择通用显卡驱动,这样一般情况下都能正常使用。

预备知识
    在开发环境安装完成后,我们将要步入开发过程。在实际动手之前,我们先要学习一些预备知识。
    在设备驱动程序中,要作很多工作,包括初始化,设备对象创建等等工作,其中一些是很重要的,必须实现,一些是可选的,如果你的驱动对这些功能的要求不是很高的话,可以不实现。
    要实现的功能主要有以下几个:
初始化
创建和删除设备
I/O请求的超时处理
I/O请求的撤消
访问硬件资源
处理Windows的输入/输出请求
串行化对设备的访问
调用其它驱动程序
处理一个可热拔插的设备被加入或删除的情况
处理电源管理请求
使用Windows管理诊断功能
处理Windows的打开和关闭文件句柄的请求

从 实际工作情况来看,只有初始化模块是必不可少的。但是只有初始化模块的驱动程序什么工作也干不了,只能说它仅仅是一个概念意义上的驱动程序而已,好比失去 感觉的植物人(躯体存在,但已经没有了意志)。通常情况下,一个完整的驱动程序至少要能响应用户态程序发出的I/O访问请求。大多数情况下驱动程序要访问 它们所支持的硬件资源,并且要支持简单的电源管理功能和Windows管理诊断功能或能向系统日志写入信息。
    WDM驱动程序通常由PnP管理器载入内存,然后调用它之中的AddDevice例程来创建设备。当然,在此时还要需要一个inf安装文件而来指明该驱动程序需要的一些参数。
    系 统内核通常通过向驱动程序发送IRP包来运行驱动程序中的实现代码。我们以Windows向设备发出的ReadFile调用为例:此时Windows向驱 动程序发出一个“读”请求的IRP包,读取缓冲区的大小和位置作为IRP包中的参数指定(IRP实际上是一个数据结构,包含几个域)。如果你作过 Windows的程序,特别是用 VC作过开发的话,你应该知道,windows用户态应用程序是消息驱动的,应用程序中的代码是通过消息机制的触发而获 得运行的机会的,需要的参数是通过消息的域(wParam、lParam)传给应用程序。实际上驱动程序的动作还是可以看作是一种消息驱动方式,只不过内 核态的“消息”已经不再称作消息,而是被称作I/O请求包(IRP)。
    驱动程序通常使用DriverEntry作为入口点,与我们在Windows应用程序中定义的WinMain相似。通常情况下,它是驱动程序的默认入口点。
    注: 标准Build脚本将驱动程序入口点定为DriverEntry,你最好遵守这个假设,否则必须修改Build脚本。
    在 这个入口函数中,我们必须作必要的初始化设置,并设置必要的回调函数。我们可以这样理解:我们用c++(特别是用VC++)时,我们在类的构造函数中要作 必要的初始化操作,并要在类中作消息处理方法的映射,这样才能让需要的消息得到适当的处理。我们也要在DriverEntry例程中设置必要的IRP处理 函数。
    一般情况下,DriverEntry例程要设置以下几个IRP处理函数:
•    DriverUnload 指向驱动程序的清除例程。I/O管理器会在卸载驱动程序前调用该例程。通常,WDM驱动程序的DriverEntry例程一般不分配任何资源,所以DriverUnload例程也没有什么清除工作要做。 
•    DriverExtension->AddDevice 指向驱动程序的AddDevice函数。PnP管理器将为每个硬件实例调用一次AddDevice例程。这样将创建一个该设备对象。 
•    DriverStartIo 如果驱动程序使用标准的IRP排队方式,应该设置该成员,使其指向驱动程序的StartIo例程。如果你不理解什么是“标准”排队方式,不要着急,能后的教程中你就会完全明白,许多驱动程序都使用这种方法。 
•    MajorFunction 是一个指针数组,I/O管理器把每个数组元素都初始化成指向一个空函数,这个空函数仅返回失败。驱动程序可能仅需要处理几种类型的IRP,所以至少应该设置与那几种IRP类型相对应的指针元素,使它们指向相应的派遣函数。 
下面是一段DriverEntry例程的示例:

  1. extern "C" NTSTATUS  DriverEntry(IN PDRIVER_OBJECT DriverObject
  2. , IN PUNICODE_STRING RegistryPath)
  3.   DriverObject->DriverUnload = DriverUnload; <--1 
  4.   DriverObject->DriverExtension->AddDevice = AddDevice; 
  5.   DriverObject->DriverStartIo = StartIo; 
  6.   DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
  7.  //设置各个IRP的处理函数    ,这三个IRP是每一个WDM驱动程序必须处理的。                 
  8.   DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower; 
  9.   DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = DispatchWmi; 
  10.   ...  <--3 
  11.   servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool
  12. , RegistryPath->Length + sizeof(WCHAR));    <--4 
  13.   if (!servkey.Buffer) 
  14.     return STATUS_INSUFFICIENT_RESOURCES; 
  15.   servkey.MaximumLength = RegistryPath->Length + sizeof(WCHAR); 
  16.   RtlCopyUnicodeString(&servkey, RegistryPath); 
  17.   return STATUS_SUCCESS;   <--5 

1.    前三条语句为驱动程序的其它入口点设置了函数指针。在这里,我们用了能表达其功能的名字命名了这些函数:DriverUnload、AddDevice、StartIo。 
2.    每 个WDM驱动程序必须能处理PNP、POWER、SYSTEM_CONTROL这三种请求;应该在这里为这些请求指定派遣函数。在早期的 Windows 2000 DDK中,IRP_MJ_SYSTEM_CONTROL曾被称作IRP_MJ_WMI,所以我把系统控制派遣函数命名为 DispatchWmi。 
3.    在省略号处,你可以插入设置其它MajorFunction指针的代码。 
4.    如果驱动程序需要访问设备的服务键,可以在这里备份RegistryPath串。例如,如果驱动程序要作为WMI生产者,则需要备份这个串。这里我假设已经在某处声明了一个类型为UNICODE_STRING的全局变量servkey。 
5.    返回STATUS_SUCCESS指出函数成功。如果函数失败,应该返回NTSTATUS.H中的一个错误代码,或者返回用户定义的错误代码。STATUS_SUCCESS的值为0。 
关于DriverUnload例程的补充说明:
在WDM驱动程序中,DriverUnload例程的作用就是释放DriverEntry例程在全局初始化过程中申请的任何资源,但它几乎没什么可做。如果你在DriverEntry中备份了RegistryPath串,应该在这里释放备份所占用的内存:
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
  RtlFreeUnicodeString(&servkey);//释放先前申请的资源
}
如果DriverEntry例程返回一个失败状态代码,系统将不再调用DriverUnload例程。所以,不能让DriverEntry例程出错后产生任何副作用,必须在它返回错误代码前消除副作用(释放掉申请的系统资源)。
一般情况下,一个驱动程序可以被多个设备利用。WDM驱动程序有一个特殊的AddDevice函数,PnP管理器为每个设备实例调用该函数。以下为该函数的原型定义:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
{
}
DriverObject参数指向一个驱动程序对象,就是你在DriverEntry入口例程中初始化的那个驱动程序对象。pdo参数指向设备堆栈底部的物理设备对象。
对于功能驱动程序,其AddDevice函数的基本职责是创建一个设备对象并把它连接到以pdo为底的设备堆栈中。相关步骤如下: 
1.    调用IoCreateDevice创建设备对象,并建立一个私有的设备扩展对象。 
2.    注册一个或多个设备接口,以便应用程序能够发现设备的存在。另外,还可以给出设备名并创建符号连接。 
3.    初始化设备扩展和设备对象的Flag成员。 
4.    调用IoAttachDeviceToDeviceStack函数把新设备对象放到堆栈上。 
下面我将详细解释这些步骤。
创建设备对象
调用IoCreateDevice函数创建设备对象,例如:

  1. PDEVICE_OBJECT fdo; 
  2. NTSTATUS status = IoCreateDevice(DriverObject, 
  3. sizeof(DEVICE_EXTENSION), 
  4. NULL, 
  5. FILE_DEVICE_UNKNOWN, 
  6. FILE_DEVICE_SECURE_OPEN, 
  7. FALSE, 
  8. &fdo); 

第一个参数(DriverObject) 就是AddDevice的第一个参数。该参数用于在驱动程序和新设备对象之间建立连接,这样I/O管理器就可以向设备发送指定的IRP。
第二个参数是设备扩展结构的大小。I/O管理器自动分配这个内存,并把设备对象中的DeviceExtension指针指向这块内存。
第三个参数在本例中为NULL。它可以是命名该设备对象的UNICODE_STRING串的地址。决定是否命名设备对象以及以什么名字命名还需要仔细考虑,我将在后面深入认真地讨论这个问题。
第 四个参数(FILE_DEVICE_UNKNOWN) 是设备类型。这个值可以被设备硬件键(注册表中包含该硬件信息的键值)或类键(注册表中包含该类驱 动信息的键值)中的可替换值(overriding values)所替代,如果这两个键都含有该参数的替换值,那么硬件键中的可替换值具有更高的优先 权。对于属于某个已存在类的设备,必须在这些地方指定正确的值,因为驱动程序与外围系统的交互需要依靠这个值。另外,设备对象的默认安全设置也依靠这个设 备类型值。
第五个参数(FILE_DEVICE_SECURE_OPEN) 为设备对象提供Characteristics标志。这些标志主要关 系到块存储设备(如软盘、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指针。然后我们进行到下一步,初始化设备扩展,做与创建新设备对象相关的其它工作,如果在这之后又发现了错误,那么在返回前应先释放 刚创建的设备对象并返回状态码。见下面例子代码:

  1. NTSTATUS status = IoCreateDevice(...); 
  2. if (!NT_SUCCESS(status)) 
  3.   return status; 
  4. ... 
  5. if (<some other error discovered>) 
  6.   IoDeleteDevice(fdo); 
  7.   return status; 

为设备命名
Windows 使 用对象管理器集中管理系统中的大量的内部数据结构(每个对象在系统中都表现为一个数据结构),包括驱动程序对象和设备对象。为了便于区别,每个对象都有名 称,对象管理器用一个层次化的命名空间来管理这些名称。图中是DevView(一个设备观察工具,在驱动开发网 站<http://www.driverdevelop.com有下载>)显示的顶层对象名。此工具以文件夹形式显示的对象是目录对象,它可 以包含子目录或常规对象,其它图标则代表正常对象。
通常设备对象都把自己的名字放到\Device目录中。在Windows 2000中,设备的 名称有两个用途。第一个用途,通过命名后,其它内核模式部件可以通过调用IoGetDeviceObjectPointer函数找到该设备,找到设备对象 后,就可以向该设备的驱动程序发送IRP(I/O请求包)。
另一个用途,允许用户态的应用程序打开命名设备的句柄,这样它们就可以向驱动程序发送 IRP。应用程序可以使用标准的CreateFile API打开命名设备句柄,然后用ReadFile、WriteFile,和 DeviceIoControl向驱动程序发出请求(关于这些API函数的详细说明和使用,我们将在后面的文章中详述)。应用程序打开设备句柄时使 用\\.\路径前缀。在C/C++语言程序中使用时,需要转化为’\\\\.\\’,这是由语法规定的。在内部,I/O管理器在执行名称搜索前自动把 \\.\转换成\??\。为了把\??目录中的名字与名字在其它目录(例如,在\Device目录)中的对象相连接,对象管理器实现了一种称为符号连接 (symbolic link)的对象。
符号连接
符号连接有点象WIDOWS桌面上的快捷方式,符号连接在Windows NT/2K中 的主要用途是把处于列表前面的DOS形式的名称连接到设备上。符号连接可以使对象管理器在分析一个名称时能跳到命名空间的某个地方。例如我们通常见到的C 盘,其实它是就是一个设备(磁盘)的符号链接。如果你用过unix/linux操作系统的话,你会对符号链接有所理解,这里的符号链接相当于unix /linux系统中的软链接。
 
术语
类键
所有设备类的类键都出现在HKLM\System\CurrentControlSet\Control\Class键中。它们的键名是由Microsoft赋予的GUID值。
硬件键 
硬件键包含单个设备的信息。

在上期中我们讲了符号连接,在应用层开发中我们可以调用以下函数来创建一个\??目录下的符号链接:
BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "\\Device\\SECTEST_0");
调用成功后,将会在设备命名空间的\??目录下生成一个名为”barf“的符号链接,该链接指向”“\\Device\\SECTEST_0“这个对象。
在核心态的驱动程序中,我们需要调用以下的函数来创建相应的符号链接:
IoCreateSymbolicLink(linkname, targname);
Linkname是要创建的符号链接名,相当于上面函数中的”barf”,targname是该链接指向的设备对象。
如 果你创建了一个指向不存在的设备对象的符号链接,系统并不会作任何检查,当你访问这个符号链接时只会收到一个错误报告。所以你必须要自己保证链接的目的对 象真正存在。如果你想允许用户模式程序能超越这个连接而转到其它地方,应使用IoCreateUnprotectedSymbolicLink函数替代上 面的IoCreateSymbolicLink函数。
给设备命名后我们就可以很方便地打开该设备进行访问了。但在方便的同时你需要注意一个很严重 的问题:“安全性”。一旦为设备命名后,符何核心态的驱动程序都可以打开该设备的句柄,从而访问此设备。而且更糟的是,任何用户态的应用程序也可以通过建 立该设备名的符号链接而访问到该设备。而这种情况可能是你不愿意看到的。
一旦你决定要为你的设备命名时,你应该将这个设备对象的名称放到对象名空间的“\Device”目录中,我们可以使用以下的核心态函数来创建设备,同时给设备命名:
  1. UNICODE_STRING devname; 
  2. RtlInitUnicodeString(&devname, L"\\Device\\Simple0"); 
  3. IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...); 
这 里的UNICODE_STRING devname就是用来存放设备名的地方。RtlInitUnicodeString是unicode串初始化函数, 第一个参数是要初始化的变量地址,第二个为设备名常量。第二个参数前的大写L是将这个常量转换成此函数需要的宽字符串。一般我们使用如下的格式为设备命 名:
设备名0
其中的0为设备的实例号(即产生实例的顺序)。
说到这里我要提醒一下大家,在驱动程序中一般不使用Ansi字符串,取而代之的是UniCode字符串,它以16位表示一个字符。这点和在WinCE下开发软件很相似。
在 以前的老式驱动程序中(Win 3.2 or Win95)中大量使用设备命名(包括直接用名字和名字的符号链接)的方式来访问设备。这样做有两个很主要 的问题。一是安全性问题。在上面我们已经讲了这样做有潜在的安全性问题,符何程序只要知道该设备的名字就可以访问它。第二个问题是你的应用程序要访问该设 备必须事先知道它的名字,否则不能访问。这在测试用的设备或私有设备(只为你的应用程序服务而不向第三方提供接口)的情况下是可以的。但是如果你的硬件设 备还要为第三方的程序服务或者有可能有第三方的公司为你的设备写驱动时就会有很多问题。可能你对该设备的命名会和其它的设备相重复。而且这样的命名很依赖 程序员本身所使用的自然语言。
为了解决这个问题,微软在设计WDM框架时引入了一个新的命名方案。该方案与任何自然语言无关,且易于扩展,可广泛 地用于软件用硬件,并且易于归档。该方案依靠一个设备接口的概念。它基本上是软件如何访问硬件的一个说明。一个设备接口由一个唯一的128位的GUID标 识。一般情况下我们可以使用GUIDGEN工具生成这个标识(GUIDGEN工具可以在VC++企业版的可执行程序目录下找到)。由于采用了独特的生成算 法,你永远也不用担心重复出现GUID的情况。这样一个GUID就唯一标识了一种设备接口。
 
生成的代码如下所示
  1. // {CAF53C68-A94C-11D2-BB4A-00C04FA330A6} 
  2. DEFINE_GUID(<<name>>,//将此处改成你的名字  
  3. 0xCAF53C68, 0xA94C, 0x11D2, 0xBB, 0x4A, 0x00, 0xC0, 0x4F, 0xA3, 0x30, 0xA6); 

此为GUIDGEN程序工作时的截屏。可以选择四种格式输出。一般情况下我们选择第二种。并且为了便于管理,我们把要用于的GUID声明集中放到一个头文件中。

 

你可以把设备接口想象成锁和钥匙。这样应用程序就可以准确地访问需要访问的设备。
我们可以在功能驱动程序的AddDevice例程序中注册一个或多个设备接口,程序如下:
  1. #include <initguid.h> 
  2. #include "guids.h" 
  3. //其它声明 
  4. NTSTATUS AddDevice(...) 
  5. //其它代码 
  6. IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname); 
  7. //其它代码 
其中的GUID_SIPMLE就是我们要注册的接口的GUID的定义。对此段代码,我们作如下说明:
我 们包含了GUIDS.H头文件,那里定义了DEFINE_GUID宏。DEFINE_GUID通常声明一个外部变量。在驱动程序的某些地方,我们不得不为 将要引用的每个GUID保留初始化的存储空间。系统头文件INITGUID.H利用某些预编译指令使DEFINE_GUID宏在已经定义的情况下仍能保留 该存储空间。 
我使用单独的头文件来保存我要引用的GUID定义。这是一个好的想法,因为用户模式的代码也需要包含这些定义,但它们不需要那些仅与内核模式驱动程序有关的声明。 
IoRegisterDeviceInterface 的第一个参数必须是设备PDO的地址。第二个参数指出与接口关联的GUID,第三个参数指出额外的接口细分类名。只有Microsoft的代码才使用名称 细分类方案。第四个参数是一个UNICODE_STRING串的地址,该串用于接收设备对象的符号连接名。 
IoRegisterDeviceInterface 的返回值是一个Unicode 字符串,这样可以在不知道驱动程序的具体编码的情况下(也就是说没看过你的驱动程序的具体代码),应用程序可以确定并打开 该设备的句柄。这个返回值是很奇怪的,形如以下情形:\DosDevices\0000000000000007#{CAF53C68-A94C- 11d2-BB4A-00C04FA330A6}.
即它的名字是0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6}
注册过程实际上是先创建一个符号链接,然后把它记入注册表。当驱动程序在响应PnP请求IRP+MN_START_DEVICE时,驱动程序将调用IoSetDeviceInterfaceState函数”使能”该接口:
IoSetDeviceInterfaceState(&pdx->ifname, TRUE);
所谓使能也就是使此符号链接指向具体的PDO对象。
在 响应这个调用过程中,I/O管理器将创建一个指向设备PDO的符号连接对象。以后,驱动程序会执行一个功能相反的调用禁止该接口(用FALSE做参数调用 IoSetDeviceInterfaceState)。最后,I/O管理器删除符号连接对象,但它保留了注册表项,即这个名字将总与设备的这个实例关 联;但符号连接对象与硬件一同到来或消失。
枚举设备接口
 内核模式代码和用户模式代码都能定位含有它们感兴趣接口的设备。下面我将解释如 何在用户模式中枚举所有含有特定接口的设备。枚举代码写起来十分冗长,最后我不得不写一个C++类来实现。你可以在DEVICELIST.CPP和 DEVICELIST.H文件中找到这些代码。它们声明并实现了一个CDeviceList类,该类包含一个CDeviceListEntry对象数组。
一些声明代码去掉了,详细的文章请看驱动开发网上的志宁专栏(http://www.driverdevelop.com/column.php?sortid=3)。
所有实际的工作都发生在CDeviceList::Initialize函数中。其执行过程大致是这样:先枚举所有接口GUID与构造函数得到的GUID相同的设备,然后确定一个“友好”名,我们希望向最终用户显示这个名字。最后返回找到的设备号。下面是这个函数的代码:
  1. int CDeviceList::Initialize()//枚举设备的接口的具体实现代码 
  2.   HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL
  3. , DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);     
  4.   if (info == INVALID_HANDLE_VALUE) 
  5.     return 0; 
  6.   SP_INTERFACE_DEVICE_DATA ifdata; 
  7.   ifdata.cbSize = sizeof(ifdata); 
  8.   DWORD devindex; 
  9.   for (devindex = 0; SetupDiEnumDeviceInterfaces(info
  10. , NULL, &m_guid, devindex, &ifdata); ++devindex)     
  11.   { 
  12.     DWORD needed; 
  13.     SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL);                 
  14.  
  15.     PSP_INTERFACE_DEVICE_DETAIL_DATA detail =
  16.  (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed);  
  17.     detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA); 
  18.     SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)}; 
  19.     SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did)); 
  20.     TCHAR fname[256];                                             
  21.     if (!SetupDiGetDeviceRegistryProperty(info, 
  22. &did, 
  23. SPDRP_FRIENDLYNAME,
  24. NULL, 
  25. (PBYTE) fname, 
  26. izeof(fname), 
  27. NULL) 
  28.       && !SetupDiGetDeviceRegistryProperty(info, 
  29. &did, 
  30. SPDRP_DEVICEDESC, 
  31. NULL, 
  32. (PBYTE) fname, 
  33. sizeof(fname), 
  34. NULL) 
  35.     ) 
  36.       _tcsncpy(fname, detail->DevicePath, 256); 
  37.     CDeviceListEntry e(detail->DevicePath, fname);                             
  38.     free((PVOID) detail); 
  39.     m_list.Add(e); 
  40.   } 
  41.   SetupDiDestroyDeviceInfoList(info); 
  42.   return m_list.GetSize(); 
该语句打开一个枚举句柄,我们用它寻找包含了指定GUID接口的所有设备。 
循环调用SetupDiEnumDeviceInterfaces以寻找每个与此相匹配的设备。 
有两项信息是我们需要的,接口的“细节”信息和设备实例信息。这个“细节”信息就是设备的符号名。因为它的长度可变,所以我们两次调用了SetupDiGetDeviceInterfaceDetail。第一次调用确定了长度,第二次调用获得了名字。 
通过询问注册表中的FriendlyName键或DeviceDesc键,我们获得了设备的“友好”名称。 
我们用设备符号名同时作为连接名和友好名创建了类CDeviceListEntry的一个临时实例e。 
友好名 
你可能会疑惑,注册表怎么会有设备的FriendlyName名。安装设备驱动程序的INF文件中有一个指定设备参数的段,这些参数将被添加到注册表中。通常我们可以在这里为设备提供一个FriendlyName名。
注:在windows 2k下和Windows 98下的inf文件有少许的不同,即使是同一个设备的inf文件,也要作过适当修改后才能同时用于两个平台下。
作好以上工作后,我们还要初始化一些其它的数据结构才能完成设备加载工作。
在AddDevice中还需要加入其它一些步骤来初始化设备对象,下面我将按顺序描述这些步骤。
设备扩展的内容和管理全部由用户决定。该结构中的数据成员应直接反映硬件的专有细节以及对设备的编程方式。大多数驱动程序都会在这里放入一些数据项,下面代码声明了一个设备扩展结构:
  1. typedef struct _DEVICE_EXTENSION { 
  2.   PDEVICE_OBJECT DeviceObject; 
  3.   PDEVICE_OBJECT LowerDeviceObject; 
  4.   PDEVICE_OBJECT Pdo; 
  5.   UNICODE_STRING ifname; 
  6.   IO_REMOVE_LOCK RemoveLock; 
  7.   DEVSTATE devstate; 
  8.   DEVSTATE prevstate; 
  9.   DEVICE_POWER_STATE devpower; 
  10.   SYSTEM_POWER_STATE syspower; 
  11.   DEVICE_CAPABILITIES devcaps; 
  12.  ...//其它一些结构 
  13. } DEVICE_EXTENSION, *PDEVICE_EXTENSION; 
我模仿DDK中官方的结构声明模式声明了这个结构。 
我 们可以用设备对象中的DeviceExtension指针定位自己的设备扩展。同样,我们有时也需要在给定设备扩展时能定位设备对象。因为某些函数的逻辑 参数就是设备扩展本身(这里有设备每个实例的全部信息)。所以,我认为这里应该有一个DeviceObject指针。 
我在一些地方曾提到过,在调用IoAttachDeviceToDeviceStack函数时,应该把紧接着你下面的设备对象的地址保存起来。LowerDeviceObject成员用于保存这个地址。 
有一些服务例程需要PDO的地址,而不是堆栈中某个高层设备对象的地址。由于定位PDO非常困难,所以最好的办法是在AddDevice执行时在设备扩展中保存一个PDO地址。 
无论你用什么方法(符号连接或设备接口)命名你的设备,都希望能容易地获得这个名字。所以,这里我用一个Unicode串成员ifname来保存设备接口名。如果你使用一个符号连接名而不是设备接口,应该使用一个有相关含义的成员名,例如“linkname”。 
当你调用IoDeleteDevice删除这个设备对象时,需要使用一个自旋锁来解决同步安全问题,我将在第六章中讨论同步问题。因此,需要在设备扩展中分配一个IO_REMOVE_LOCK对象。AddDevice有责任初始化这个对象。 
你可能需要一个成员来记录设备当前的PnP状态和电源状态。DEVSTATE和POWERSTATE是枚举类型变量,我假设事先已经在头文件中声明了这些变量类型。我将在后面章节中讨论这些状态变量的用途。 
电源管理的另一个部分涉及电源能力设置的恢复,设备扩展中的devcaps结构用于保存这些设置。 
下面是AddDevice中的初始化语句(着重设备扩展部分的初始化):
  1. NTSTATUS AddDevice(...) 
  2.   PDEVICE_OBJECT fdo; 
  3.   IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo); 
  4.   PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; 
  5.   pdx->DeviceObject = fdo; 
  6.   pdx->Pdo = pdo; 
  7.   IoInitializeRemoveLock(&pdx->RemoveLock, ...); 
  8.   pdx->devstate = STOPPED; 
  9.   pdx->devpower = PowerDeviceD0; 
  10.   pdx->syspower = PowerSystemWorking; 
  11.   IoRegisterDeviceInterface(..., &pdx->ifname); 
  12.   pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...); 
初始化默认的DPC对象
许 多设备使用中断来完成操作。中断服务例程(ISR)不能调用用于报告IRP完成的函数(IoCompleteRequest)。利用DPC(延迟过程调 用)可以避开这个限制。你的设备对象中应包含一个辅助DPC对象,它可以调度你的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中初始化的对象。这些对象可能包括各种同步对象,各种队列头(queue anchors),聚集/分散列表缓冲区,等等。
初始化设备标志
设 备对象中有两个标志位需要在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。
在AddDevice中最后一件需要做的事是清除设备对象中的DO_DEVICE_INITIALIZING标志:
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
当 这个标志设置时,I/O管理器将拒绝任何打开该设备句柄的请求或向该设备对象上附着其它设备对象的请求。在驱动程序完成初始化后,必须清除这个标志。在以 前版本的Windows NT中,大部分驱动程序在DriverEntry中创建所有需要的设备对象。当DriverEntry返回时,I/O管理器自动 遍历设备对象列表并清除该标志。但在WDM驱动程序中,设备对象在DriverEntry返回后才创建,所以I/O管理器不会自动清除这个标志,驱动程序 必须自己清除它。
这是本节的内容。 (speedingboy)
本站文章除注明转载外,均为本站原创或编译欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,同学习共成长。转载请注明:文章转载自:罗索实验室 [http://www.rosoo.net/a/201101/10834.html]
本文出处:CSDN博客 作者:speedingboy
顶一下
(0)
0%
踩一下
(0)
0%
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
栏目列表
将本文分享到微信
织梦二维码生成器
推荐内容