关键成功因素分析:WDM驱动开发之路(2)

来源:百度文库 编辑:中财网 时间:2024/04/30 02:35:30
WDM驱动开发之路(2)


WDM驱动开发之路(二)
上一篇中我们已经了解了WDM的一般知识,知道了Windows操作系统和WDM驱动的体系结构。
本篇我们介绍开发之前的准备工作,包括开发环境准备、预备知识。
开发环境准备
对于开发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例程的示例:
extern "C" 
NTSTATUS  DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
  DriverObject->DriverUnload = DriverUnload;                            <--1
  DriverObject->DriverExtension->AddDevice = AddDevice;
  DriverObject->DriverStartIo = StartIo;
  DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp; //设置各个IRP的处理函数    ,这三个IRP是每一个WDM驱动程序必须处理的。                
  DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;
  DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = DispatchWmi;
  ...                                                <--3
  servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool, RegistryPath->Length + sizeof(WCHAR));    <--4
  if (!servkey.Buffer)
    return STATUS_INSUFFICIENT_RESOURCES;
  servkey.MaximumLength = RegistryPath->Length + sizeof(WCHAR);
  RtlCopyUnicodeString(&servkey, RegistryPath);
  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函数创建设备对象,例如:
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) 是设备类型。这个值可以被设备硬件键(注册表中包含该硬件信息的键值)或类键(注册表中包含该类驱动信息的键值)中的可替换值(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指针。然后我们进行到下一步,初始化设备扩展,做与创建新设备对象相关的其它工作,如果在这之后又发现了错误,那么在返回前应先释放刚创建的设备对象并返回状态码。见下面例子代码:
NTSTATUS status = IoCreateDevice(...);
if (!NT_SUCCESS(status))
  return status;
...
if ()
{
  IoDeleteDevice(fdo);
  return status;
}
为设备命名
Windows 使用对象管理器集中管理系统中的大量的内部数据结构(每个对象在系统中都表现为一个数据结构),包括驱动程序对象和设备对象。为了便于区别,每个对象都有名称,对象管理器用一个层次化的命名空间来管理这些名称。图中是DevView(一个设备观察工具,在驱动开发网站)显示的顶层对象名。此工具以文件夹形式显示的对象是目录对象,它可以包含子目录或常规对象,其它图标则代表正常对象。
通常设备对象都把自己的名字放到\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值。
硬件键 
硬件键包含单个设备的信息。
未完待续
 (本文参考《Programming the Windows Driver Model》中译本一书,并将相关术语作了恰当的更改。)