本章重点介绍驱动程序中的处理IRP请求的派遣函数。所有对设备的操作最终将转化为IRP请求,这些IRP请求会被传递到派遣函数处理。本章主要介绍了IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_DEVICE_CONTROL的派遣函数。这些IRP请求分别有缓冲区方式、直接方式和其他方式的操作。缓冲区方式和直接方式在驱动程序开发过程中经常用到。
派遣函数主要功能是负责处理I/O请求,其中大部分I/O请求是在派遣函数中处理的。
用户模式下所有对驱动程序的IO请求,全部由操作系统转化为一个叫做IRP的数据结构,不同的IRP数据会被“派遣”到不同的派遣函数(Dispatch Function)中,这也是派遣函数名字的由来。
IRP与派遣函数
IRPde处理机制类似与Windows应用程序的“消息处理”机制,驱动程序接收到不同类型IRP后,会进入不同的派遣函数,在派遣函数中IRP得到处理。
IRP(I/O Request Package)输入输出请求包。应用程序发出IO请求,操作系统将IO请求转化为相应的IRP数据,不同类型的IRP会根据类型传递到不同的派遣函数内。
IRP两个基本属性:一个是MajorFunction,另一个是MinorFunction,分别记录IRP的主类型和子类型,操作系统根据MajorFunction就爱那个IRP派遣到不同的派遣函数中,在派遣函数中还可以继续根据这个IRP属于那种MinorFunction。一般的NT式驱动程序和WDM驱动程序都是在DriverEntry函数中注册派遣函数的。
在DriverEntry传入的参数pDriverObject中,有一个函数指针数组MajorFunction,通过设置这个数组,可以将IRP的类型和派遣函数关联起来,当然在进入DriverEntry之前,操作系统会将_IopInvalidDeviceRequest的地址填满整个数组。所以未设定的仍是这个默认值。
IRP类型
IRP的基本原理类似于Windows应用程序中“消息”的概念。在Win32编程中,程序是由“消息”驱动的。不同的消息,会被分发到不同的消息处理函数。如果没有对应的处理函数,它会进入系统默认的消息处理函数中。
IRP的处理类似这种方式。文件I/O的相关函数,如CreateFile、ReadFile等函数使操作系统产生IRP_MJ_CREATE、IRP_MJ_READ等不同的IRP,这些IRP被传递到驱动程序的派遣函数中。
另外,内核中的文件I/O处理函数,如ZwCreateFile、ZwReadFile等同样会创建相应的IRP_MJ_CREATE、IRP_MJ_READ等IRP,并将IRP传递到相应驱动的相应派遣函数中。
下图列出IRP类型,并对器产生的来源做了说明
对派遣函数的简单处理
大部分IRP都源于文件IO处理Win32 API,如CreateFile、ReadFile等。最简单的处理方法就是在相应的派遣函数中,将IRP的状态设置为成功,然后结束IRP请求,并让派遣函数返回成功。下面代码是最简单的处理IRP请求的派遣函数。
除了设置完成状态,还要设置这个IRP请求操作了多少字节,这个例子中简单的设为0。最后通过IoCompleteRequest函数完成。
这个函数的具体声明是:
VOID IoCompleteRequest( IN PIRP Irp, // 代表需要被结束的IRP。
IN CCHAR PriorityBoost); // 代表线程恢复时的优先级。
为了解释优先级,需要了解一下I/O相关的Win32 API的内部操作过程。(以ReadFile为例)。
ReadFile调用ntdll中的NtReadFile。其中ReadFile函数是Win32 API,而NtReadFile函数是Native API。
ntdll中的NtReadFile进入到内核模式,调用系统服务中的NtReadFile函数。
系统服务函数NtReadFile创建IRP_MJ_WRITE类型的IRP,然后它将这个IRP发送到某个驱动程序的派遣函数中。NtReadFile然后返回去等待一个事件,这时当前进程进入“睡眠”状态,也可以说当前线程被阻塞住或者线程属于“Pending”状态。
在派遣函数中一般会将IRP请求结束,结束IRP是通过IoCompleteRequest函数。在IoCompleteRequest函数内部会设置刚才等待的事件,“睡眠”的线程被恢复运行。
IoCompleteRequest函数的第二个参数PriorityBoost代表一种优先级,指的是被阻塞的线程以何种优先级恢复运行。一般情况下,优先级设置为IO_NO_INCREMENT。对于某些特殊情况,需要将阻塞的线程以“优先”的身份恢复运行。
下图是完成优先级
通过设备链接打开设备
本节介绍利用文件IO相关的为win32 API 对设备进行“打开”和关闭操作。要打开设备,必须通过设备的名字才能的到该设备的句柄。前面介绍过,每个设备都有名称,如HelloDDK驱动程序的设备名为“\Device\MyDDKDevice”,但是设备名无法被用户模式下的应用程序查询到,设备只能被内核模式下的程序查询到。
在应用程序中,设备可以通过符号连接进行访问。驱动程序通过IoCreateSymbolicLink函数创建符号链接。HelloDDK驱动程序的设备所对应的符号链接为 “\??\HelloDDK”。在编写程序时,符号链接的写法需要稍微改一下,将前面的 “\??\”改为 “\\.\” 。因此符号链接 “\??\HelloDDK” 就变成了 “\\.\HelloDDK”。写成C语言的字符串就是 “\\\\.\\HelloDDK”。
下面演示如何利用CreateFile来打开设备句柄,以及如何利用CloseHandle关闭句柄。
#include <windows.h>
#include <stdio.h>
int main()
{
HANDLE hDevice =
CreateFile("\\\\.\\HelloDDK",
GENERIC_READ | GENERIC_WRITE,
0, // share mode none
NULL, // no security
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL ); // no template
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Failed to obtain file handle to device: "
"%s with Win32 error code: %d\n",
"MyWDMDevice", GetLastError() );
return 1;
}
CloseHandle(hDevice);
return 0;
}
编写一个更通用的派遣函数
首先介绍一个重要的数据结构 IO_STACK_LOCATION,即I/O堆栈,这个数据结构和IRP紧密相连。
驱动对象有一个设备栈,IRP会被操作系统发送到设备栈的顶层,如果设备对象的派遣函数没有结束IRP的请求,则会一层一层转发,因此IRP有IO_STACK_LOCATION数组,记录对应设备中所做的操作。对于本层设备对应的IO_STACK_LOCATION,可以通过IoCurrentIrpStackLocation函数得到,如:
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
应用程序可以用Dbgview查看驱动输出的log信息。
跟踪IRP的利器IRPTrace
选择Message | Hook Setup ,然后会弹出一个窗口,选中左边的“Drivers”选项卡,程序会枚举出系统加载的所有驱动程序。
缓冲区方式读写操作
驱动程序所创建的设备一般有三种读写方式,一种是缓冲区方式,一种是直接方式,一种是其他方式。这里先主要介绍缓冲区方式。
缓冲区设备
利用IoCreateDevice创建设备完成后,需要对设备对象的Flags子域进行设置。设置不同的Flags会导致不同的方式操作设备。
读写操作一般是由ReadFile或者WriteFile函数引起的,这里先以WriteFile函数为例进行介绍。WriteFile要求用户提供一段缓冲区,并说明缓冲区的大小,然后WriteFIle将这段内存的数据传入驱动程序中。这段缓冲区是在用户模式下,驱动程序如果直接引用这段内存时十分危险的。解决这个问题的一个办法就是缓冲区方式读写。这种办法操作系统会将应用程序缓冲区的数据复制到内核模式下的地址中。这样,无论操作系统如何切换进程,内核地址不会改变。IRP的派遣函数会对内核模式下的缓冲区操作,而不是操作用户模式地址的缓冲区。这样做的优点是比较简单的解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之间复制数据,影响效率。
缓冲区设备读写
ReadFile或者WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域会记录这段内存地址,当IRP请求结束时,这段内存地址会被复制到ReadFile提供的缓冲区中。
以缓冲区方式无论是“读”还是“写”设备,都会发生用户模式地址与内核模式地址的数据复制。复制的过程由操作系统负责。用户模式地址由ReadFile或者WriteFile提供,内核模式地址由操作系统负责分配和回收。另外在派遣函数中可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile请求多少字节。通过IO_STACK_LOCATION中的Parameters.Write.Length知道WriteFile写多少字节。ReadFile和WriteFile指定操作这么多字节,并不一定操作了这么多字节,在派遣函数中,应该设置IRP的子域IoStatus.Information ,他记录了设备实际操作了多少字节。ReadFile和WriteFile分别从字节的第四个参数得到真正操作了多少字节。
下面演示如何利用缓冲区方式读设备,本例的驱动程序返回给应用程序的数据都是0xAA ,因此应用程序会从设备中读到一连串的0xAAA。
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKRead\n"));
//对一般IRP的简单操作,后面会介绍对IRP更复杂的操作
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
ULONG ulReadLength = stack->Parameters.Read.Length;
// 完成IRP
//设置IRP完成状态
pIrp->IoStatus.Status = status;
//设置IRP操作了多少字节
pIrp->IoStatus.Information = ulReadLength; // bytes xfered
memset(pIrp->AssociatedIrp.SystemBuffer,0xAA,ulReadLength);
//处理IRP
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave HelloDDKRead\n"));
return status;
}
应用程序使用ReadFile进行读写:
int main()
{
HANDLE hDevice =
CreateFile("\\\\.\\HelloDDK",
GENERIC_READ | GENERIC_WRITE,
0, // share mode none
NULL, // no security
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL ); // no template
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Failed to obtain file handle to device: "
"%s with Win32 error code: %d\n",
"MyWDMDevice", GetLastError() );
return 1;
}
UCHAR buffer[10];
ULONG ulRead;
BOOL bRet = ReadFile(hDevice,buffer,10,&ulRead,NULL);
if (bRet)
{
printf("Read %d bytes:",ulRead);
for (int i=0;i<(int)ulRead;i++)
{
printf("%02X ",buffer[i]);
}
printf("\n");
}
CloseHandle(hDevice);
return 0;
}
另外可以通过IRP Trace工具查看设备对应的IO_STACK_LOCATION结构。
缓冲区设备模拟文件读写
NTSTATUS HelloDDWrite(IN PDEVICE_OBJECT PDevObj, IN PIRP pIrp)
{
KdPrint((" Enter HelloDDWrite\n" ));
NTSTATUS status = STATUS_SUCCESS;
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
// 获取存储的长度
ULONG ulWriteLength = stack->Parameters.Write.Length;
// 获取存储偏移量
ULONG ulWriteOffset = (ULONG)stack->Parameters.Write.ByteOffset.QuadPart;
if(ulWriteLength+ulWriteOffset > MAX_FILE_LENGTH)
{
//如果存储长度+偏移量大于缓冲区长度,则返回无效
status = STATUS_FILE_INVALID;
ulWriteLength = 0;
}
else
{
//将写入的数据,存储在缓冲区内
memcpy(pDevExt->buffer+ulWriteOffset,pIrp->AssociatedIrp.SystemBuffer,ulWriteLength);
status = STATUS_SUCCESS;
// 设置新的文件长度
if( ulWriteLength+ulWriteOffset > pDevExt->file_Length)
{
pDevExt->file_length = ulWriteLength+ulWriteOffset;
}
}
pIrp->IoStatus.Status = status; //设置IRP完成状态
pIrp->IoStatus.Information = ulWriteLength; // 实际操作多少字节
IoCompleteRequest( pIrp,IO_NO_INCREMENT ); // 结束IRP请求
KdPrint(("Leave HelloDDWrite\n" ));
return status;
}
此外,还介绍了读文件和查询文件信息的IRP,这里不做详细的介绍。
直接方式读写操作
直接读写设备
创建完对象后,设备对象属性设置为DO_BUFFERED_IO属性。
PDevObj->Flags |= DO_DIRECT_IO;
和缓冲区方式读写设备不同,直接方式读写设备,操作系统会将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍,这样,用户模式的缓冲区和内核模式的缓冲区哪个区指向的是同一区域的物理内存,无论操作系统如何切换进程,内核模式地址都保持不变。
操作系统先将用户模式的地址锁定后,操作系统用内存描述表(MDL数据结构)记录这段内存。MDL记录这段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址是mdl->StartVa,这段虚拟内存的首地址对于第一个页地址的偏移量是mdl->ByteOffset。因此虚拟内存的首地址应该是mdl->StartVa + mdl->ByteOffset。
DDK提供了几个宏方便程序员得到这几个值
#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount)
#define MmGetMdlByteOffset(Mdl) ((Mdl)->ByteOffset)
#Define MmGetMdlVirtualAddress(Mdl) ((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))
IO 设备控制操作
除了读写设备之外,应用程序还可以通过另外一个Win32 API DeviceIoControl操作设备。DeviceIoControl内部会使用操作系统创建一个IRP_MJ_DEVICE_CONTROL类型的IRP,然后操作系统会将这个IRP转发到派遣函数中。程序员可以用这个定义读写之外的其他操作,它可以让应用程序和驱动程序进行通信。例如要对设备进行初始化操作,程序员定义一个IO控制码,然后通过DeviceIoControl将这个控制码和请求一起传递给驱动程序,在派遣函数中分别对不同的IO控制码进行处理。
BOOL DeviceIoControl(
HANDLE hDevice, //已打开的设备
DWORD dwIoControlCode, //控制码
LPVOID lpInBuffer, //输入缓冲区
DWORD nInBufferSize, // 输入缓冲区大小
LPVOID lpOutBuffer, // 输出缓冲区
DWORD nOutBufferSize, // 输出缓冲区大小
LPDWORD lpBytesReturned, // 实际返回字节数 对应派遣函数pIrp->IoStatus.Information
LPOVERLAPPED lpOverlapped //是否overlap操作
)
第二个参数是IOCTL,是一个32位的无符号整数。IOCTL需要符合DDK规定,
DDK特意提供了一个宏CTL_CODE,其定义如下:
CTL_CODE( DeviceType, Function, Method, Access)
DeviceType:设备对象类型,这个类型应和创建设备(IoCreateDevice)时的类型匹配。一般是形如FILE_DEVICE_XX的宏。
Function:这是驱动程序定义的IOCTL码,其中:
0x0000 到 0x7FFF:为微软保留。
0x800 到 0xFFF:由程序员自己定义。
Method :这是操作模式,可以是下列四种模式之一。
METHOD_BUFFERED:使用缓冲区方式操作。
METHOD_IN_DIRECT:使用直接写方式操作。
METHOD_OUT_DIRECT:使用直接读方式操作。
METHOD_NEITHER: 使用其他方式操作
Access:访问权限,如果没有特殊要求,一般使用FILE_ANY_ACCESS。
缓冲内存模式IOCTL
首先用CTL_CODE宏定义 定义IOCTL码
#define IOCTL_TEST1 CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
驱动程序中使用CTL_CODE需要包含NTDDK.h头文件,而在应用程序中需要包含winioctl.h
NTSTATUS HelloDDKDeviceIOControl(IN PEDVICE_OBJECT pDevObj,IN PIRP pIrp)
{
NTSTATUS status = STATUS_SUCCESS;
KdPrint(("Enter HelloDDKDeviceIOControl\n"));
// 得到当前堆栈
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
// 得到输入缓冲区的大小
ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;
// 得到输出缓冲区的大小
ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;
//得到IOCTL码
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG info = 0;
switch(code)
{
case IOCTL_TEST1:
{
KdPrint(("IOCTL_TEST1\n"));
// 缓冲区方式IOCTL
UCHAR *InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
for(ULONG i=0; i<cbin; i++)
{
KdPrint(("%X\n",InputBuffer(i)));
}
// 操作输出缓冲区
UCHAR *OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
memset(OutBuffer,0xAA,cbout);
// 设置实际操作输出缓冲区长度
info = cbout;
break;
}
default:
status = STATUS_INVALID_VARIANT;
}
//设置IRP完成状态
pIrp->IoStatus.Status = status;
// 设置IRP请求操作的字节数
pIrp->IoStatus.Information = info;
IoCompleteRequest( pIrp,IO_NO_INCREMENT);
KdPrint(("Leave HelloDDKDeivceIOControl\n"));
return status;
}