派遣函数(2) - 设备对象的3种读写方式

转载自:http://write.blog.csdn.net/postedit



读写操作

设备对象一共可以有三种读写方式,分别是缓冲区方式读写,直接方式读写,其他方式读写。这三种方式的Flags分别对应为DO_BUFFERED_IODO_DIRECT_IO0

在驱动程序创建设备对象的时候,需要考虑好该设备是采用何种读写方式。当IoCreateDevice创建完设备后,需要对设备对象的Flags子域进行设置。设置不同的Flags子域会导致以不同的读写方式操作设备。

 

示例代码:

//创建设备

status = IoCreateDevice(pDriverObj,   sizeof(DEVICE_EXTENSION),

&(UNICODE_STRING)ustrDeviceName,

                            FILE_DEVICE_UNKNOWN, 0, TRUE, &pDeviceObj);

//判断是否创建成功

if (!NT_SUCCESS(status))

{

         return status;

}

//设置读写方式,这里为缓冲区读写方式

pDeviceObj->Flags |= DO_BUFFERED_IO;

 

 

1.       缓冲区方式读写设备

读写操作一般是由ReadFileWriteFile函数引起的。例如,WriteFile要求用户提供一段带有数据的缓冲区,并且说明缓冲区的大小,然后WriteFile将这段内存的数据传入到驱动程序中。

对于缓冲区读写方式来说,操作系统会将用户应用程序提供的缓冲区中的数据复制到内核模式下的地址中。IRP的派遣函数将会对内核模式下的缓冲区进行操作,而不是操作用户模式下的缓冲区。对于ReadFile来说,当IRP请求结束时(一般是由IoCompleteRequest函数结束IRP),这段内存地址会被复制到ReadFile提供的缓冲区中,以此读出在内核中的数据。

这样做的优点是,比较简单的解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之间复制数据,影响了运行效率。在少量内存操作时,可以使用该方法。

 

以“缓冲区”方式读写设备时,操作系统会分配一段内核模式下的内存。这段内存大小等于ReadFile或者WriteFile指定的字节数。并且ReadFile或者WriteFile创建的IRPAssociatedIrp.SystemBuffer子域会记录这段内存地址。

另外,在派遣函数中,我们还可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile请求多少字节。通过中的Parameters.Write.Length子域知道WriteFile写入多少字节。

然而,WriteFileReadFile指定对设备操作多少字节,并不意味着操作了这么多字节。在派遣函数中,应该设置IRP的子域IoStatus.Information。这个子域记录设备实际操作了多少字节。

而用户模式下的ReadFileWriteFile分别通过各自的第四个参数得到真实操作了多少字节。

 

示例代码:

驱动:

NTSTATUS FuckRead(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;

}

派遣函数(2) - 设备对象的3种读写方式 - Fly - 从C开始

  

 

2.       直接方式读写设备

这种方式需要在创建完设备对象后,在设置设备属性的时候,对Flags子域设置为DO_DIRECT_IO

和缓冲区方式读写设备不同,直接方式读写设备,操作系统会将用户模式下的缓冲区锁住。然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区内核模式的缓冲区指向的是同一区域的物理内存

操作系统先将用户模式的地址锁住后,操作系统用内存描述符表(MDL数据结构)记录这段内存。用户模式的这段缓冲区在虚拟内存上是连续的,但是在物理内存上可能是离散的。如下图所示:

派遣函数(2) - 设备对象的3种读写方式 - Fly - 从C开始

 

 

         MDL记录这段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址是mdl->StartVa,这段虚拟内存的首地址对于第一个页地址偏移量为mdl->ByteOffset。因此,这段虚拟内存的首地址应该是mdl->StartVa + mdl->ByteOffestDDK提供了几个宏,方便我们得到这几个数值:

#define   MmGetMdlByteCount(Mdl)          ((Mdl)->ByteCount)

#define   MmGetMdlByteOffsetMdl) ((Mdl)->ByteOffset)

#define   MmGetMdlVirtualAddress (Mdl)         

                                     ((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))

 

         我们通过IRPpIrp->MdlAddress得到MDL数据结构,这个结构描述了被锁住的缓冲区内存。通过DDK的三个宏MmGetMdlByteCountMmGetMdlVirtualAddressMmGetMdlByteOffset可以得到锁住缓冲区的长度,虚拟内存地址,偏移量。

 

示例代码:

NTSTATUS FuckRead(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;

}

派遣函数(2) - 设备对象的3种读写方式 - Fly - 从C开始

  

3.       其他方式读写设备

在调用IoCreateDevice创建设备后,对pDevObj->Flags即不设置DO_BUFFERED_IO,也不设置DO_DIRECT_IO,此时采用的读写方式就是其他读写方式。

在使用其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中,直接操作应用程序的缓冲区是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。

用这种方式读写时,ReadFileWriteFile提供的缓冲区内存地址,可以在派遣函数中通过pIrp->UserBuffer字段得到。需要读取的字节数可以从I/O堆栈中的stack->Parameters.Read.Length字段得到。

使用用户模式的内存时要格外小心,因为ReadFile有可能把空指针地址或者非法地址传递给驱动程序。因此,驱动程序使用用户模式地址前,需要探测这段内存是否可读写。探测可读写,可以使用ProbeForWrite函数和try块。

 

示例代码:

NTSTATUS FuckRead(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;

}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值