除了ReadFile和WriteFile外,还有一个函数可以让用户模式程序访问内核模式驱动:DeviceIoControl。这是经常用的一个API, 原型如下:
BOOL WINAPI DeviceIoControl( _In_ HANDLE hDevice,//已经打开的设备 _In_ DWORD dwIoControlCode,控制码 _In_opt_ LPVOID lpInBuffer,//输入缓冲区 _In_ DWORD nInBufferSize,//输入缓冲区大小 _Out_opt_ LPVOID lpOutBuffer,//输出缓冲区 _In_ DWORD nOutBufferSize,//输出缓冲区大小 _Out_opt_ LPDWORD lpBytesReturned,//返回驱动操作的字节数 _Inout_opt_ LPOVERLAPPED lpOverlapped//用于Overlapped操作 );
其他参数都好理解,除了控制码需要讲一下。MSDN解释(http://msdn.microsoft.com/en-us/library/windows/hardware/ff543023(v=vs.85).aspx)。
控制码
其实控制码只是一个32位长度的DWORD,只是这32位内容里面不同的位对应了不同的意思,见上图。DDK提供了一个宏来生成这个控制码,
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \ ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \ )
DeviceType:对应创建FDO(IoCreateDevice)时候指定的设备类型。
Function:是控制码的一个标志,其中0 - 0x7FFF已经保留给微软用了,我们可以使用>=0x8000的值。比如0x8000表示驱动加密功能,0x8001表示驱动解密功能。
TransferType:定义系统如何在驱动和用户模式的caller之间传递数据。就像ReadFile,WriteFile的内存操作一样。创建设备对象的时候,DEVICE_OBJECT->Flags需要指定一个内存操作方式,比如DO_BUFFERED_O或者DO_DIRECT_IO。注意DEVICE_OBJECT->Flags指定的内存方式只对ReadFile和WriteFile起作用。DeviceIoControl需要在控制码里面指定,也就是这里的TransferType。这里有三种方式:METHOD_BUFFERED,METHOD_IN_DIRECT or METHOD_OUT_DIRECT和METHOD_NEITHER(参见:http://msdn.microsoft.com/en-us/library/windows/hardware/ff543023(v=vs.85).aspx)。METHOD_BUFFERED比较简单,就是输入输出都是用缓冲区方式。METHOD_IN_DIRECT/METHOD_OUT_DIRECT比较让人困惑。首先对于METHOD_IN_DIRECT/METHOD_OUT_DIRECT,输入都是使用的缓冲区方式(通过Irp->AssociatedIrp.SystemBuffer获得内核模式下的缓冲),困惑在于输出缓冲(METHOD_IN_DIRECT/METHOD_OUT_DIRECT只对输出缓冲起作用)。MSDN上面是这么解释的(http://msdn.microsoft.com/en-us/library/windows/hardware/ff540663(v=vs.85).aspx):
For these transfer types, IRPs supply a pointer to a buffer atIrp->AssociatedIrp.SystemBuffer. This represents the input buffer that is specified in calls toDeviceIoControl andIoBuildDeviceIoControlRequest. The buffer size is specified byParameters.DeviceIoControl.InputBufferLength in the driver'sIO_STACK_LOCATION structure.
For these transfer types, IRPs also supply a pointer to an MDL atIrp->MdlAddress. This represents the output buffer that is specified in calls toDeviceIoControl andIoBuildDeviceIoControlRequest. However, this buffer can actually be used as either an input buffer or an output buffer, as follows:
METHOD_IN_DIRECT is specified if the driver that handles the IRP receives data in the buffer when it is called. The MDL describes an input buffer, and specifying METHOD_IN_DIRECT ensures that the executing thread has read-access to the buffer.
METHOD_OUT_DIRECT is specified if the driver that handles the IRP will write data into the buffer before completing the IRP. The MDL describes an output buffer, and specifying METHOD_OUT_DIRECT ensures that the executing thread has write-access to the buffer.
For both of these transfer types, Parameters.DeviceIoControl.OutputBufferLength specifies the size of the buffer that is described by the MDL.
这个所谓的“输出缓冲”其实也可以当作输入缓冲。比方说用户模式的caller在输出缓冲里面放点东西,那么驱动自然可以读取这些数据。当然输出缓冲的主要作用还是让驱动程序在这个缓冲里面放东西,然后用户模式的caller可以从这个输出缓冲得到驱动的“输出”。当使用METHOD_IN_DIRECT的时候,会检查发起IRP_MJ_DEVICE_CONTROL的caller(调用DeviceIoControl的进程)是否具有读权限,METHOD_OUT_DIRECT会检查caller是否具有写权限。举个例子,caller用只读方式打开设备,那么METHOD_IN_DIRECT可以成功,然而METHOD_OUT_DIRECT会失败。如果caller用读写权限打开设备,那么METHOD_IN_DIRECT和METHOD_OUT_DIRECT都可以成功。最后一个是METHOD_NEITHER,也就是系统之间将用户模式的虚拟地址送给驱动,驱动自己来处理用户模式地址和内核模式地址的一些关系。这个难度比较大,很容易发生严重问题,往往是灾难性的。比较少用,贴一下MSDN的解释:
The I/O manager does not provide any system buffers or MDLs. The IRP supplies the user-mode virtual addresses of the input and output buffers that were specified toDeviceIoControl orIoBuildDeviceIoControlRequest, without validating or mapping them.
The input buffer's address is supplied by Parameters.DeviceIoControl.Type3InputBuffer in the driver's IO_STACK_LOCATION structure, and the output buffer's address is specified byIrp->UserBuffer.
Buffer sizes are supplied by Parameters.DeviceIoControl.InputBufferLength andParameters.DeviceIoControl.OutputBufferLength in the driver'sIO_STACK_LOCATION structure.
比较常用的是METHOD_BUFFERED和METHOD_IN_DIRECT/METHOD_OUT_DIRECT,这3种类型里面,输入统统是buffered方式,输出由这3个值决定,METHOD_BUFFERED指定输出缓冲使用“缓冲方式buffered”,METHOD_IN_DIRECT/METHOD_OUT_DIRECT指定输出缓冲使用“直接方式”,METHOD_IN_DIRECT和METHOD_OUT_DIRECT在权限上有些区别。
Access:指定权限,I/O管理器根据设定的权限决定是否需要将caller的IRP传递给驱动。
The I/O manager sends the IRP for any caller that has a handle to the file object that represents the target device object.
The I/O manager sends the IRP only for a caller with read access rights, allowing the underlying device driver to transfer data from the device to system memory.
The I/O manager sends the IRP only for a caller with write access rights, allowing the underlying device driver to transfer data from system memory to its device.
比如使用FILE_READ_DATA,那么caller如果是用read权限打开设备,ok,IO管理器会将DeviceIoControl传递给驱动,如果没有read权限,那么DeviceIoControl是失败了。
控制码是使用DeviceIoControl的一个关键。下面举个例子,这个例子里面主要有2个知识点:
1. 如何使用DeviceIoControl在用户模式和内核模式之间通信;
2. 如何使用直接方式内存操作。
驱动程序(支持IRP_MJ_DEVICE_CONTROL)
代码很简单,增加一个IRP_MJ_DEVICE_CONTROL的处理函数,这个函数就是将caller传进来的buffer的数据按照字节和'm'亦或,然后把亦或后的数据填充到输出缓冲返回给caller。先看代码:
#define IOCTL_ENCODE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_IN_DIRECT, FILE_ANY_ACCESS) NTSTATUS HelloWDMIOControl(IN PDEVICE_OBJECT fdo, IN PIRP Irp) { KdPrint(("Enter HelloWDMIOControl\n")); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); //得到输入缓冲区大小 ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength; //得到输出缓冲区大小 ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength; //得到IOCTRL码 ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; NTSTATUS status; ULONG info = 0; switch (code) { case IOCTL_ENCODE: { //获取输入缓冲区,IRP_MJ_DEVICE_CONTROL的输入都是通过buffered io的方式 char* inBuf = (char*)Irp->AssociatedIrp.SystemBuffer; for (ULONG i = 0; i < cbin; i++)//将输入缓冲区里面的每个字节和m亦或 { inBuf[i] = inBuf[i] ^ 'm'; } //获取输出缓冲区,这里使用了直接方式,见CTL_CODE的定义,使用了METHOD_IN_DIRECT。所以需要通过直接方式获取out buffer KdPrint(("user address: %x, this address should be same to user mode addess.\n", MmGetMdlVirtualAddress(Irp->MdlAddress))); //获取内核模式下的地址,这个地址一定> 0x7FFFFFFF,这个地址和上面的用户模式地址对应同一块物理内存 char* outBuf = (char*)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); ASSERT(cbout >= cbin); RtlCopyMemory(outBuf, inBuf, cbin); info = cbin; status = STATUS_SUCCESS; } break; default: status = STATUS_INVALID_VARIANT; break; } Irp->IoStatus.Status = status; Irp->IoStatus.Information = info; IoCompleteRequest(Irp, IO_NO_INCREMENT); KdPrint(("Leave HelloWDMIOControl\n")); return status; }
定义一个控制码,使用FILE_DEVICE_UNKNOWN类型,因为创建设备对象的时候使用的是这个类型。用0x800表示这个控制码。使用METHOD_IN_DIRECT方式操作输出缓冲。访问类型是FILE_ANY_ACCESS,意思是说任何caller只要打开了这个设备(无论使用什么权限),IO管理器都会将DeviceIoControl的IRP传递给驱动。
在IRP_MJ_DEVICE_CONTROL处理函数里面有个switch语句,这个例子只处理定义好的控制码,如果caller传递其它的控制码进来,直接就返回个错误。对于IOCTL_ENCODE控制码,驱动会做这么几个事情:
从irp->AssociatedIrp.SystemBuffer里面获取输入内容 -> 将这些内容按照字节和'm'进行亦或 -> 将亦或完的数据填充到输出缓冲。
其中,MmGetMdlVirtualAddress(Irp->MdlAddress)可以得到用户模式下“输出缓冲”被锁定的虚拟内存的地址。
MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority) 可以得到用户模式下虚拟内存指向的物理内存在内核模式下的一个映射。也就是说这2个函数分别返回用户模式下的虚拟地址和内核模式下的虚拟地址,这2个地址对应同一块物理内存(直接方式交互内存)。
OK,这个驱动就简单处理了IRP_MJ_DEVICE_CONTROL请求,其实只处理IOCTL_ENCODE控制码,将传递进来的buffer编码(亦或),然后将编码后的数据通过输出缓冲返回给caller。
用户模式测试例子
// TestWDMDriver.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> #define DEVICE_NAME L"\\\\.\\HelloWDM" int _tmain(int argc, _TCHAR* argv[]) { HANDLE hDevice = CreateFile(DEVICE_NAME,GENERIC_READ | GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hDevice != INVALID_HANDLE_VALUE) { char* inbuf = "hello world"; char outbuf[12] = {0}; DWORD dwBytes = 0; BOOL b = DeviceIoControl(hDevice, CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_IN_DIRECT, FILE_ANY_ACCESS), inbuf, 11, outbuf, 11, &dwBytes, NULL); for (int i = 0; i < 11; i++) { outbuf[i] = outbuf[i] ^ 'm'; } printf("DeviceIoControl, ret: %d, outbuf: %s, outbuf address: %x operated: %d bytes\n", b, outbuf, outbuf, dwBytes); CloseHandle(hDevice); } else printf("CreateFile failed, err: %x\n", GetLastError()); return 0; }
相当的简单,就是将字符串“hello world"通过DeviceIoControl传递给驱动,然后从输出缓冲中得到驱动的返回。将返回的数据和'm‘亦或,然后打印。我们知道2次亦或可以得到原始值(A ^ B ^ B = A)。那么假如程序正确的话,log应该输出“hello world”。打开debugview,看一下输出。
cmd里面得到outbuf的内容是hello world,成功。同时可以注意红色线画的部分,可以看到caller里面输出缓冲的虚拟地址是0x12ff70,内核模式下驱动通过MmGetMdlVirtualAddress(Irp->MdlAddress)得到的地址也是0x12ff70。MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority)得到的是同一块物理内存在内核模式下的映射,这个地址一定 > 0x7FFFFFFF,因为内核模式的地址存在于进程虚拟地址空间的高2g,0x80000000 - 0xFFFFFFFF,例子里面忘了打印,但是一定是这样的。当然用户模式的虚拟地址一定<= 0x7FFFFFFF,比如打印出来的0x0012ff70。
完整代码包:http://download.csdn.net/detail/zj510/4802072
DDK编译驱动(直接使用build编译),vs2008编译调用例子。