驱动开发(9)处理缓冲I/O设备的读写请求

本博文由CSDN博主zuishikonghuan所作,版权归zuishikonghuan所有,转载请注明出处: http://blog.csdn.net/zuishikonghuan/article/details/50413094

在上面的几篇博文中,介绍了 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 上工作的 32bit Windows 操作系统,虚拟内存的大小恒为4GB虚拟内存是Windows操作系统的最基本机制之一,原理是将线性地址映射到物理内存,他不能被关闭,也不能修改大小,如果你问为什么是4GB,原因很简单,32位的指针的寻址能力只有32位,因此指针最多表示4GB的地址空间。虚拟内存一般是低2GB是用户模式下的,而高2GB由内核使用,虚拟内存分为非页内存和分页内存,非页内存不能交换到磁盘上,而分页内存可以暂时转储到磁盘上,一旦一个虚拟内存页面被转储到文件,那么此页面就被打上一个“脏的”标志,一旦程序访问这样的内存页面,就会触发一个“缺页中断”,从而引发异常处理程序,异常处理程序会将页面从磁盘移到物理内存中,并映射到程序试图访问的虚拟内存地址上。虚拟内存解决了这么几个问题:1。使不同进程的内存空间是私有的,为多任务提供了基础;2。内存页面可以设置访问规则,比如不可执行标记,就是传说中的 DEP ,为了防止hacker通过溢出攻击入侵,但是早已被破解,原理是溢出调用 VirtualProtect 修改虚拟内存保护规则,但从 Vista/7/8/blue/10 开始,引入了地址空间布局随机化(ASLR)(即不管是否需要地址重定向,只要能重定向,则一律重定向,XP上是必须重定向时才会重定向,可以不重定向就不重定向),虽然有堆喷射技术等可以实现绕过 ASLR ,但是当 ASLR 和 DEP 同时工作时,溢出攻击将会变得异常困难(这也提醒我们,开发应用程序时最好启用映像地址随机化)。3。隔离了 R3 和 R0 的内存空间;4。可以将分页内存转储磁盘(这个转储功能是可以关闭和配置的,内存页面转储到磁盘是虚拟内存的一个可选功能,但非页内存永远不能转储到磁盘)

回归正题,说完这些,问题已经呼之欲出,进程上下文切换使用户模式的虚拟内存空间切换,我们提供的缓冲区必定是用户模式虚拟内存的一部分,因此,进入内核模式后,如果进程上下文切换了(上文已经说了,Windows 是抢占式调度的,因此这很可能发生),那么我们的缓冲区指针不就变成野指针了吗!

当应用程序打开一个设备并发出一个 I/O 请求时,会载人到驱动程序的派遣函数中,虽然 此时驱动程序的 Dispatch Function 运行在调用 I/O 函数的用户模式线程上下文中,此时访问用户模式虚拟内存并不会带来影响,但现实情况总是要比理想环境复杂很多,首先,驱动程序可能异步处理 I/O 请求(异步完成 IRP 会在以后说)驱动程序会立即返回 Dispatch Function ,将 IRP 加入一个处理队列,并在之后在其他线程中完成他,此时线程上下文可能会随时被切换,从而使用户模式的虚拟内存地址变成野指针;另一种常见的情况是处理 I/O 请求的设备并非只有一个,比如文件系统驱动程序会将 IRP 发送到磁盘设备驱动程序中,而 对于之后的设备的派遣函数而言,并不能确定运行在原来用户模式线程上下文中,这会带来同样的问题,即此时线程上下文可能会随时被切换,从而使用户模式的虚拟内存地址变成野指针。

所以,操作系统为了解决这个问题,方便硬件驱动程序开发人员,提出了缓冲I/O设备,直接I/O设备等概念。缓冲I/O设备,就是进入内核后,操作系统分配一块内核空间虚拟内存作为缓冲区,并把用户模式虚拟内存的缓冲区中的数据复制进来,复制完成后,传给驱动程序的地址就是内核模式的地址了,内核模式虚拟内存不会因进程上下文切换而改变映射关系,因此这个问题就解决了。

当然,这种方法不一定是最好的方法,因为缓冲区的复制会损失性能,下一篇我们再说其他的方法。

呜,说了这么多,不管你能不能理解,下面来看看如何处理缓冲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。


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值