Windows驱动开发-设备读写方式
设备读写方式共三种:
方式 | Flag | 特点 |
缓冲区方式读写 |
DO_BUFFERED_IO | I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区。而你的驱动程序将使用这个系统缓冲区工作。I/O管理器负责在系统缓冲区和用户模式缓冲区之间复制数据。 |
直接方式读写 |
DO_DIRECT_IO | I/O管理器锁定了包含用户模式缓冲区的物理内存页,并创建一个称为MDL(内存描述符表)的辅助数据结构来描述锁定页。因此你的驱动程序将使用MDL工作。 |
Neither方式 | 0 | I/O管理器仅简单地把用户模式的虚拟地址传递给你。而使用用户模式地址的驱动程序应十分小心。 |
1,缓冲区方式读写
其优点是比较简单的解决了将用户地址传入驱动的问题,缺点是需要用户模式和内核模式之间数据复制,可想而知,运行效率会受到影响。适合少量内存操作时使用的一种方法。
创建好设备IoCreateDevice后,需要设置读写方式
pDevObj->Flags |= DO_BUFFERED_IO
以readfile为例,首先应用程序中需要提供一段缓冲区并把缓冲区大小作为参数传入,例如:
UCHAR OutputBuffer[10]; DWORD RetLen = 0; readfile(hDevice,OutputBuffer,sizeof(OutputBuffer),&RetLen,NULL);
OutputBuffer是提供的输出缓冲区,是用户模式的内存地址,操作系统将此缓冲区的数据复制到内核模式下的地址中,sizeof(OutputBuffer)是缓冲区的大小,
而RetLen是真正的输出的字节数。
那么内核模式怎么得到此内核模式地址呢?怎么得到writefile或readfile的字节数呢?答案在下面。
此内核模式下的地址可以通过此readfile创建的IRP的AssociatedIrp.SystemBuffer得到。
假如请求的IRP为PIRP pIrp(一般是派遣函数的参数),那么:
UCHAR* OutputBuffer= (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;
而readfile请求的字节数为IO_STACK_LOCATION中的Parameters.Read.Length,
writefilew为IO_STACK_LOCATION中的Parameters.Write.Length
//得到当前堆栈 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); //得到readfile缓冲区大小 ULONG cbread = stack->Parameters..Read.Length; //得到writefile缓冲区大小 ULONG cbwrite = stack->Parameters.Write.Length;
得到了内核模式下的缓冲区地址了就可以对此缓冲区操作了。比如:
UCHAR* OutputBuffer= (UCHAR*)pIrp->AssociatedIrp.SystemBuffer; ULONG cbread = stack->Parameters..Read.Length; memcpy(OutputBuffer,0xBB,cbread);
这样用户模式下的缓冲区内得到的数据是0xBB。
还要设置实际操作的字节数,pIrp->IoStatus.Information = cbread;(实际操作的字节数不一定要设置为缓冲区的大小,但也不应该大于缓冲区的大小)
那么用户模式下readfile的RetLen被设置为cbread。
下面是IRP_MJ_READ的派遣函数:
NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { KdPrint(("Enter DispatchRead\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 DispatchRead\n")); return status; }
2,直接方式读写设备
操作系统会将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。无论操作系统如何切换进程,内核模式地址都保持不变。
创建好设备IoCreateDevice后,需要设置读写方式Flag
pDevObj->Flags |= DO_DIRECT_IO
这里涉及到内存描述符表结构体MDL
typedef struct _MDL { struct _MDL *Next; CSHORT Size; CSHORT MdlFlags; struct _EPROCESS *Process; PVOID MappedSystemVa; PVOID StartVa; //给出了用户缓冲区的虚拟地址,第一个页地址,这个地址仅在拥有数据缓冲区的用户模式进程上下文中才有效 ULONG ByteCount; //是缓冲区的字节长度 ULONG ByteOffset; //是缓冲区起始位置在一个页帧中的偏移值,那么缓冲区的首地址是mdl->StartVa+mdl->ByteOffset } MDL, *PMDL;
用图表示内存描述符表(MDL)结构为:
由图可知用户模式的这段缓冲区在虚拟内存上是连续的,但在物理内存上可能是离散的。
下面是一些MDL相关的函数
IoAllocateMdl | 创建MDL |
IoBuildPartialMdl | 创建一个已存在MDL的子MDL |
IoFreeMdl | 销毁MDL |
MmBuildMdlForNonPagedPool | 修改MDL以描述内核模式中一个非分页内存区域 |
MmGetMdlByteCount | 取缓冲区字节大小(得到mdl->ByteCount) |
MmGetMdlByteOffset | 取缓冲区在第一个内存页中的偏移(得到mdl->ByteOffset) |
MmGetMdlVirtualAddress | 取虚拟地址((PVOID)(PCHAR)(mdl->StartVa+mdl->ByteOffset)) |
MmGetSystemAddressForMdl | 创建映射到同一内存位置的内核模式虚拟地址 |
MmGetSystemAddressForMdlSafe | 与MmGetSystemAddressForMdl相同,但Windows 2000首选 |
MmInitializeMdl | (再)初始化MDL以描述一个给定的虚拟缓冲区 |
MmPrepareMdlForReuse | 再初始化MDL |
MmProbeAndLockPages | 地址有效性校验后锁定内存页 |
MmSizeOfMdl | 取为描述一个给定的虚拟缓冲区的MDL所占用的内存大小 |
MmUnlockPages | 为该MDL解锁内存页 |
以readfile为例介绍直接方式读取设备:
//用户模式调用readfile: UCHAR OutputBuffer[10]; DWORD RetLen = 0; readfile(hDevice,OutputBuffer,sizeof(OutputBuffer),&RetLen,NULL); //内核模式得到要读取的字节数:(与以缓冲区读写方式一样) //得到当前堆栈 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); //得到readfile要读取的字节数 ULONG cbread = stack->Parameters..Read.Length; //另外,通过IRP的pIrp->MdlAddress得到MDL数据结构,这个结构描述了被锁定的缓冲区的内存。
下面是一个IRP_MJ_READ的派遣函数:
NTSTATUS DispathRead(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp) { KdPrint(("Enter DispathRead\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); //mdl虚拟内存的长度 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在内核模式下的映射,被映射到内核模式下的内存地址,必定在0X80000000-0XFFFFFFFF之间 PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority); KdPrint(("kernel_address:0X%08X\n",kernel_address)); memset(kernel_address,0XAA,ulReadLength); //对内核模式下的内存地址进行操作 pIrp->IoStatus.Information = ulReadLength; //设置实际操作字节数 } pIrp->IoStatus.Status = status; IoCompleteRequest( pIrp, IO_NO_INCREMENT ); KdPrint(("Leave DispatchRead\n")); return status; }