下面的例子说明了内核模式的驱动程序怎样初始化它自己。
这个最小的驱动程序必须被手动加载,它没有访问任何的硬件,但是它创建了一个名字是MINIMAL0的内部设备名和一个符号连接名(MIN1),它们都在一个简单的C语言文件Driver.cpp中,它的头文件Driver.h声明了非硬件的驱动程序指定的信息,例如DEVICE_EXTENSION。
DriverEntry例程
在这个例子中,DriverEntry例程比较小而且简单,它的工作包括:
1. 宣布其它例程的入口点。对于这个例子只有Unload例程。
2. 创建一个逻辑设备。对于测试的目的,仅仅创建一个简单的设备。
NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
{ ULONG ulDeviceNumber = 0;
NTSTATUS status;
// 如果这个驱动过程控制一个硬件设备,应该将定位硬件的代码放到这儿
// 可以调用IoReportResourceUsage,端口,IRQs和DMA信道会被标记上使用中,它们就
// 在这个驱动程序的控制之下了。
// 宣布其它例序的入口点
pDriverObject->DriverUnload = DriverUnload;
// 随着时间的流逝,MajorFunction队列将会被充满
// 创建一个新的设备对象
status = CreateDevice(pDriverObject, ulDeviceNumber);
return status;
}
CreateDevice例程
真正创建设备对象的工作是在CreateDevice例程中完成,虽然这个例程没有做很多的工作,模块化这个工作是合适的,它的任务包括:
1. 选择和组成一个内部设备名。
2. 创建这个内部的设备名。调用IoCreateDevice的时候要指定DEVICE_EXTENSION的大小。
3. 初始化DEVICE_EXTENSION。在这个最小的驱动程序中,DEVICE_EXTENSION保存一个指向设备对象后指针和内部设备名和符号连接名。
4. 构造一个符号连接名和连接这个名字。
// CreateDevice添加一个新的设备
// pDriverObject - I/O管理器传递这个参数
// ulDeviceNumber - 逻辑设备号码(以0为基础)
NTSTATUS CreateDevice ( IN PDRIVER_OBJECT pDriverObject,
IN ULONG ulDeviceNumber )
{ NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
// 构造内部设备名
CUString devName("//Device//MINIMAL"); // 对于"minimal"设备
devName += CUString(ulDeviceNumber);
// 现在创建设备
status = IoCreateDevice( pDriverObject, sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName, FILE_DEVICE_UNKNOWN,
0, TRUE, &pDevObj );
if (!NT_SUCCESS(status)) return status;
// 初始化设备Extension
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj; //后指针
pDevExt->DeviceNumber = ulDeviceNumber;
pDevExt->ustrDeviceName = devName;
CUString symLinkName("//??//MIN"); // 构造符号连接名
symLinkName += CUString(ulDeviceNumber+1); // 以1 为基础
pDevExt->ustrSymLinkName = symLinkName;
//创建符号连接名
status = IoCreateSymbolicLink( &(UNICODE_STRING)symLinkName,
&(UNICODE_STRING)devName );
if (!NT_SUCCESS(status))
{ IoDeleteDevice( pDevObj );
return status; }
return STATUS_SUCCESS; // Made it
}
CreateDevice使用了一个C++类CUString,使用这个类使Unicode字符串添加到设备名简单化,这个类会在以后讨论。
Reinitialize例程
中间层的驱动程序在系统导入的时候可能需要延迟它们的初始化直到低层的驱动程序完成加载。如果所有驱动程序都属于开发者,在安装的时候设置不同的Registry项来确定驱动程序的加载顺序。如果驱动程序是被不同的制造商提供的话,这时加载顺序是不确定的,这个中间驱动程序必须注册和执行Reinitialize例程。
Execution上下文
如果DriverEntry例程发现它因为系统加载的资源不够而不能完成它的初始化的话,它可以声明一个在引导过程中的稍后一个时间执行的Reinitialize例程,Reinitialize例程运行在PASSIVE_ LEVEL的IRQL,它只能访问分页的系统资源。Reinitialize例程对于在系统引导的时候自动的驱动程序加载是非常有用的。
Reinitialize例程的作用
Reinitialize例程执行任何DriverEntry例程不能完成的驱动程序初始化工作。如果Reinitialize例程发现环境仍然不适合,它会再次调用IoRegisterDriverReinitialization函数去注册它自己。
参数(IRQL == PASSIVE_LEVEL) |
意义 |
IN PDRIVER_OBJECT pDriverObject |
指向驱动程序对象的指针 |
IN PVOID Context |
在注册表指定的上下文块 |
IN ULONG Count |
基于0的计数 |
表6.3 Reinitialize例程的参数(没有返回值)
Unicode例程
一般情况下,一旦驱动程序被加载,它将一直保持在系统中直到系统重新激活。Unicode例程使驱动程可以被卸载。在DriverEntry例程中声明Unicode例程。I/O管理器将在驱动程序被自动或者手动卸载的时候调用Unicode例程。
执行的上下文环境
I/O管理器在驱动程序被移出存储器之前调用Unload例程,Unload例程还是PASSIVE_LEVEL的IRQL,它只能访问分页的系统资源。
Unload例程的作用
虽然各个驱动程序的Unload例程不尽相同,但是它大致执行下列工作:
1. 1. 对于一些硬件,设备的状态应该存储在注册表中。在下次DriverEntry例程执行的时候驱动程序可以恢复到最近的状态。例如,声卡驱动程序可能存储当前它的音量设置信息。
2. 2. 如果这个设备支持中断,Unload例程必须禁止它们和断开它们与中断对象的连接。一旦中断对象被删除,设备将不会产生任何中断请求。
3. 释放属于驱动程序的任何硬件。
4. 从Win32的名字空间移除符号连接名。这个动作可以调用IoDeleteSymbolicLink来实现。
5. 使用IoDeleteDevice移除设备对象。
6. 如果管理多部件的控制器,为每一个连接到控制器的设备重复步骤4和5,然后移除控制器对象它自己,使用IoDeleteController函数。
7. 重复步骤4到6,移除所有的术语这个驱动程序的控制器和设备。
8. 释放驱动程序持有的任何缓冲池。
对于WDM驱动程序,这些任务在RemoveDevice例程中执行。
注意,一个驱动程序的Unload例程不是在系统被关闭的时候被调用。在系统被关闭的时候任何特殊的工作在特殊的shutdown例程中执行。
驱动程序卸载实例
在最小的驱动程序中,Unload例程执行取消DriverEntry例程中执行的操作,它必须删除每一个符号连接名和创建的设备对象,Unload例程依赖驱动程序对象的连接列表。
驱动程序对象的DeviceObject域指向驱动过程控制的第一个设备,每个设备通过NextDevice域指向下一个设备。记得一个DEVICE_EXTENSION结构里有一个指向符设备对象的后指针。
// DriverUnload pDriverObject - I/O管理器传递的参数
VOID DriverUnload ( IN PDRIVER_OBJECT pDriverObject )
{ PDEVICE_OBJECT pNextObj;
// 循环每一个驱动过程控制的设备
pNextObj = pDriverObject->DeviceObject;
while (pNextObj != NULL)
{
//从设备对象中取出设备Extension
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
extObj->DeviceExtension;
// 取出符号连接名
UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
IoDeleteSymbolicLink(&pLinkName); // 删除符号连接名
//一个问题...我们需要删除设备对象,但是设备对象是被pNextObj存储着的,如果我们先删
//除这个设备对象,我们将不能在列表中到达下一个设备,除非创建另一个指针,可以使用设备
//Extension的后指针。所以先刷新下一个指针,这样会简单些...
pNextObj = pNextObj->NextDevice;
// 使用设备Extension 删除设备
IoDeleteDevice( pDevExt->pDevice );
} //最后, 如果在DriverEntry 例程中分配有硬件,可以使用IoReportResourceUsage
//函数在这儿进行释放
}
Shutdown例程
如果驱动程序在操作系统消失之前有特殊的工作要做,这就需要一个Shutdown例程。
执行的上下文环境
在系统关闭的时候I/O管理器调用Shutdown例程,它的IRQL是PASSIVE_LEVEL,所以它也是可以访问分页的系统资源。
参数 |
意义 |
IN PRDRIVE_OBJECT pDriverObject |
指向驱动程序对象的指针 |
IN PIRP pIrp |
指向shutdown例程的IRP |
Return value |
STATUS_SUCCESS或者STATUS_XXX |
表6.5 Shutdown例程的参数
Shutdown例程的作用
Shutdown例程的主要目的是将设备设置为静止的状态,存储一些设备信息到系统的注册表。
于驱动程序的Unload例程不同的是,Shutdown例程不必释放驱动程序资源,因为操作系统很快就要关闭了。
启用关闭通知
在驱动程序对象中没有直接的宣布Shutdown例程的地方,系统关闭事件是I/O管理器对驱动程序的请求。需要在驱动程序对象的MajorFunction列表中纪录Shutdown例程的入口点。
使用IoRegisterShutdownNotification函数通知I/O管理器一个设备有兴趣接收Shutdown通知消息,下列代码片段演示如何通知I/O管理器:
NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
{ :
pDriverObject->MajorFunction[ IRP_MJ_SHUTDOWN ] = Shutdown;
IoRegisterShutdownNotification( pDriverObject );
: }
测试驱动程序
甚至是无用的最小的驱动程序,也有好几种方法进行测试,可以测试以下几个方面:
1. 是否编译连接成功。
2. 是否在加载和卸载的时候毁坏系统。
3. 是否创建设备对象和Win32符号连接成功。
4. 是否在unload的时候释放所有它占用的资源。
这些目标不是非常高,但它是购建剩下的有用的驱动程序的一个重要的里程碑。
Win2000 DDK
Windows 2000设备驱动开发包必须被安装在开发的系统中,通常,它安装在一个名字是NTDDK的活页夹下面,DDK允许两种不同的编译环境,checked或者free,checked编译环境像Visual C++的Debug编译环境一样,而free编译环境则像Release编译环境一样。这两个不同编译环境有不同的库的设置,Libchk和Libfre,最初的驱动程序最好使用checked编译环境执行。
创建驱动程序的结果
一旦驱动程序成功的编译和连结后,就创建了一个*.SYS的文件,这个例子是Minimal.SYS。这个文件必须在它被加载之前被复制到适当的路径,一般是/WINNT/System32/Drivers,WINNT这个部分取决于安装WIN2000时选择的名字。
手动安装内核模式的驱动程序
要安装一个新的设备驱动程序,仅仅拷贝驱动程序文件到目标目录是不够的,对于非WDM的驱动程序,适当的标识必须在驱动程序加载之前放入注册表,
注册表的HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services路径下保持了一个可用的设备驱动程序列表,每个驱动程序都有一个独一无二的子键,它的名字必须于驱动程序*.SYS的名字相同,只是没有.SYS的扩展名。
对于每个驱动程序子键,必须提供三个DWORD值: ErrorControl,Start和Type,这些标识描述了当驱动程序被加载(例如,系统导入或者需要使用)的时候,它怎样汇报错误(例如,MsgBox),和一个非常一般的驱动程序类型(内核模式或者文件系统)的描述。对于最小的驱动程序这些设置可能是:
ErrorControl = 1
Start = 3
Type = 1
随意地,必须设置一个DisplayName名字,当使用设备管理器的时候,这个名字显示在计算器的控制面板窗口。
一个文件Minimal.reg自动的设置这个子键和值,仅仅只需要在Windows Explorer中双击文件名就可以了,添加之后重启Windows 2000。
加载驱动程序
一旦安装到系统中,设备的注册键的Start值可以设定系统引导的时候自动加载或者不加载驱动程序,如果Start值是3,意思是需要加载。在控制面板可以开始和停止驱动程序。
服务控制管理器(SCM)处理了大部分的动态的加载设备驱动程序的工作,SCM管理大多数的运行在Win2000下的软件组件。例如,它可以开始,停止和控制服务。一个服务是一个运行在用户模式的上下文环境之外的程序。
服务控制管理器处理了大部分的设备驱动程序动态加载的工作,它管理运行在Win2000下的大部分的软件组件。例如,它可以开始,停止和控制这些服务。一个服务是运行在笨拙的用户的上下文环境之外的程序,计算机管理控制台为加载设备的驱动程序提供有用的信息和一个手动的加载和卸载内核模式驱动程序的机制。
WIN2000计算机管理控制台
一旦这个最小的驱动程序被成功的创建和拷贝到驱动程序路径中,计算器管理控制台可以检测到它的出现。打开设备管理器,它会显示系统中现在安装的设备列表,这个最小的驱动程序将在里面。通过双击驱动程序图标,选择常规标 签,可以选择开始和停止驱动程序。
系统工具中的系统信息工具也可以显示驱动程序的信息。
WINOBJ实用工具
Win32平台的SDK提供了一个实用的工具WINOBJ,这个工具允许用户显示Win2000对象管理器的内部名字空间,对于设备驱动程序的作者,它是非常有用的。
WINOBJ有很多版本,更新的版本请到http://www.sysinternals.com下载。
图6.4. WINOBJ的接口
当使用合适版本的WINOBJ我们的最小的驱动程序Minimal0应该显示在Device路径下面,名字Min1则在??路径下面,驱动程序名字Minimal在Driver路径下面。当驱动程序停止的时候,Minimal0和Min1应该消失。不要忘记刷新显示的示图。
小结
我们完成了Win2000设备驱动程序的初始化工作,这个Minimal驱动程序只是内核模式的驱动程序,以后会介绍怎样将它转变成为WDM驱动程序。
下一章会讨论有使用价值的读,写和I/O控制派遣例程。