Windows驱动开发技术详解第七章(派遣函数)

本文详细阐述了驱动程序如何处理输入输出请求包(IRP),包括IRP的结构、注册与派遣,以及IRP在文件IO、设备访问和DeviceIoControl中的作用。讲解了缓冲区读写、直接内存模式和不同内存模式的IOCTL操作,以及如何在驱动程序中正确处理和传递这些请求。
  1. 驱动程序的主要功能是负责处理IO请求,绝大部分IO请求在派遣函数中完成
  2. 用户模式下所有对驱动程序的IO请求,全部由操作系统转化为一个叫IRP的数据结构
  3. 不同的IRP数据会被派遣到不同的派遣函数中

IRP

全名:输入输出请求包
两个基本属性:MajorFunction(IRP主类型)和MinorFunction(IRP子类型)
操作系统根据MajorFunction将IRP派遣到不同的派遣函数中

IRP派遣函数的注册

一般来说,NT式和WDM式驱动程序都是在DriverEntry函数中注册的派遣函数
示例代码:
在这里插入图片描述
DriverEntry函数的入参pDriverObject中,有个函数指针数组MajorFunction,该数组中的每个元素都记录着一个函数的地址。例子中只对四种类型的IRP设置了派遣函数,但IRP的类型并不止这四种,对于其他没有设置的IRP派遣函数,系统默认使用_IopInvalidDeviceRequest函数与之关联。在进入DriverEntry之前,操作系统会将_IopInvalidDeviceRequest填满整个MajorFunction数组

IRP与派遣函数的联系:
在这里插入图片描述

IRP的类型

IRP的概念与应用层中的消息类似
文件相关的应用层IO函数,CreateFile、ReadFile、WriteFile、CloseHandle等函数会使操作系统产生IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_CLOSE等不同的IRP,这些IRP会被传送到驱动程序的派遣函数中

文件相关的内核层IO函数,ZwCreateFile、ZwReadFile、ZwWriteFile、ZwClose等函数同样会创建相应的IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_CLOSE等IRP,并将IRP传送到相应驱动的派遣函数中

还有些IRP由系统组件所创建,如IRP_MJ_SHUTDOWN是在Windows的即插即用组件在即将关闭系统时发出的
在这里插入图片描述

对IRP的简单处理

大部分的IRP都源于文件IO处理win32API时,如CreateFile、ReadFile等
对这些IRP最简单的处理就是在相应的派遣函数中:

  1. 将IRP状态设置为成功,并填写操作了多少字节
  2. 调用IoCompleteRequest结束IRP请求
  3. 让派遣函数返回成功
    在这里插入图片描述
    将IRP的状态设置为STATUS_SUCCESS,此时发起IO请求的win32API(如WriteFile)会返回TRUE;
    如果设置为不成功,此时发起IO请求的win32API(如WriteFile)会返回FALSE,调用GatLastError时得到的错误代码会和IRP设置的状态相一致

派遣函数还需设置这个IRP请求操作了多少个字节,例子中设置为0.
如果ReadFile产生的IRP,这个字节数代表从设备读了多少个字节
如果WriteFile产生的IRP,这个字节数代表向设备写了多少个字节

通过IoCompleteRequest结束IRP请求

void IofCompleteRequest(
  PIRP  Irp,
  CCHAR PriorityBoost
);

IRP:代表需要被结束的IRP
PriorityBoost:代表线程恢复时的优先级别,一般设置为IO_NO_INCREMENT
在这里插入图片描述

win32文件API操作流程:ReadFile为例
  1. ReadFile调用ntdll中的NtReadFile(ReadFile是win32API,NtReadFile是NativeAPI)
  2. ntdll中的NtReadFile进入内核模式,并调用系统服务中的NtReadFile函数
  3. 系统服务函数NtReadFile创建IRP_MJ_WRITE类型的IRP,然后将这个IRP发送到某个驱动程序的派遣函数中。
  4. 然后系统服务函数NtReadFile去等待一个事件,此时当前线程进入睡眠状态,即堵塞住,Pending状态
  5. 派遣函数中,处理完毕后,一般会将IRP请求结束,结束IRP通过IofCompleteRequest函数,IofCompleteRequest内部会设置刚才等待的事件
  6. 睡眠的线程被恢复运行
通过设备的符号链接打开设备
  1. 每个设备都有设备名称(如\Device\MyDDKDevice),同时可通过IoCreateSymbolicLink创建符号链接
  2. 要想打开设备,必须知道设备的名称
  3. 设备的名称无法由应用层的程序查询到,只能由内核模式下的程序查询到
  4. 在应用层中可以通过设备的符号链接进行访问
  • 在内核模式下,符号链接以\??\或者\DosDevices\开头
  • 在用户模式下,符号链接以\\.\开头

举例:设备的符号链接在不同模式下的写法

内核模式用户模式
\??\MyDDKDevice\\.\MyDDKDevice
\DOsDevices\MyDDKDevice

在这里插入图片描述

IRP与驱动和设备的关系(IO_STACK_LOCATION结构)

驱动对象会创建一个个设备对象,并将这些设备对象叠成一个垂直的结构(设备栈)
IRP请求会被操作系统发送到设备栈的顶层
如果顶层的设备处理完了IRP(即调用IofCompleteRequest函数结束IRP),则此IRP终止,不会继续向下层设备发送
如果顶层的设备未处理完IRP(即未调用IofCompleteRequest函数结束IRP),则此IRP将被继续将下层设备转发

一个IRP可能被转发多次,为了记录IRP在每层设备中做的操作,IRP有一个IO_STACK_LOCATION数组,数组的元素数目应该大于IRP所穿越的设备数。
每个IO_STACK_LOCATION元素记录着对应设备中做的操作
可通过IoGetCurrentIrpStackLocation函数获得本层设备所对应的IO_STACK_LOCATION

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);

IO_STACK_LOCATION结构的MajorFunction子域记录着IRP的类型

可以在DriverEntry中将所有IRP都与同一个派遣函数关联,在派遣函数中通过调用IoGetCurrentIrpStackLocation获得设备对应的IO_STACK_LOCATION,然后再通过IO_STACK_LOCATION的MajorFunction成员,获得派遣函数入参IRP的类型

缓冲区方式读写操作设备

驱动程序所创建的设备一般共有三种读写方式:

  1. 缓冲区方式
  2. 直接方式
  3. 其他方式

驱动创建设备时,需要指定设备何种读写方式,即IoCreateDevice的Flag参数

Flag读写方式
DO_BUFFERED_IO缓冲区方式读写
DO_DIRECT_IO直接方式读写
0其他方式读写

在这里插入图片描述

读写操作一般由ReadFile和WriteFile发起,Write为例
WriteFile调用时,要求用户提供一段缓冲区,缓冲区内存放要写入的数据
然后操作系统将该缓冲区的地址传入驱动程序中
由于操作系统多线程机制,随时切换到其他线程,驱动程序再处理WriteFile调用时,拿到的地址是应用层的地址,而应用层的地址不同程序相互独立。驱动在处理此次调用时线程可能切换到另一个其他进程的线程了,此时将产生崩溃

问题解决:
使用缓冲区读写,操作系统将WriteFile调用时提供的内存复制到内核空间内,此时无论切换到哪个线程,内核空间中的地址始终是不变的。IRP派遣函数将对内核空间中的内存进行操作,而不是操作用户空间的内存
优点:比较简单的解决了将用户地址引入驱动的问题
缺点:需要在用户空间和内核空间中复制数据,影响运行效率
建议:少量内存操作时采用此种方法

  1. 以缓冲方式读写设备时,操作系统将WriteFile提供的用户模式内存复制到内核模式下,作为缓冲区,该地址由WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域记录
  2. 以缓冲方式读写设备时,操作系统会分配一段内核模式下的内存,该段内存的大小等于ReadFile或WriteFile指定的字节数,并且ReadFile或WriteFile创建的IRP的AssociatedIrp.SystemBuffer会记录这段内存地址,当IRP请求结束时,这段内存会被复制到ReadFile提供的缓冲区中
  3. 缓冲方式无论"读"还是"写"都会发生用户模式和内核模式地址的数据复制,复制过程由操作系统负责。用户模式地址由ReadFile或WriteFile提供,内核模式由操作系统负责分配和回收
  4. 在派遣函数中,可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile请求多少字节,通过IO_STACK_LOCATION中的Parameters.Write.Length子域知道WriteFile请求多少字节
  5. 然而ReadFile和WriteFile指定对设备操作多少字节,并不真正意味着操作了这么多字节,派遣函数中应当设置IRP的子域IoStatus.Information,这个子域记录设备实际操作了多少字节

缓冲区读写示例:
在这里插入图片描述

直接读写方式操作设备

创建完设备对象后,在设置对象属性时,设置为DO_DIRECT_IO
在这里插入图片描述
直接读写设备不会出现复制应用层内存至内核层,而是直接将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样用户模式和内核模式的缓冲区指向的是同一区域的物理内存

操作系统将用户模式的缓冲区锁定后,用内存描述符表(MDL数据结构)记录这段内存,
内存描述符表通过pIrp->MdlAddress获得
在这里插入图片描述

内存描述符表(MDL)

虚拟内存的大小存储在mdl->ByteCount里
虚拟内存的第一个页地址是mdl->StartVa
虚拟内存的首地址对于第一个页地址的偏移量是mdl->ByteOffset
因此,这段虚拟内存的首地址是:mdl->StartVa + mdl->ByteOffset
DDK提供宏方便获取数值
在这里插入图片描述

其他读写操作设备的方式

调用IoCreateDevice之后,对pDevObj->Flags既不设置DO_BUFFERED_IO也不设置DO_DIRECT_IO,此时采用的读写方式就是其他读写方式

在使用其他读写方式时,派遣函数直接读写应用程序提供的缓冲区地址,此时非常危险
只有驱动程序和应用程序运行在相同线程的上下文的情况下,才能使用此种方式

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

使用用户模式时,要小心用户模式传入的空指针和不可读写内存,需进行探测可读或可写

IO设备控制操作(DeviceIoControl)

应用程序除了用ReadFileWriteFile以外,还可通过DeviceIoControl操作设备
DeviceIoControl可用于应用程序和驱动进行通信

BOOL DeviceIoControl(
  [in]                HANDLE       hDevice,				//已经打开的设备
  [in]                DWORD        dwIoControlCode,		//控制码
  [in, optional]      LPVOID       lpInBuffer,			//输入缓冲区
  [in]                DWORD        nInBufferSize,		//输入缓冲区大小
  [out, optional]     LPVOID       lpOutBuffer,			//输出缓冲区
  [in]                DWORD        nOutBufferSize,		//输出缓冲区大小
  [out, optional]     LPDWORD      lpBytesReturned,		//实际返回的字节数
  [in, out, optional] LPOVERLAPPED lpOverlapped			//是否OVERLAP操作
);

其中,lpBytesReturned对应派遣函数中的IRP结构中的pIrp->IoStatus.Information
dwIoControlCode:IO控制码,也称IOCTL值,32位无符号整型,需符合DDK规定
在这里插入图片描述
DDK提供了一个宏CTL_CODE,便于生产IOCTL码

CTL_CODE(DeviceType, Function, Method, Access)

DeviceType:设备对象的类型,应该与IoCreateDevice时的类型相匹配,形如FILE_DEVICE_XX宏
Function:驱动程序定义好的IOCTL码,其中

  • 0x0000 ~ 0x7FFF 微软保留
  • 0x8000 ~ 0xFFFF 程序员自己定义

Method:操作模式

模式含义
METHOD_BUFFERED使用缓冲区方式操作
METHOD_IN_DIRECT使用直接写方式操作
METHOD_OUT_DIRECT使用直接读方式操作
METHOD_NEITHER使用其他方式操作

Access:访问权限,一般使用FILE_ANY_ACCESS

缓冲内存模式IOCTL

使用缓存内存模式的IOCTL时,需要在使用CTL_CODE宏定义IOCTL时,指定Method参数为METHOD_BUFFERED
在缓冲内存模式下,DeviceIoControl内部,用户提供的输入缓冲区内容被复制到IRP中的pIrp->AssociatedIrp.SystemBuffer内存地址,复制到字节数是由DeviceIoControl指定的输入字节数
派遣函数可以读取pIrp->AssociatedIrp.SystemBuffer内存地址,获得应用程序提供的缓冲区内容。另外,pIrp->AssociatedIrp.SystemBuffer的内存地址也作为输出缓冲区,由派遣函数写入。操作系统会将这个地址的数据再次复制到DeviceIoControl提供的输出缓冲区,复制的字节数由pIrp->IoStatus.Information决定,这个数值并且作为DeviceIoControl的第七个参数输出

派遣函数使用步骤:

  1. 通过IoGetCurrentIrpStackLocation函数得到当前IO堆栈
  2. 通过stack->Parameters.DeviceIoControl.InputBufferLength得到输入缓冲区大小
  3. 通过stack->Parameters.DeviceIoControl.OutputBufferLength得到输出缓冲区大小
  4. 最后通过stack->Parameters.DeviceIoControl.IoControlCode得到IOCTL
  5. 派遣函数中通过switch来处理不同的IOCTL

示例代码:
在这里插入图片描述

直接内存模式IOCTL

使用缓存内存模式的IOCTL时,需要在使用CTL_CODE宏定义IOCTL时,指定Method参数为METHOD_IN_DIRECT 或者METHOD_OUT_DIRECT

直接内存模式IOCTL输入缓冲区的内容被复制到Irp->AssosiatedIrp.SystemBuffer内存地址,复制的字节数按照DeviceIoControl指定的输入字节数。这个步骤和缓冲区模式IOCTL一样的

直接内存模式IOCTL输出缓冲区的处理和缓冲区模式IOCTL不同。
操作系统将DeviceIoControl指定的输出缓冲区锁定,然后在内核模式地址下重新映射一段地址,派遣函数中的pIrp->MdlAddress记录DeviceIoControl指定的输出缓冲区,派遣函数应当使用MmGetSystemAddressForMdlSafe将这段内存映射到内核模式下的内存地址

派遣函数使用步骤:

  1. 通过IoGetCurrentIrpStackLocation函数得到当前IO堆栈
  2. 通过stack->Parameters.DeviceIoControl.InputBufferLength得到输入缓冲区大小
  3. 通过stack->Parameters.DeviceIoControl.OutputBufferLength得到输出缓冲区大小
  4. 最后通过stack->Parameters.DeviceIoControl.IoControlCode得到IOCTL
  5. 派遣函数中通过switch来处理不同的IOCTL

METHOD_IN_DIRECT和METHOD_OUT_DIRECT的差别:
当以只读模式打开设备时,METHOD_IN_DIRECT将会成功,而METHOD_OUT_DIRECT将会失败。
当以读写模式打开设备时,METHOD_IN_DIRECT和METHOD_OUT_DIRECT都会执行成功

示例代码:
在这里插入图片描述

其他内存模式IOCTL

使用其他内存模式的IOCTL时,需要在使用CTL_CODE宏定义IOCTL时,指定Method参数为METHOD_NEITHER

其他内存模式IOCTL很少用到,使用时必须保证调用DeviceIoControl线程与派遣函数运行在同一线程上下文中

DeviceIoControl提供的输入缓冲区地址,派遣函数可以通过IO堆栈的stack->Parameters.DeviceIoControl.Type3InputBuffer得到。
DeviceIoControl提供的输出缓冲区地址,派遣函数可以通过pIrp->UserBuffer得到

派遣函数使用步骤:

  1. 通过IoGetCurrentIrpStackLocation函数得到当前IO堆栈
  2. 通过stack->Parameters.DeviceIoControl.InputBufferLength得到输入缓冲区大小
  3. 通过stack->Parameters.DeviceIoControl.OutputBufferLength得到输出缓冲区大小
  4. 最后通过stack->Parameters.DeviceIoControl.IoControlCode得到IOCTL
  5. 派遣函数中通过switch来处理不同的IOCTL

此种模式下,派遣函数最好对传入的用户模式地址进行判断

示例代码:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值