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;
}

原文地址:https://www.cnblogs.com/a-s-m/p/12332727.html

时间: 2024-08-26 11:54:39

Windows驱动开发-设备读写方式的相关文章

windows驱动开发-设备扩展

设备对象Device_Object记录通用设备信息,另外一些信息记录在设备扩展里,设备扩展由程序员自己定义,由程序员指定内容和大小,由I/O管理器创建,并保存在非分页内存中. 驱动程序中,尽量避免使用全局函数,因为全局函数往往导致函数的不可重入性, 重入性是指,在多线程程序中,多个函数并行运行,函数的运行结果不会根据函数的先后调用顺序而导致不同, 解决办法是,将全局变量以设备扩展的形式存储,并加以适当的同步保护措施, 除此之外,在设备扩展中,还会记录以下一些内容, 1,设备对象反向指针, 2,设

驱动开发之 设备读写方式:直接方式

上一节介绍了缓冲区方式读写,这一节咱们来看看直接方式读写设备. 1. 直接方式读写设备,操作系统会将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍.这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存.无论操作系统如何切换进程,内核模式地址都保持不变. 创建好设备IoCreateDevice后,需要设置DO_DIRECT_IO,  pDevObj->Flags |= DO_DIRECT_IO. 2. 这里涉及到内存描述符表(MDL) MDL结构的声明如下

Windows驱动开发(二)

本节主要介绍驱动开发的一些基础知识. 1. 驱动程序的基本组成 1.1. 最经常见到的数据结构 a. DRIVER_OBJECT驱动对象 [cpp] view plaincopy // WDK中对驱动对象的定义 // 每个驱动程序都会有一个唯一的驱动对象与之对应 // 它是在驱动加载时被内核对象管理程序创建的 typedef struct _DRIVER_OBJECT { CSHORT Type; CSHORT Size; // // The following links all of the

Windows驱动开发(中间层)

Windows驱动开发 一.前言 依据<Windows内核安全与驱动开发>及MSDN等网络质料进行学习开发. 二.初步环境 1.下载安装WDK7.1.0(WinDDK\7600.16385.1) 地址:https://msdn.microsoft.com/en-us/windows/hardware/hh852365 2.下载InstDrv软件(用于安装.启动.停止.卸载驱动) 界面如下: 注:srvinstw.exe 也可以安装.卸载sys文件,但需要手动开启.关闭,即在cmd命令窗口下执行

Windows 驱动开发 - 1

上篇<Windows 驱动开发 - 基础2>已经介绍了windows大概的框架.那么使用WDF有那些的不同呢? 我们知道在WDF中,KMDF是必须的.而KMDF是架构在WDM之上的.  WDM驱动程序模型 在 WDM 驱动程序模型中,每个硬件设备至少有两个驱动程序.其中一个驱动程序我们称为功能(function)驱动程序,通常它就是你认为的那个硬件设备驱动程序.它了解使硬件工作的所有细节,负责初始化 I/O 操作,有责任处理 I/O 操作完成时所带来的中断事件,有责任为用户提供一种设备适合的控

Windows 驱动开发 - 5

上篇<Windows 驱动开发 - 4>我们已经完成了硬件准备.但是我们还没有具体的数据操作,例如接收读写操作. 在WDF中进行此类操作前需要进行设备的IO控制,已保持数据的完整性. 我们知道进行WDF开发主要是跟随IRPs的"脚步". I/O Request Delivery Mechanism I/O request type UMDF delivery mechanism KMDF delivery mechanism Read Queue Queue Write Q

Windows驱动开发(一)

笔者学习驱动编程是从两本书入门的.它们分别是<寒江独钓--内核安全编程>和<Windows驱动开发技术详解>.两本书分别从不同的角度介绍了驱动程序的制作方法. 在我理解,驱动程序可分为两类三种: 第一类:传统型驱动 传统型驱动的特点就是所有的IRP都需要自己去处理,自己实现针对不同IRP的派发函数.其可以分为以下两种: 1. Nt式驱动:此驱动通过注册系统服务来加载,并且不支持即插即用功能(即没有处理IRP_MJ_PNP这个IRP). 2. WDM驱动:此驱动不通过注册系统服务来加

Windows 驱动开发基础(六)NT驱动的基本结构

Windows 驱动开发基础系列,转载请标明出处:http://blog.csdn.net/ikerpeng/article/details/38821919 NT类型的驱动即不是即插即用的驱动,主要包括3部分:DriverEntry函数,CreateDevice函数,DriverUnload函数.其实还有IRP派遣函数,但是这里先不做介绍. 接下来详细的说明一下. 系统启动的时候,就创建了系统进程:驱动加载的时候,系统启动一个新的线程,创建一个驱动对象.而当系统线程调用DriverEntry函

【转】Windows驱动开发如何入门

1.http://blog.csdn.net/charlessimonyi/article/details/50904854 (2016年03月16日 14:55:36) 2. 搞Windows驱动开发是一件痛苦的事情,特别是初学Windows驱动开发.有的人觉得Windows驱动开发就是把开发包WDK下载下来,然后只要掌握了C/C++语言,接下来无非就是类库调来调去,像调用MFC.QT之类的库那样,看着书和MSDN上的文档来就行了.等真正接触以后才发现根本不是那么一回事,痛苦源于以下几点: 痛