派遣函数是Windows驱动程序中的重要概念。驱动程序的主要功能是负责处理I/O请求,其中大部分I/O请求是在派遣函数中处理的。
用户模式下所有对驱动程序的I/O请求,全部由操作系统转换为一个叫做IRP数据结构,不同的IRP会被“派遣”到不同的派遣函数中。
IRP与派遣函数
IRP的处理机制类似于Windows应用程序中的“消息处理”,驱动程序接收到不同的IRP后,会进入不同的派遣函数,在派遣函数中IRP得到处理。
1.IRP
在Windows内核中,有一种数据结构叫做IRP(I/O Request Package),即输入输出请求包。上层应用程序与底层驱动程序通信时,应用程序会发出I/O请求。操作系统将I/O请求转化为相应的IRP数据,不同类型的IRP会被传递到不同的派遣函数中。
IRP有两个基本的重要属性,一个是MajorFunction,另一个MinorFunction,分别记录IRP的主类型和子类型,操作系统根据MajorFunction将IRP“派遣”到不同的派遣函数中,在派遣函数中还可以继续判断这个IRP属于哪种MinorFunction。
下面是HelloDDK的DriverEntry中关于派遣函数的注册:
#pragma INITCODE extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegisterPath ) { NTSTATUS status; KdPrint(("Enter DriverEntry\n")); //设置卸载函数 pDriverObject->DriverUnload = HelloDDKUnload; //设置派遣函数 pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = HelloDDKDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = HelloDDKDispatchRoutine; //创建驱动设备对象 status = CreateDevice(pDriverObject); KdPrint(("Leave DriverEntry\n")); return status; }
文件I/O的相关函数,如CreateFile,ReadFile,WriteFile,CloseHandle等函数会使操作系统产生出IRP_MJ_CREATE,IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_CLOSE等不同的IRP。另外,内核中的文件I/O处理函数,如ZwCreateFile,ZwReadFile,ZwWriteFile,ZwClose,他们同样会产以上IRP。
一下列出了IRP的类型,并对其产生的来源做了说明
IRP类型 来源
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_CREATE 创建设备,CreateFile会产生此IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_CLOSE 关闭设备,CloseHandle会产生此IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_CLEANUP 清除工作,CloseHandle会产生此IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_DEVICE_CONTROL DeviceControl函数会产生此IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_PNP 即插即用消息,NT驱动不支持次IRP,WDM驱动才支持次IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_POWER 在操作系统处理电源消息时,产生次IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_QUERY_INFORMATION 获取文件长度,GetFileSize会产生IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_READ 读取设备内容,ReadFile会产生此IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_SET_INFORMATION 设置文件长度,GetFileSize会产生IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_SHUTDOWN 关闭系统前会产生此IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_SYSTEM_CONTROL 系统内部产生的控制信息,类似于内核调用DeviceControl函数
-----------------------------------------------------------------------------------------------------------------------------------------------
IRP_MJ_WRITE 对设备进行WriteFile时会产生此IRP
-----------------------------------------------------------------------------------------------------------------------------------------------
3.对派遣函数的简单处理
大部分的IRP都源于文件I/O处理Win32API,处理这些IRP最简单的方法就是在相应的派遣函数中,将IRP状态设置为成功,然后结束IRP的请求,并让派遣函数成功返回。结束IRP的请求使用函数IoCompleteRequest.。下面代码演示了一种最简单的处理IRP请求的派遣函数。
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { KdPrint(("Enter HelloDDKDispatchRoutine\n")); //对一般IRP的简单操作 NTSTATUS status = STATUS_SUCCESS; //设置IRP完成状态 pIrp->IoStatus = status; //设置IRP操作了多少字节 pIrp->IoStatus.Information = 0; //处理IRP IoCompleteRequest(pIrp,IO_NO_INCREMENT); KdPrint(("Leave HelloDDKDispatchRputine")); return status; }
除了设置IRP的完成状态,派遣函数还要设置这个IRP操作了多少字节。
派遣函数将IRP请求结束,这是通过IoCompleteRequest函数完成的。
4.通过设备链接打开设备
要打开设备,必须通过设备名字才能得到该设备的句柄。前面介绍过,每个设备都有设备名称,如HelloDDK驱动程序的设备名称为“\\Device\\MyDDKDevice”,但是设备名称无法被用户模式下的应用程序查询到,设备名只能被内核模式下的程序查询到。在应用程序中需要通过符号链接进行访问。
下面程序演示在用户模式下打开驱动设备:
#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; }
在Windows驱动开发中,有一个重要的内核数据结构,IO_STACK_LOCATION,即I/O堆栈,这个数据结构和IRP紧密相连。
驱动对象会创建一个个设备对象,并将这些设备对象“叠”成一个垂直结构,被称为“设备栈”。IRP会被操作系统发送到设备栈顶层,如果顶层设备结束了本次IRP的请求,则I/O请求结束,如果不让I/O请求结束,可以将IRP继续转发到下一层设备。因此,一个IRP可能会被转发多次。为了记录IRP在每层设备中的操作,IRP会有一个IO_STACK_LOCATION数组,每个IO_STACK_LOCATION元素记录着对应设备中做的操作。对于本层的IO_STACK_LOCATION,可以通过IoGetCurrentIrpStackLocation函数得到。IO_STACK_LOCATION结构中会记录IRP的类型,即IO_STACK_LOCATION中的MajorFuncation子域。
下面代码增加了派遣函数的难度:
#pragma PAGEDCODE NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { KdPrint(("Enter HelloDDKDispatchRoutine\n")); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); //建立一个字符串数组与IRP类型对应起来 static char * irpname[] = { "IRP_MJ_CREATE", "IRP_MJ_CREATE_NAMED_PIPE", "IRP_MJ_CLOSE", "IRP_MJ_READ", "IRP_MJ_WRITE", "IRP_MJ_QUERY_INFORMATION", "IRP_MJ_SET_INFORMATION", "IRP_MJ_QUERY_EA", "IRP_MJ_SET_EA", "IRP_MJ_FLUSH_BUFFERS", "IRP_MJ_QUERY_VOLUME_INFORMATION", "IRP_MJ_SET_VOLUME_INFORMATION", "IRP_MJ_DIRECTORY_CONTROL", "IRP_MJ_FILE_SYSTEM_CONTROL", "IRP_MJ_DEVICE_CONTROL", "IRP_MJ_INTERNAL_DEVICE_CONTROL", "IRP_MJ_SHUTDOWN", "IRP_MJ_LOCK_CONTROL", "IRP_MJ_CLEANUP", "IRP_MJ_CREATE_MAILSLOT", "IRP_MJ_QUERY_SECURITY", "IRP_MJ_SET_SECURITY", "IRP_MJ_POWER", "IRP_MJ_SYSTEM_CONTROL", "IRP_MJ_DEVICE_CHANGE", "IRP_MJ_QUERY_QUOTA", "IRP_MJ_SET_QUOTA", "IRP_MJ_PNP", }; UCHAR type = stack->MajorFunction; if (type >= arraysize(irpname)) { KdPrint(("-Unknow IRP ,major type %X\n",type)); } else { KdPrint(("\t%s\n",irpname[type])); } //对一般IRP的简单操作,后面会介绍对IRP更复杂的操作 NTSTATUS status = STATUS_SUCCESS; //完成IRP pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp,IO_NO_INCREMENT); KdPrint(("Leave HelloDDKDispatchRoutine\n")); return status; }
缓冲区方式读写操作
驱动程序所创建的设备一般会有三种读写方式,一种是缓冲区方式,一种是直接方式,一种是其他方式。
1.缓冲区方式
IOCreateDevice创建完设备后,需要对设备对象的Flags子域进行设置,设置不同的Flags会导致以不同的方式操作设备。
设备对象一共可以有三种读写方式,这三种方式的Flags分别对应为DO_BUFFERED_ID,DO_DIRECT_IO和0,缓冲区方式读写相对简单。
读写操作一般是由ReadFile或者WriteFile函数引起的,这里以WriteFile函数为例进行介绍。WriteFile要求用户提供一段缓冲区,并且说明缓冲区的大小,然后WriteFile将这段内存的数据传入到驱动程序中。
这段缓冲区内存是用户模式的内存地址,驱动程序如果直接引用这段内存是十分危险的。如果以缓冲区方式读写,操作系统会将应该用程序提供缓冲区的数据复制到内核模式下的地址中,这样无论操作系统如何切换进程,内核模式的地址都不回改变。IRP派遣函数真正操作的是内核模式下的缓冲区地址,而不是用户模式下的缓冲区地址。但是这样做会有一定的效率影响。
2.缓冲区设备读写
以缓冲区方式写设备时,操作系统将WriteFile提供的用户模式的缓冲区复制到内核模式地址下,这个地址由WriteFile创建的IRP的AssociateIrp.SystemBuffer子域记录。
另外,在派遣函数中也可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile请求多少字节。通过IO_STACK_LOCATION中的Parameters.Write.Length子域知道WriteFile请求多少字节。
然后,WriteFile和ReadFile指定对设备操作多少字节,并不真正意味着操作了这么多字节。在派遣函数中,应该设置IRP的子域IoStatus.Information.这个子域记录设备实际操作了多少字节。
下面代码演示了如何利用“缓冲区”方式读设备:
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; memset(pIrp->AssociatedIrp.SystemBuffer,0XAA,ulReadLength); //处理IRP IoCompleteRequest(pIrp,IO_NO_INCREMENT); KdPrint(("Leave HelloDDKRead\n")); return status; }
#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; } 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; }
1.直接读取设备:
除了“缓冲区”方式读写设备外,另一种方式是直接方式读写设备。这种方式需要在创建完设备对象后,在设置设备属性的时候,设置为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))
下面结合代码演示如何编写直接方式设备的派遣函数
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { KdPrint(("Enter HelloDDKRead\n")); PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); ULONG ulReadLength = stack->Parameters.Read.Length; KdPrint(("ulReadLength:%d\n",ulReadLength)); ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress); PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress); ULONG mdl_offset = MmGetMdlByteOffset(pIrp->MdlAddress); KdPrint(("mdl_address:0X%08X\n",mdl_address)); KdPrint(("mdl_length:%d\n",mdl_length)); KdPrint(("mdl_offset:%d\n",mdl_offset)); if (mdl_length!=ulReadLength) { //MDL的长度应该和读长度相等,否则该操作应该设为不成功 pIrp->IoStatus.Information = 0; status = STATUS_UNSUCCESSFUL; }else { //用MmGetSystemAddressForMdlSafe得到MDL在内核模式下的映射 PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority); KdPrint(("kernel_address:0X%08X\n",kernel_address)); memset(kernel_address,0XAA,ulReadLength); pIrp->IoStatus.Information = ulReadLength; // bytes xfered } pIrp->IoStatus.Status = status; IoCompleteRequest( pIrp, IO_NO_INCREMENT ); KdPrint(("Leave HelloDDKRead\n")); return status; }
其他方式的读写操作:
这里暂时不讨论此种方法。