1.IRP的处理机制类似Windows应用程序中的 “消息处理”机制,驱动程序接受到不同的IRP后,会进入不同的派遣函数,在派遣函数中IRP得到处理。
IRP(输入输出请求包),它是输入输出相关的重要数据结构,上层应用程序与底层驱动程序通信时,应用程序会发出I/O请求。操作系统将I/O请求转化为相应的IRP数据,不同类型的IRP会根据类型传递到不同的派遣函数内。可以使用IRPTrace来跟踪IRP。
2.三种读写方式:①缓冲区方式(DO_BUFFERED_IO) ②直接方式(DO_DIRECT_IO) ③其它方式 0
①缓冲区方式:操作系统将应用程序提供缓冲区的数据复制到内核模式下的地址中,这样,无论操作系统如何切换进程,内核模式的地址都不会改变,IRP的派遣函数将会对内核模式下的缓冲区操作而不是操作用户模式地址的缓冲区。
读写操作一般是由ReadFile和WriteFile函数引起的。例如,WriteFile要求用户提供一段带有数据的缓冲区,并且说明缓冲区的大小,然后WriteFile将这段内存的数据传入到驱动程序中。
对于缓冲区读写方式来说,操作系统会将用户应用程序提供的缓冲区中的数据复制到内核模式下的地址中。IRP的派遣函数将会对内核模式下的缓冲区进行操作,而不是操作用户模式下的缓冲区。对于ReadFile来说,当IRP请求结束时(一般是由IoCompleteRequest函数结束IRP),这段内存地址会被复制到ReadFile提供的缓冲区中,以此读出在内核中的数据。
这样做的优点是,比较简单的解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之间复制数据,影响了运行效率。在少量内存操作时,可以使用该方法。
以“缓冲区”方式读写设备时,操作系统会分配一段内核模式下的内存。这段内存大小等于ReadFile或者WriteFile指定的字节数。并且ReadFile或者WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域会记录这段内存地址。
另外,在派遣函数中,我们还可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile请求多少字节。通过中的Parameters.Write.Length子域知道WriteFile写入多少字节。
然而,WriteFile和ReadFile指定对设备操作多少字节,并不意味着操作了这么多字节。在派遣函数中,应该设置IRP的子域IoStatus.Information。这个子域记录设备实际操作了多少字节。
而用户模式下的ReadFile和WriteFile分别通过各自的第四个参数得到真实操作了多少字节。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 驱动: NTSTATUS MyRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { KdPrint(("进入IRP_MJ_READ派遣函数!\n")); PIO_STACK_LOCATION pIrpStackLoc = IoGetCurrentIrpStackLocation(pIrp); //readLength 和 ReadFile函数中的第三个参数数值相同 //是想要读取的字节数 ULONG readLength = pIrpStackLoc->Parameters.Read.Length; //pIrp->IoStatus.Information的值就是ReadFile函数返回的第四个参数的值 //是实际读取的字节数 pIrp->IoStatus.Information = readLength; pIrp->IoStatus.Status = STATUS_SUCCESS; //填充内核模式下的缓冲区 RtlFillMemory(pIrp->AssociatedIrp.SystemBuffer, readLength, 'A'); //完成IRP IoCompleteRequest(pIrp, IO_NO_INCREMENT); KdPrint(("离开IRP_MJ_READ派遣函数!\n")); return STATUS_SUCCESS; } 应用程序: #include <windows.h> #include <stdio.h> int main(void) { HANDLE hDevice; hDevice = CreateFile("\\\\.\\HelloDDK",GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hDevice == INVALID_HANDLE_VALUE) { DWORD dwError = GetLastError(); printf("%d\n", dwError); } //多分配一个字节,使得printf可以读到'\0'结束 char readBuffer[11] = {0}; DWORD ulLength; ReadFile(hDevice, readBuffer, 10, &ulLength, NULL); printf("%s\n", readBuffer); CloseHandle(hDevice); getchar(); return 0; } |
②直接方式:这种方式需要在创建完设备对象后,在设置设备属性的时候,对Flags子域设置为DO_DIRECT_IO。
和缓冲区方式读写设备不同,直接方式读写设备,操作系统会将用户模式下的缓冲区锁住。然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。
操作系统先将用户模式的地址锁住后,操作系统用内存描述符表(MDL数据结构)记录这段内存。用户模式的这段缓冲区在虚拟内存上是连续的,但是在物理内存上可能是离散的。如下图所示:
MDL记录这段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址是mdl->StartVa,这段虚拟内存的首地址对于第一个页地址偏移量为mdl->ByteOffset。因此,这段虚拟内存的首地址应该是mdl->StartVa + mdl->ByteOffest。DDK提供了几个宏,方便我们得到这几个数值:
#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount)
#define MmGetMdlByteOffsetMdl) ((Mdl)->ByteOffset)
#define MmGetMdlVirtualAddress (Mdl)
((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))
我们通过IRP的pIrp->MdlAddress得到MDL数据结构,这个结构描述了被锁住的缓冲区内存。通过DDK的三个宏MmGetMdlByteCount,MmGetMdlVirtualAddress,MmGetMdlByteOffset可以得到锁住缓冲区的长度,虚拟内存地址,偏移量。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | NTSTATUS MyRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { KdPrint(("进入IRP_MJ_READ派遣函数!\n")); //得到当前IO堆栈 NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); //获取指定的读字节数 ULONG ulReadLen = stack->Parameters.Read.Length; KdPrint(("ulReadLen:%d\n", ulReadLen)); //得到锁定缓冲区的长度 ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress); //得到锁定缓冲区的偏移量 ULONG mdl_offset= MmGetMdlByteOffset(pIrp->MdlAddress); //得到锁定缓冲区的首地址,用户模式下地址 PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress); KdPrint(("mdl_address:0x%08X\n", mdl_address)); KdPrint(("mdl_length:%d\n", mdl_length)); KdPrint(("mdl_offset:%d\n", mdl_offset)); //mdl的长度应该和要读取的长度相等,否则操作设为不成功。 if (mdl_length != ulReadLen) { pIrp->IoStatus.Information = 0; status = STATUS_UNSUCCESSFUL; } else { //用MmGetSystemAddressForMdlSafe得到MDL在内核模式下的映射 PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority); KdPrint(("kernel_address:0x%08X", kernel_address)); //填充内存 RtlFillMemory(kernel_address, mdl_length, 'B'); pIrp->IoStatus.Information = mdl_length; } //设置完成状态 pIrp->IoStatus.Status = status; //结束IRP请求 IoCompleteRequest(pIrp, IO_NO_INCREMENT); KdPrint(("离开IRP_MJ_READ派遣函数!\n")); return status; } |
③其他方式读写设备
在调用IoCreateDevice创建设备后,对pDevObj->Flags即不设置DO_BUFFERED_IO,也不设置DO_DIRECT_IO,此时采用的读写方式就是其他读写方式。
在使用其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中,直接操作应用程序的缓冲区是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。
用这种方式读写时,ReadFile和WriteFile提供的缓冲区内存地址,可以在派遣函数中通过pIrp->UserBuffer字段得到。需要读取的字节数可以从I/O堆栈中的stack->Parameters.Read.Length字段得到。
使用用户模式的内存时要格外小心,因为ReadFile有可能把空指针地址或者非法地址传递给驱动程序。因此,驱动程序使用用户模式地址前,需要探测这段内存是否可读写。探测可读写,可以使用ProbeForWrite函数和try块。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | NTSTATUS MyRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { KdPrint(("进入IRP_MJ_READ派遣函数!\n")); NTSTATUS status = STATUS_SUCCESS; //得到当前堆栈 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); //得到要读取数据的长度 ULONG ulReadLength = stack->Parameters.Read.Length; //得到用户模式下数据的地址 PVOID user_address = pIrp->UserBuffer; KdPrint(("user_address:0x%08X\n", user_address)); __try { KdPrint(("进入__try块!\n")); //测试用户模式下的地址是否可写 ProbeForWrite(user_address, ulReadLength, 4); RtlFillMemory(user_address, ulReadLength, 'C'); KdPrint(("离开__try块!\n")); } __except(EXCEPTION_EXECUTE_HANDLER) { KdPrint(("进入__except块!\n")); status = STATUS_UNSUCCESSFUL; ulReadLength = 0; } //设置完成状态 pIrp->IoStatus.Status = status; //设置操作字节数 pIrp->IoStatus.Information = ulReadLength; //结束IRP请求 IoCompleteRequest(pIrp, IO_NO_INCREMENT); KdPrint(("离开IRP_MJ_READ派遣函数!\n")); return status; } |
3.IO设备控制操作
除了用ReadFile(读设备)和WriteFile(写设备)以外,应用程序还可以通过另外一个WIN32 API函数DeviceIoControl操作设备。DeviceIoControl内部会产生一个IRP_MJ_DEVICE_CONTROL类型的IRP,然后操作系统会将这个IRP转发到派遣函数中。
我们可以用DeviceIoControl定义除读写以外的其他操作,它可以让应用程序和驱动程序进行通信。例如,要对一个设备进行初始化操作,程序员自定义一种I/O控制码,然后用DeviceIoControl将这个控制码和请求一起传递给驱动程序。在派遣函数中,分别对不同的I/O控制码进行处理。
BOOL DeviceIoControl(
HANDLE hDevice, // 已经打开的设备
DWORD dwIoControlCode, // 控制码
LPVOID lpInBuffer, //输入缓冲区
DWORD nInBufferSize, //输入缓冲区大小
LPVOID lpOutBuffer, // 输出缓冲区
DWORD nOutBufferSize, // 输出缓冲区大小
LPDWORD lpBytesReturned, // 实际返回字节数
LPOVERLAPPED lpOverlapped //是否异步操作
);
其中,lpBytesReturned对应派遣函数中的IRP结构中的pIrp->IoStatus.Information。
dwIoControlCode是I/O控制码,控制码也称IOCTL值,是一个32位的无符号整形。IOCTL需要符合DDK的规定。
DDK提供了一个宏CTL_CODE,方便我们定义IOCTL值,其定义如下:
CTL_CODE(DeviceType, Function, Method, Access)
DeviceType:设备对象的类型,这个设备应和创建设备(IoCreateDevice)时的类型相匹配。一般形式如FILE_DEVICE_xxxx的宏。
Function:这是驱动程序定义的IOCTL码。其中:0X0000-0X7FFF为微软保留。0X8000到0XFFFF由程序员自己定义。
Method:这个是操作模式。可以是以下四种模式的一种:
(1) METHOD_BUFFERED:使用缓冲区方式操作
(2) METHOD_IN_DIRECT:使用直接写方式操作
(3) METHOD_OUT_DIRECT:使用直接读方式操作
(4) METHOD_NEITHER:使用其他方式操作
Access:访问权限,一般为FILE_ANY_ACCESS
①缓冲内存模式IOCTL
使用这种模式时,在Win32 API 函数DeviceIoControl的内部,用户提供的缓冲区的内容会被复制到IRP中的pIrp->AssociatedIrp.SystemBuffer内存地址,复制的字节数是由DeviceIoControl指定的输入字节数。
派遣函数可以读取pIrp->AssociatedIrp.SystemBuffer的内存地址,从而获得应用程序提供的输入缓冲区。另外,派遣函数还可以写入pIrp->AssociatedIrp.SystemBuffer的内存地址,这被当做设备输出的数据。操作系统会将这个地址的数据再次复制到DeviceIoControl提供的输出缓冲区中。复制的字节数有pIrp->IoStatus.Information指定。而DeviceIoControl可以通过它的第七个参数得到这个操作字节数。
派遣函数先通过IoGetCurrentStackLocation函数得到当前I/O堆栈。派遣函数通过stack->Parameters.DeviceIoControl.InputBufferLength得到输入缓冲区的大小,通过stack->Parameters.DeviceIoControl.OutputBufferLength得到输出缓冲区的大小。最后通过stack->Parameters.DeviceIoControl.IoControlCode得到IOCTL。在派遣函数中通过switch语句分别处理不同的IOCTL。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | //---------------- 定义IOCTL: #define CTL_TEST1 CTL_CODE(\ FILE_DEVICE_UNKNOWN, \ 0X800, \ METHOD_BUFFERED, \ FILE_ANY_ACCESS) Win32测试程序: #include <windows.h> #include <winioctl.h> #include <stdio.h> #include "..\NT_Driver\ioctl.h" int main(void) { HANDLE hDevice; //打开设备 hDevice = CreateFile("\\\\.\\HelloDDK", GENERIC_READ| GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hDevice == INVALID_HANDLE_VALUE) { printf("CreateFile Error : %d\n", GetLastError()); return -1; } BOOL bRet; CHAR inBuffer[10]; memset(inBuffer, 'B', sizeof(inBuffer)); CHAR outBuffer[10]; DWORD dwReturn; bRet = DeviceIoControl(hDevice, CTL_TEST1, inBuffer, sizeof(inBuffer), &outBuffer, sizeof(outBuffer), &dwReturn, NULL); if (bRet) { for (int i=0; i<(int)dwReturn; i++) { printf("%c ", outBuffer[i]); } printf("\n"); return 0; } else return -1; } 驱动层: NTSTATUS MyDeviceControl(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n")); NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); ULONG inLength = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG outLength = stack->Parameters.DeviceIoControl.OutputBufferLength; ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; switch(code) { case CTL_TEST1:{ KdPrint(("CTL_TEST1\n")); CHAR* inBuffer = (CHAR*)pIrp->AssociatedIrp.SystemBuffer; for (int i=0; i<(int)inLength; i++) { KdPrint(("%c ", inBuffer[i])); } CHAR* outBuffer =(CHAR*) pIrp->AssociatedIrp.SystemBuffer; RtlFillMemory(outBuffer, outLength, 'A'); break; } default: status = STATUS_INVALID_VARIANT; } pIrp->IoStatus.Information = outLength; pIrp->IoStatus.Status = status; IoCompleteRequest(pIrp, IO_NO_INCREMENT); KdPrint(("离开IR_MJ_DEVICE_CONTROL处理函数!\n")); return status; } |
②直接内存模式IOCTL
当使用这种模式时,在用CTL_CODE宏定义这种IOTL时,应该制定Method参数为METHOD_OUT_DIRECT或者METHOD_IN_DIRECT。直接模式的IOCTL同样可以避免驱动程序访问用户模式的内存地址。
METHOD_IN_DIRECT: if the caller of DeviceIoControl or IoBuildDeviceIoControlRequest will pass data to the driver.
METHOD_OUT_DIRECT: if the caller of DeviceIoControl or IoBuildDeviceIoControlRequest will receive data from the driver.
在调用DeviceIoControl时,输入缓冲区的内容被复制到IRP中的pIrp->AssociatedIrp.SystemBuffer内存地址,复制的字节数由DeviceIoControl指定。这个步骤和缓冲区模式的IOCTL的处理时一样的。
但是当对于DeviceIoControl指定的输出缓冲区的处理,直接模式的IOCTL和缓冲区模式的IOCTL却是以不同方式处理的。操作系统会将DeviceIoControl指定的输出缓冲区锁定,然后在内核模式下重新映射一段地址。
派遣函数中的IRP结构中的pIrp->MdlAddress记录DeivceIoControl指定的输出缓冲区。派遣函数应该使用MmGetSystemAddressForMdlSafe将这段内存映射到内核模式下的内存地址。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 定义IOCTL: #define IOCTL_TEST2 CTL_CODE(\ FILE_DEVICE_UNKNOWN, \ 0x801, \ METHOD_IN_DIRECT, \ FILE_ANY_ACCESS) 驱动层: NTSTATUS MyDeviceControl(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n")); NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); ULONG inLength = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG outLength = stack->Parameters.DeviceIoControl.OutputBufferLength; ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; switch(code) { case CTL_TEST2: { KdPrint(("CTL_TEST2!\n")); CHAR* inBuffer = (CHAR*)pIrp->AssociatedIrp.SystemBuffer; for (int i=0; i<(int)inLength; i++) { KdPrint(("%c ", inBuffer[i])); } //调用MmGetSystemAddressForMdlSafe获得内核模式下的映射地址 CHAR* outBuffer = (CHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority); KdPrint(("0x%08X\n", outBuffer)); RtlFillMemory(outBuffer, outLength, 'A'); break; } default: status = STATUS_INVALID_VARIANT; } pIrp->IoStatus.Information = outLength; pIrp->IoStatus.Status = status; IoCompleteRequest(pIrp, IO_NO_INCREMENT); KdPrint(("离开IR_MJ_DEVICE_CONTROL处理函数!\n")); return status; } |
③其他内存模式IOCTL
在这个模式中,在用CTL_CODE定义IOCTL时,应该指定Method为METHOD_NEITHER。这种方式IOCTL很少被用到,因为它直接访问用户模式地址。使用用户模式地址必须保证调用DeviceIoControl的线程与派遣函数运行在同一个线程上下文中。
对于DeviceIoControl提供的输入缓冲区的地址,派遣函数可以通过I/O堆栈的stack->Parameters.DeviceIoControl.Type3InputBuffer获得。同时,派遣函数可以通过pIrp->UserBuffer获得DeviceIoControl的输出缓冲区。
由于驱动程序的派遣函数不能保证传递进来的用户模式地址是合法的。所以最好对传入的用户模式地址进行可读写判断。使用ProbeForRead和ProbeForWrite函数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 定义IOCTL: #define IOCTL_TEST2 CTL_CODE(\ FILE_DEVICE_UNKNOWN, \ 0x801, \ METHOD_NERTHER, \ FILE_ANY_ACCESS) 驱动层: NTSTATUS MyDeviceControl(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n")); NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); ULONG inLength = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG outLength = stack->Parameters.DeviceIoControl.OutputBufferLength; ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; switch(code) { case CTL_TEST3: { KdPrint(("CTL_TEST3!\n")); CHAR* inBuffer = (CHAR*)stack->Parameters.DeviceIoControl.Type3InputBuffer; KdPrint(("inBuffer address:0x%08X", inBuffer)); CHAR* outBuffer = (CHAR*)pIrp->UserBuffer; KdPrint(("outBuffer address:0x%08X", outBuffer)); __try { KdPrint(("进入try块!\n")); ProbeForRead(inBuffer, inLength, 4); for (ULONG i =0; i<inLength; i++) { KdPrint(("%c", inBuffer[i])); } ProbeForWrite(outBuffer, outLength, 4); RtlFillMemory(outBuffer, outLength, 'C'); } __except(EXCEPTION_EXECUTE_HANDLER) { KdPrint(("进入except块!\n")); status = STATUS_UNSUCCESSFUL; } break; } default: status = STATUS_INVALID_VARIANT; } pIrp->IoStatus.Information = outLength; pIrp->IoStatus.Status = status; IoCompleteRequest(pIrp, IO_NO_INCREMENT); KdPrint(("离开IR_MJ_DEVICE_CONTROL处理函数!\n")); return STATUS_SUCCESS; } |