在上面的几篇博文中,介绍了IRP与派遣函数,通过例子“磁盘设备的绝对读写”演示了在应用程序中向一个设备发出I/O请求,并实现了驱动程序中处理一个I/O请求——由DeviceIoControl这个Win32API经过一系列调用,在内核中生成的IRP_MJ_DEVICE_CONTROL这个IRP。
首先我们需要来看看什么叫“缓冲I/O设备”
还记得我们在之前的“NT驱动的基本结构”一篇博文中,我们调用内核函数创建设备后,对DEVICE_OBJECT进行了一些操作,比如:取得设备扩展地址,不知道大家注意到这行代码了没有:
pDevObj->Flags |= DO_BUFFERED_IO;
我们修改了DEVICE_OBJECT结构中的Flags成员,使用按位或的方式增加了一个DO_BUFFERED_IO标志,将设备修改成了缓冲I/O设备。
提示:其实缓冲I/O设备设个名词不一定准确,或许叫“使用缓冲I/O工作的设备”更好一点。 |
让我们先来看看什么是”缓冲I/O设备”,并以此为线索了解一些原理。
当我们调用ReadFile(Ex)和WriteFile(Ex)读写文件,管道或者设备时,我们需要提供一个缓冲区的指针,如果是同步读(注意是同步),那么ReadFile返回后,我们要读的数据就在缓冲区里了,如果是同步写(同样,注意是同步),则WriteFile结束后,我们要写的数据就写完了,当然都是没有出错的情况下。
这一切看起来似乎并没有什么特殊的,我们早已经对这两个函数熟悉到不能再熟悉,但是你有没有发现有些不对劲,但是又不知道在哪里呢?
我们来回忆一下API调用过程,我们调用的ReadFile和WriteFile,是Win32子系统提供的编程接口,也就是Win32API,Win32API会对实参进一步包装,可能调用其他子系统API,但最终都会调用从ntdll导出的NT Native API,nativeAPI调用KiFastSystemCall,随后进入内核,进入内核在老硬件平台和老版本winNT上是通过软中断int 2e,现在一般是有专门的指令,为了减少从R3切换到R0带来的性能损失,进入内核后,调用内核模式下的函数,比如ZwReadFile,这个函数会查SSDT,找到NtReadFile并调用之,之后,会调用内核中I/O管理器的接口,I/O管理器构造生成IRP,并发送到相应设备所在驱动程序的派遣函数中。
你可能会奇怪,对啊,没有什么啊,我们不就是要写派遣函数从而处理这些IRP么?那么问题来了,Windows是一个多任务抢占式调度的操作系统(上世纪的Windows1.0等不是抢占式的,是多任务协作式调度的),虽然在某一个具体的时刻,一个CPU(或一个CPU核心)不可能是多任务的,但从宏观上说,操作系统中有大量的线程是并发执行的!于是,“进程上下文”和“线程上下文”在频繁地被切换,上下文中记录了CPU的执行现场,寄存器,堆栈地址等,用于上下文切换后恢复现场使得程序得以继续执行,进程上下文的切换就意味着用户模式虚拟内存(线性地址空间)的切换,博主现在发现,在驱动开发(1)基础知识一篇博文中对虚拟内存一笔带过真是个错误,所以在这里简单说一下:
虚拟内存也叫线性地址空间,在Intel 80x86架构的CPU上工作的32bitWindows操作系统,虚拟内存的大小恒为4GB,虚拟内存是Windows操作系统的最基本机制之一,原理是将线性地址映射到物理内存,他不能被关闭,也不能修改大小,如果你问为什么是4GB,原因很简单,32位的指针的寻址能力只有32位,因此指针最多表示4GB的地址空间。虚拟内存一般是低2GB是用户模式下的,而高2GB由内核使用,虚拟内存分为非页内存和分页内存,非页内存不能交换到磁盘上,而分页内存可以暂时转储到磁盘上,一旦一个虚拟内存页面被转储到文件,那么此页面就被打上一个“脏的”标志,一旦程序访问这样的内存页面,就会触发一个“缺页中断”,从而引发异常处理程序,异常处理程序会将页面从磁盘移到物理内存中,并映射到程序试图访问的虚拟内存地址上。虚拟内存解决了这么几个问题:1。使不同进程的内存空间是私有的,为多任务提供了基础;2。内存页面可以设置访问规则,比如不可执行标记,就是传说中的DEP,为了防止hacker通过溢出攻击入侵,但是早已被破解,原理是溢出调用VirtualProtect修改保护规则,但从Vista/7/8/blue/10开始,引入了动态基地址机制(即不管是否需要地址重定向,只要能重定向,则一律重定向,XP上是必须重定向时才会重定向,可以不重定向就不重定向),免疫了溢出攻击!3。隔离了R3和R0的内存空间;4。可以将分页内存转储磁盘(这个转储功能是可以关闭和配置的,内存页面转储到磁盘是虚拟内存的一个可选功能,但非页内存永远不能转储到磁盘)
回归正题,说完这些,问题已经呼之欲出,进程上下文切换使用户模式的虚拟内存空间切换,我们提供的缓冲区必定是用户模式虚拟内存的一部分,因此,进入内核模式后,如果进程上下文切换了(上文已经说了,Windows是抢占式调度的,因此这很可能发生),那么我们的缓冲区指针不就变成野指针了吗!所以,操作系统必须解决这个问题,因此出现了缓冲I/O设备,直接I/O设备等概念。缓冲I/O设备,就是进入内核后,操作系统分配一块内核空间虚拟内存作为缓冲区,并把用户模式虚拟内存的缓冲区中的数据复制进来,这个过程是可以被打断从而调度其他程序的,复制完成后,传给驱动程序的地址就是内核模式的地址了,内核模式虚拟内存不会因进程上下文切换而改变映射关系,因此这个问题就解决了。
当然,这种方法不一定是最好的方法,因为缓冲区的复制会损失性能,下一篇我们再说其他的方法。
可能你发现了新问题,既然从用户模式虚拟内存复制到内核模式虚拟内存的过程可以被打断,那么驱动程序处理时也和用户模式程序同步中断不就没有上面的问题了吗,而事实是,Windows操作系统是一个支持异步处理的操作系统,驱动程序的派遣函数执行线程并不是应用程序调用API的线程,驱动程序的线程属于一个叫SYSTEM的虚拟进程,其PID恒为4,而API本质是异步的,之所以ReadFile和WriteFile会是同步读写,是因为ReadFile和WriteFile在内部调用ReadFileEx和WriteFileEx,他们立即返回后调用了WaitForSingleObject等待内核线程执行完毕从而挂起了用户模式的线程,所以驱动程序访问用户模式的虚拟内存是非常不可靠的,也不可能做到同时中断。
呜,说了这么多,不管你能不能理解,下面来看看如何处理缓冲I/O设备的读写请求。
我们的驱动程序显然是没有具体的存储硬件的,因此我们不得不寻找一些其他方法,本例中分配了一些内核虚拟内存(分页内存)当做是存储空间。
在驱动开发中,在某些时候使用分页内存会引发页故障而使内核崩溃(传说中的蓝屏死机),一般和“中断请求级”有关,蓝屏死机其实是内核的一种异常处理,只不过处理的有些简单粗暴,蓝屏,输出一些调试信息,然后死机,不响应用户的输入。但本例中没有影响,我们以后再讨论什么时候不能使用分页内存。
是时候说说IRP结构中几个成员的意思了,IRP结构是如何定义的看之前我写的博客“IRP与派遣函数”:
MdlAddress:指向一个描述的用户缓冲区的 MDL (内存描述符表)的指针,如果驱动程序不使用直接 I/O(DO_DIRECT_IO),此指针为 NULL。本例中使用缓冲方式,因此不涉及这个,下一篇博文中详细介绍直接 I/O。
Flags:文件系统驱动程序使用此字段,只读。
AssociatedIrp.SystemBuffer:指向内核模式虚拟内存缓冲区的指针。用于缓冲I/O方式。
IoStatus:包含一个驱动程序存储状态和信息的 IO_STATUS_BLOCK 结构。
Status:完成状态,
Pointer:保留。仅供内部使用。
Information:这是设置为依赖于请求的值。例如,成功完成了读写请求,用于设置操作的字节数。
其他的不再一一列举了,如有兴趣可以看MSDN
应用程序,发出读写请求的程序,源码:
#include "stdafx.h" #include<Windows.h> int _tmain(int argc, _TCHAR* argv[]) { //打开设备 HANDLE handle = CreateFileA("\\\\.\\MyDevice1_link", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (handle == INVALID_HANDLE_VALUE){ MessageBoxA(0, "打开设备失败", "错误", 0); return 0; } unsigned char buffer1[50] = { 0 }; unsigned char buffer2[50] = { 0 }; DWORD len; //测试1:写入超出驱动申请的缓冲区大小的数据 if (!WriteFile(handle, buffer1, 1000, &len, NULL)) printf("1: failed\n");//当然会失败 //测试2:写入字符串hello, driver,偏移量为5 //也就是说,跳过前五个字节再写入 sprintf((char*)buffer1, "hello, driver\r\n"); OVERLAPPED ol = { 0 }; ol.Offset = 5; if (WriteFile(handle, buffer1, strlen((char*)buffer1), &len, &ol)){ printf("2: len: %d\n", len); } //测试3: 读出0-48(共49字节)的数据,并使用16进制输出 if (ReadFile(handle, buffer2, 49, &len, NULL)){ printf("3: len: %d\n", len); for (int i = 0; i < len; i++){ printf("0x%02X ", buffer2[i]); } } //测试4: 获取驱动缓冲区已使用的大小(抽象成文件大小) printf("used: %d\n", GetFileSize(handle, NULL)); getchar(); CloseHandle(handle); return 0; }
驱动程序源码:
#include <ntddk.h> extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriverObject); extern "C" NTSTATUS DefDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp); extern "C" NTSTATUS WriteDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp); extern "C" NTSTATUS ReadDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp); extern "C" NTSTATUS QueryInfomationDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp); #define BUFFER_LENGTH 512 //缓冲区长度 //我们定义的设备扩展 typedef struct _DEVICE_EXTENSION { UNICODE_STRING SymLinkName;//符号链接名 //这是为我们要处理读写请求而准备的缓冲区长度和指针 ULONG filelength;//已经使用的长度(这个很像一个文件,故这样命名) PUCHAR buffer;//缓冲区指针 } DEVICE_EXTENSION, *PDEVICE_EXTENSION; #pragma code_seg("INIT") extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { DbgPrint("DriverEntry\r\n"); pDriverObject->DriverUnload = DriverUnload;//注册驱动卸载函数 //注册派遣函数 pDriverObject->MajorFunction[IRP_MJ_CREATE] = DefDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DefDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_WRITE] = WriteDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_READ] = ReadDispatchRoutine; pDriverObject->MajorFunction[IRP_MJ_QUERY_INFORMATION] = QueryInfomationDispatchRoutine; NTSTATUS status; PDEVICE_OBJECT pDevObj; PDEVICE_EXTENSION pDevExt; //创建设备名称的字符串 UNICODE_STRING devName; RtlInitUnicodeString(&devName, L"\\Device\\MyDevice1"); //创建设备 status = IoCreateDevice(pDriverObject, sizeof(DEVICE_EXTENSION), &devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevObj); if (!NT_SUCCESS(status)) return status; pDevObj->Flags |= DO_BUFFERED_IO;//将设备设置为缓冲I/O设备 pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;//得到设备扩展 //分配用于处理读写请求的缓冲区 pDevExt->buffer = (PUCHAR)ExAllocatePool(PagedPool, BUFFER_LENGTH); //设置缓冲区已使用的大小 pDevExt->filelength = 0; //内存清零 RtlZeroMemory(pDevExt->buffer, BUFFER_LENGTH); //创建符号链接 UNICODE_STRING symLinkName; RtlInitUnicodeString(&symLinkName, L"\\??\\MyDevice1_link"); pDevExt->SymLinkName = symLinkName; status = IoCreateSymbolicLink(&symLinkName, &devName); if (!NT_SUCCESS(status)) { IoDeleteDevice(pDevObj); return status; } return STATUS_SUCCESS; } extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriverObject) { DbgPrint("DriverUnload\r\n"); PDEVICE_OBJECT pDevObj; pDevObj = pDriverObject->DeviceObject; PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;//得到设备扩展 //删除符号链接 UNICODE_STRING pLinkName = pDevExt->SymLinkName; IoDeleteSymbolicLink(&pLinkName); //删除设备 IoDeleteDevice(pDevObj); } extern "C" NTSTATUS DefDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp) { DbgPrint("DefDispatchRoutine\r\n"); NTSTATUS status = STATUS_SUCCESS; pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return status; } extern "C" NTSTATUS WriteDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp) { DbgPrint("WriteDispatchRoutine\r\n"); NTSTATUS status = STATUS_SUCCESS; //得到设备扩展 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; //得到I/O堆栈的当前这一层,也就是IO_STACK_LOCATION结构的指针 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); ULONG WriteLength = stack->Parameters.Write.Length;//获取写入的长度 ULONG WriteOffset = (ULONG)stack->Parameters.Write.ByteOffset.QuadPart;//获取写入的偏移量 DbgPrint("WriteLength: %d\r\nWriteOffset: %d\r\n", WriteLength, WriteOffset);//输出相关信息 PVOID buffer = pIrp->AssociatedIrp.SystemBuffer;//得到缓冲区指针 if (WriteOffset + WriteLength > BUFFER_LENGTH){ //如果要操作的超出了缓冲区,则失败完成IRP,返回无效 DbgPrint("E: The size of the data is too long.\r\n"); status = STATUS_FILE_INVALID; WriteLength = 0; } else{ //没有超出,则进行缓冲区复制,将写入的数据复制缓冲区 memcpy(pDevExt->buffer + WriteOffset, buffer, WriteLength); status = STATUS_SUCCESS; //设置新的已经使用长度 if (WriteLength + WriteOffset > pDevExt->filelength){ pDevExt->filelength = WriteLength + WriteOffset; } } pIrp->IoStatus.Status = status;//设置IRP完成状态,会设置用户模式下的GetLastError pIrp->IoStatus.Information = WriteLength;//设置操作字节数 IoCompleteRequest(pIrp, IO_NO_INCREMENT);//完成IRP return status; } extern "C" NTSTATUS ReadDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp) { DbgPrint("ReadDispatchRoutine\r\n"); NTSTATUS status = STATUS_SUCCESS; //得到设备扩展 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; //得到I/O堆栈的当前这一层,也就是IO_STACK_LOCATION结构的指针 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); ULONG ReadLength = stack->Parameters.Read.Length;//得到读的长度 ULONG ReadOffset = (ULONG)stack->Parameters.Read.ByteOffset.QuadPart;//得到读偏移量 DbgPrint("ReadLength: %d\r\nReadOffset: %d\r\n", ReadLength, ReadOffset);//输出相关信息 PVOID buffer = pIrp->AssociatedIrp.SystemBuffer;//得到缓冲区指针 if (ReadOffset + ReadLength > BUFFER_LENGTH){ //如果要操作的超出了缓冲区,则失败完成IRP,返回无效 DbgPrint("E: The size of the data is too long.\r\n"); status = STATUS_FILE_INVALID;//会设置用户模式下的GetLastError ReadLength = 0;//设置操作了0字节 } else{ //没有超出,则进行缓冲区复制 DbgPrint("OK, I will copy the buffer.\r\n"); RtlMoveMemory(buffer, pDevExt->buffer + ReadOffset, ReadLength); status = STATUS_SUCCESS; } pIrp->IoStatus.Status = status;//设置IRP完成状态,会设置用户模式下的GetLastError pIrp->IoStatus.Information = ReadLength;//设置操作字节数 IoCompleteRequest(pIrp, IO_NO_INCREMENT);//完成IRP return status; } extern "C" NTSTATUS QueryInfomationDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp) { DbgPrint("QueryInfomationDispatchRoutine\r\n"); //用于处理应用程序GetFileSize获取文件大小(已经使用的大小) PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);//得到I/O堆栈的当前这一层,也就是IO_STACK_LOCATION结构的指针 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;//得到设备扩展 FILE_INFORMATION_CLASS fic = stack->Parameters.QueryFile.FileInformationClass;//得到FileInformationClass枚举类型 if (fic == FileStandardInformation){ PFILE_STANDARD_INFORMATION FileStandardInfo = (PFILE_STANDARD_INFORMATION)pIrp->AssociatedIrp.SystemBuffer;//得到缓冲区指针 FileStandardInfo->EndOfFile = RtlConvertLongToLargeInteger(pDevExt->filelength);//设置文件大小(已经使用的大小) } pIrp->IoStatus.Status = STATUS_SUCCESS;//设置IRP完成状态,会设置用户模式下的GetLastError pIrp->IoStatus.Information = stack->Parameters.QueryFile.Length;//设置操作字节数 IoCompleteRequest(pIrp, IO_NO_INCREMENT);//完成IRP return STATUS_SUCCESS; }
我们来看看效果图:
可以看出,我们的驱动程序成功处理了来自应用程序的读/写这两个I/O请求,对于读请求,就从内存中读出数据给应用程序,对于写请求,就写进内存。代码中带了详细的注释,再结合上文,想必大家都能很轻松地看懂。我们可以发现,这已经可以当做一个存储设备使用了,而且读写速度极快(只不过掉电后数据会丢失),如果我们再完善一下,就相当于我们为操作系统增加了一个完善的存储设备!如果我们想办法让系统识别出这个设备是一个磁盘,那么我们就相当于虚拟出来一块磁盘,并且可以让系统为其创建分区,创建卷设备,出现在“此电脑”(资源管理器)中,而且用户(不知情的前提下)和所有的应用程序都会认为这是一个真正的磁盘!这就是以后会讲到的“虚拟设备”,是不是很神奇?但这只是驱动开发魅力的冰山一角!驱动开发所能实现的还有很多,比如,各种内核中hook(利用内核hook可以劫持内核函数和对象,从而拦截应用程序发出的系统调用),各种过滤驱动(利用过滤驱动可以拦截、修改应用程序向设备发送本来想发送的请求,还可以实现对底层设备抽象,比如把网络请求转换成usb传输,而应用程序依旧以为是TCP/IP……),文件系统透明加密(其实这个也是过滤驱动)……
现在,经过这4篇博客以后,想必大家对IRP和处理I/O请求有了非常深刻的认识,并且,我们是站在原理的高度去研究的,但是,这还没完!如果你深入思考这些原理,就会发现依旧存在盲区,比如,本质是异步,但用户线程等待驱动程序处理完毕而变成了同步,那么是怎么等待的?还有,我们一定要完成掉IRP吗,还有没有其他处理方法?哈哈,这些问题,IRP的同步和异步,过滤驱动和IRP转发……我将会在之后的博客中一一做介绍、讨论和分析。那么下一篇博文,我们来看看直接I/O。