算起来,决定学习WDM也有一年时间了。不过,由于研一紧张的课程,以及研一下期教研室横向的压力,我也很少有时间能静下心来看看书看看代码。可以说是连三天打鱼两天晒网都不如。过了大半年,Walter Oney的《Windows Driver Model》好不容易看了5章,后面的章节零零星星看了一点,就开始迫不及待的投入实践了。实际上再不投入实践我都不知道要学到什么时候去了,因为以后只会越来越忙。于是,我写了我的第一个驱动程序,非常简单,就相当于我们刚学C语言时学的那个“Hello World”程序。程序虽然简单,但是通过这个过程,我对前面看书所学到的东西有了一个更深入更直观的理解,很多以前模糊的概念通过这次实践现在变得清晰了。此外,通过写这个程序,我总算是熟悉了WDM程序的总体构架以及编程过程,因此,这个程序也算是一个敲门砖吧。好了,废话就这么多,下面进入正题。
WDM,即Windows Driver Model,是Windows环境下开发驱动程序的有力工具。初学WMD的时候觉得这玩意很有些难,现在虽然不能说完全学会了,但总算是入了那个门槛了吧,回想起来,觉得难的原因主要是对windows系统了解还是太少。所以,学习WDM首先就要对系统结构有一个比较深入的了解,起码要了解系统是怎样从用户应用程序一步步通过驱动程序调用到硬件设备的。下面,给出一个WDM一书中描述windows 2000系统结构的一个截图:
在这个图中,“设备驱动程序”那一块,就是我们的驱动程序所处的位置。只了解了这一个结构还不够,我们还必须了解驱动程序之间的结构,了解它们之间是怎么交互的。驱动程序是一个分层的结构,一个硬件设备并不是只由一个驱动程序来管理,在它相关联的物理设备驱动程序之上,还有很多过滤驱动程序。与这些过滤驱动程序相关联的,就是这个物理设备对象的过滤器设备对象。那么,一个用户模式的请求,必须通过上层的过滤器设备对象,一层一层的往下传,最终才能到达物理设备对象。这有点像TCP/IP分层结构模型,一个应用层的数据包必须通过传输层、网络层这样一层一层的往下传,最终才能达到物理层并传递到网络中。而设计这样的分层模型的目的,我想应该是为了方便扩展,比如如果想对某个设备加入新的管理操作,那么不需要修改其已有的物理设备驱动程序和过滤器驱动程序,而只需要加入新的过滤器设备对象以及相应的驱动程序,在这里加入新的操作就行了。下面,还是用一个图来表示这种分层的结构模型:
这个图左边的PDO、FDO、FiDO等,就是指设备对象。而IRP——IO请求包,就是上一层设备对象向下一层设备对象发送的请求,也就是它们之间交互的信息。
另外,需要指出的一点是,在很多内核模式编程中,驱动程序并不一定要与某一个实际存在的物理设备相关联,它可以仅创建一个虚拟的设备对象,而这个设备对象不与任何实际的物理设备相关联。因为在很多情况下,用户编写驱动的目的仅仅是要让自己的代码执行在系统的内核态中。
有了前面的这些必备知识,下面就要看看到底应该怎样编写WDM驱动程序了。
同我们学习c语言的时候一样,首先要从程序入口点开始。在驱动程序中,这个入口点就是DriverEntry函数(相当于c语言的main函数),它在驱动程序被加载进内存的时候调用。DriverEntry函数有两个参数,其中第一个参数PDRIVER_OBJECT pDriverObj是指向该驱动程序对应的驱动程序对象的指针。在DriverEntry函数中,一个重要的任务就是要设定驱动程序对象的几个函数指针,这样,该驱动程序对象关联的设备对象在接收到上层的IRP的时候,就会通过驱动程序对象中设置的函数指针,找到相应的函数来做处理:
pDriverObj->DriverUnload = DriverUnload;
pDriverObj->MajorFunction[IRP_MJ_CREATE] =
pDriverObj->MajorFunction[IRP_MJ_CLOSE] =
pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDispatch;
除此之外,DriverEntry函数还有一个重要的任务,就是要创建设备对象并为其建立符号连接。(这里说明一下,在规范的WDM程序中,创建设备对象的任务本来该由AddDevice函数来做,而这个函数也是通过驱动程序对象的一个函数指针来定位的。在这种规范的WDM程序中,一旦有新硬件加入,系统就会自动通过驱动程序对象的函数指针找到AddDevice函数,并调用它来创建设备对象。但是在这里,我并不是在为实际存在的硬件写驱动,而只是写一个内核模式下的程序,因此就只需要在DriverEntry函数中创建一个设备对象就行了。)
IoCreateDevice( pDriverObj,
0,
&deviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
true,
&pDeviceObj ); //创建设备对象
IoCreateSymbolicLink( &linkName, &deviceName );
//建立符号连接
从上面调用IoCreateDevice函数的参数中还可以看出,设备对象和驱动程序对象是相关联的,这也就可以解释为什么是设备接收到IRP,而相应的处理函数却是由驱动程序对象中的函数指针定位的。至于建立符号连接,那是为了方便在用户模式中调用设备对象。
从前面设定的驱动程序对象中的函数指针可以看到,主要有两个函数:卸载函数DriverUnload和派遣函数DriverDispatch。DriverUnload函数应该很容易理解,它是在驱动程序被卸载出内存的时候调用,主要做一些释放内存之类的工作。而在我的这个程序中,所有的IRP都是在同一个函数里面进行处理的,这就是派遣函数DriverDispatch(实际上很多WDM程序都是这样做的)。下面就分别介绍一下这两个函数。
DriverUnload函数的主要任务是将创建的设备对象和符号连接删除掉,当然如果在程序中还分配了其他内存需要释放,也是在这里完成。
IoDeleteSymbolicLink( &linkName );
IoDeleteDevice( pDriverObj->DeviceObject );
派遣函数DriverDispatch主要负责处理上层的IRP。这里先要提一下,每个IRP都与两个数据结构相关联,就是IRP本身和IRP Stack——IO_STACK_LOCATION结构。在这两个结构里面,包含了所有上层传递给本层设备对象的信息。最重要的一个信息就是:在IO_STACK_LOCATION结构中,包含了IRP的功能码MajorFunction和MinorFunction(IRP的功能码标识了该IRP具体是什么请求,比如读请求的MajorFunction值为IRP_MJ_READ)。
DriverDispatch函数的处理流程一般是这样的:首先通过IRP获得IPR Stack;然后从IRP Stack中得到该IRP的主功能码MajorFunction,判断主功能码并做相应处理;处理完该请求后,根据具体情况选择完成该请求或者向下一层设备对象传递该IRP。获得IRP Stack很简单,只需要调用函数IoGetCurrentIrpStackLocation即可:
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
判断主功能码并做相应的处理这一步一般是由一个switch-case语句实现的:
switch( pIrpStack->MajorFunction )
{
case IRP_MJ_CREATE:
DbgPrint( "Info: Create!/n" );
break;
case IRP_MJ_CLOSE:
DbgPrint( "Info: Close!/n" );
break;
case IRP_MJ_DEVICE_CONTROL:
{
switch( pIrpStack->Parameters.DeviceIoControl.IoControlCode )
{
case IOCTL_GET_INFO:
{
RtlCopyMemory( pIrp->UserBuffer, "This is a test driver!", 23 );
information = 23;
break;
}
default:
break;
}
}
default:
break;
}
最后一步,如果需要完成该请求,那么应该先设置IRP结构中的IoStatus域,然后调用函数IoCompleteRequest:
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = information;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
如果需要向下一层设备对象传递该IRP,则要先初始化往下传递的IRP对应IRP Stack(可以直接将当前的IRP Stack复制给下层IRP Stack),然后调用IoCallDriver函数往下层传递该IRP:
IoCopyCurrentIrpStackLocationToNext(pIrp);
status = IoCallDriver(pLowerDeviceObj, pIrp);
以上就是对我写的这个驱动试验程序的简单分析,也是我对WDM驱动程序框架结构的一个初步理解。由于我只是初学WDM,因此一定有很多理解错误或者遗漏的地方。
下面,我再简单介绍一下怎样从用户模式的程序中调用驱动。用户模式的程序要调用驱动,首先就要打开设备,也就是驱动程序中创建的设备对象。这可以通过调用CreateFile函数来实现。CreateFile函数本来是用于打开文件,它的第一个参数就是文件名。而这里,我们以设备名作为它的第一个参数传入,那么该函数打开的就是设备了。这里所说的设备名,实际上是驱动程序里面为设备对象建立的符号连接名。比如用户模式中给出的设备名为”
//./MyDevice”,I/O管理器在执行名称搜索前先自动把”//./”转换成”/??/”,这样就成了”
/??/MyDevice”,这就是驱动程序里面建立的符号连接名了。打开设备后,用户模式的程序就可以调用ReadFile、WriteFile和DeviceIoControl等函数向驱动程序发出请求了。
最后,给出我的试验程序的源码。
//Driver部分:
#ifdef __cplusplus
extern "C" {
#endif
#include "ntddk.h"
#define DEVICE_NAME L"//Device//MyDevice"
#define LINK_NAME L"//??//MyDevice"
#define IOCTL_GET_INFO /
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
void DriverUnload( PDRIVER_OBJECT pDriverObj );
NTSTATUS DriverDispatch( PDEVICE_OBJECT pDeviceObj, PIRP pIrp );
// 驱动程序加载时调用DriverEntry例程:
NTSTATUS DriverEntry( PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegistryString )
{
DbgPrint( "DriverEntry!/n" );
NTSTATUS status;
PDEVICE_OBJECT pDeviceObj;
UNICODE_STRING deviceName;
RtlInitUnicodeString( &deviceName, DEVICE_NAME );
status = IoCreateDevice( pDriverObj, 0, &deviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, true, &pDeviceObj );
if( !NT_SUCCESS( status ) )
{
DbgPrint( "Error: Create device failed!/n" );
return status;
}
UNICODE_STRING linkName;
RtlInitUnicodeString( &linkName, LINK_NAME );
status = IoCreateSymbolicLink( &linkName, &deviceName );
if( !NT_SUCCESS( status ) )
{
DbgPrint( "Error: Create symbolic link failed!/n" );
IoDeleteDevice( pDeviceObj );
return status;
}
pDriverObj->DriverUnload = DriverUnload;
pDriverObj->MajorFunction[IRP_MJ_CREATE] =
pDriverObj->MajorFunction[IRP_MJ_CLOSE] =
pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDispatch;
return STATUS_SUCCESS;
}
void DriverUnload( PDRIVER_OBJECT pDriverObj )
{
DbgPrint( "DriverUnload!/n" );
if( pDriverObj->DeviceObject != NULL )
{
UNICODE_STRING linkName;
RtlInitUnicodeString( &linkName, LINK_NAME );
IoDeleteSymbolicLink( &linkName );
IoDeleteDevice( pDriverObj->DeviceObject );
}
return;
}
NTSTATUS DriverDispatch( PDEVICE_OBJECT pDeviceObj, PIRP pIrp )
{
ULONG information = 0;
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
switch( pIrpStack->MajorFunction )
{
case IRP_MJ_CREATE:
DbgPrint( "Info: Create!/n" );
break;
case IRP_MJ_CLOSE:
DbgPrint( "Info: Close!/n" );
break;
case IRP_MJ_DEVICE_CONTROL:
{
switch( pIrpStack->Parameters.DeviceIoControl.IoControlCode )
{
case IOCTL_GET_INFO:
{
RtlCopyMemory( pIrp->UserBuffer, "This is a test driver!", 23 );
information = 23;
break;
}
default:
break;
}
}
default:
break;
}
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = information;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
#ifdef __cplusplus
}
#endif
//用户模式程序部分:
#include "stdafx.h"
#include "stdio.h"
#include "windows.h"
#define DEVICE_NAME ".//MyDevice"
#define CTL_CODE( DeviceType, Function, Method, Access ) (
/
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) /
)
#define FILE_DEVICE_UNKNOWN
0x00000022
#define METHOD_NEITHER
3
#define FILE_ANY_ACCESS
0
#define IOCTL_GET_INFO /
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
int main(int argc, char* argv[])
{
HANDLE hDevice = CreateFile( DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if( hDevice == INVALID_HANDLE_VALUE )
{
printf( "Error: Can't open the device!/n" );
return -1;
}
unsigned long numOfBytesReturned;
char info[32] = {0};
if( DeviceIoControl( hDevice, IOCTL_GET_INFO, NULL, 0, info, 32, &numOfBytesReturned,
NULL ) == true )
printf( "Information: %s /n", info );
CloseHandle( hDevice );
Sleep( 3000 );
return 0;
}