驱动程序在 DriverEntry 中完成初始化之后,主要工作就是处理请求了
7.1 IRP 简介
IRP 是一个结构,通常由执行体中的各种管理器之一从非分页内存中分配,驱动程序也可以分配,不过需要自己释放
IRP 总是会和一个或多个 IO 栈位置结构(IO_STACK_LOCATION
)一起分配,在内存中,IO 栈位置结构紧跟着 IRP 结构的后面,IO 栈位置结构的数量就是设备栈中设备对象的数量
这一组 IO 栈位置结构中,只有一个是给驱动程序使用的,通过 API IoGetCurrentIrpStackLocation
进行获取
7.2 设备节点
Windows 的 I/O 系统是以设备为中心而不是以驱动程序为中心的。
- 设备对象可以被命名,设备对象的句柄可以被打开。
- Windows 支持设备分层—一个设备可以层叠在另一个设备上面。
- 任何以下层设备为目标的请求都会先到达最上层。
- 一个设备“堆叠”在另一个上面。这组设备被称为设备栈,有时候会被称作设备节点。每个设备实际上是一个 DEVICE_OBJECT 结构,通过调用标准的 IoCreateDevice 函数创建。
设备栈可以与硬件相关也可以不相关
图中三种设备对象:
- PDO 物理设备对象:由负责特定总线的驱动程序创建(有某些设备位于该总线的设备槽里)
- FDO 功能设备对象:硬件厂商提供的能驱动硬件发挥功能的驱动程序创建的设备对象
- FiDO 过滤设备对象:过滤驱动创建的
设备对象创建流程:
- 当一个硬件插入总线某个设备槽中,总线驱动程序识别出来了该硬件,它创建一个 PDO,并从控制器中获取基本信息(设备的设备 ID,开发商 ID 等)并通知管理器
- 管理器向总线驱动请求 PDO 列表,向总线查询完整设备 ID,找到这个新的 PDO
- 管理器去注册表查询硬件 ID(
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\PCI\硬件 ID
),如果该驱动以前安装过了,就在这里进行注册,管理器装载该驱动程序 - 驱动程序装载并创建 FDO,这里会调用 IoAttachDeviceToDeviceStack,将 FDO 置于 PDO 之上
如果有过滤驱动的话,会自下而上的进行加载
硬件 ID 信息:
硬件 ID 信息里的 Service 是注册的驱动名,注册的驱动程序都会记录在注册表 计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\驱动名
中,这里会有驱动程序实际的映像地址,以便管理器将驱动进行加载
如果将一个可拔插设备(例如某种有 usb 接口的单片机)的驱动程序路径故意篡改成一个有问题的驱动的路径,当该设备插入此电脑后,就会自动加载问题驱动了,该想法待尝试,鉴于驱动签名好难搞,就算了
底层过滤器会在两个地方搜索:注册表查询硬件 ID 信息的那个位置的硬件 ID 键处及相应的类别处,该类别基于 HKLM\System\CurrentControlSet\Control\Classes
下面列出的 ClassGuid 值。值的名称是 LowerFilters,它是一个包含服务名称的多字符串值,指向同样的 Services 键。上层过滤器以类似的方式进行搜索,但是值的名称是 UpperFilters。
IRP 流程
IRP 会从最上层开始下发,每一层驱动都会接收到,当驱动收到 IRP 之后,可以做如下选择:
- 将请求向下传递(调用 IoSkipCurrentIrpStackLocation 确保下一个设备能看到同一个 IO 栈位置,调用 IoCallDriver 传递下层设备对象和 IRP。 )
- 自己完全处理此请求(调用 IoCompleteRequest)
- 处理请求之后再向下传递(1+2)
当驱动调用 IoCompleteRequest 完成请求处理之后,IRP 就会往上进行传递,通常传递到管理器那里。
7.3 IRP 和 IO 栈位置
IRP 结构重要字段:
1: kd> dt _IRP
ntdll!_IRP
+0x000 Type : Int2B
+0x002 Size : Uint2B
+0x004 AllocationProcessorNumber : Uint2B
+0x006 Reserved : Uint2B
+0x008 MdlAddress : Ptr64 _MDL // 指向可选的内存描述符列表,用于直接IO
+0x010 Flags : Uint4B
+0x018 AssociatedIrp : <anonymous-tag> // 一般指向系统分配的非分页池缓冲区
+0x020 ThreadListEntry : _LIST_ENTRY
+0x030 IoStatus : _IO_STATUS_BLOCK // 包括IRP状态和information,后者根据IRP类型而不同,对于读写IRP含义是读写的字节数
+0x040 RequestorMode : Char
+0x041 PendingReturned : UChar
+0x042 StackCount : Char
+0x043 CurrentLocation : Char
+0x044 Cancel : UChar
+0x045 CancelIrql : UChar
+0x046 ApcEnvironment : Char
+0x047 AllocationFlags : UChar
+0x048 UserIosb : Ptr64 _IO_STATUS_BLOCK
+0x050 UserEvent : Ptr64 _KEVENT // 指向事件对象的指针,一般由用户层再OVERLAPPED中提供
+0x058 Overlay : <anonymous-tag>
+0x068 CancelRoutine : Ptr64 void // 指向取消例程的指针,当用户层CancelIo时会调用
+0x070 UserBuffer : Ptr64 Void // 指向相关IRP的用户缓冲区
+0x078 Tail : <anonymous-tag>
IO_STACK_LOCATION 结构的重要字段:
1: kd> dt _IO_STACK_LOCATION
ntdll!_IO_STACK_LOCATION
+0x000 MajorFunction : UChar // IRP主功能代码
+0x001 MinorFunction : UChar // 有些IRP有次功能代码,一般用不到
+0x002 Flags : UChar
+0x003 Control : UChar
+0x008 Parameters : <anonymous-tag> // 不同操作有不同的结构
+0x028 DeviceObject : Ptr64 _DEVICE_OBJECT // 与IRP关联的设备对象,IRP例程一般也会有指向设备对象的指针
+0x030 FileObject : Ptr64 _FILE_OBJECT // 与IRP关联的文件对象,多数情况不需要用
+0x038 CompletionRoutine : Ptr64 long // 完成例程,为上一层而设置
+0x040 Context : Ptr64 Void // 传递给完成例程的参数
查看 IRP 信息
windbg 搜索 IRP:
!irpfind
windbg 获取特定 IRP 结构:会列出 IO 栈信息和保存其中的信息
!irp irpAddress
7.4 分发例程
分发例程是通过主功能代码连接起来的函数,所有分发例程都有相同的原型
基本上都遵循一组特定的操作:
- 检查错误,如果有错误,立马将 IRP 以特定的状态完成
- 处理请求
常见 IRP 处理请求:
IRP 类型 | 用户模式请求 | 内核模式请求 |
---|---|---|
IRP_MJ_CREATE | CreateFile | ZwCreateFile |
IRP_MJ_CLOSE | CloseHandle | ZwClose |
IRP_MJ_READ | ReadFile | ZwReadFile |
IRP_MJ_WRITE | WriteFile | ZwWriteFile |
IRP_MJ_DEVICE_CONTROL | DeviceIoControl | ZwDeviceIoControlFile |
IRP_MJ_INTERNAL_DEVICE_CONTROL | 无 | ZwDeviceIoControlFile |
完成请求
IRP 请求处理完一定要完成,否则会导致句柄泄露,产生僵尸进程
通过 IofCompleteRequest 完成请求
void IofCompleteRequest(
PIRP Irp,
CCHAR PriorityBoost
);
第二个参数的含义是提升线程的优先级,一般不需要提升,使用 IO_NO_INCREMENT
首先 IRP 需要将 IoStatus.status 进行设置,分发例程的返回值必须和这个状态一致
当有错误时,IoStatus.information 必须设置为 0
7.5 访问用户缓冲区
一些分发例程接收客户提供的缓冲区,通常分发例程再 IRQL0 和请求线程上下文中被调用,为了正确访问用户提供的缓冲区,内核提供了两种途径:缓冲 IO 和直接 IO
缓冲 IO
通过设置设备对象的标志来使用该种 IO:
DeviceObject->Flags |= DO_BUFFERED_IO;
该设置只影响读写请求,缓冲 IO 的工作流程:
- IO 管理器在系统空间从非分页池中分配一个跟用户缓冲区一样大小的缓冲区,保存到 IRP 中的 AssociatedIrp->SystemBuffer 中,长度为 IO 栈中 Parameters.Read.Length 或者 Parameters.Write.Length
- 对于写请求,IO 管理器将用户缓冲区内容复制到系统缓冲区
- 调用驱动的分发例程(此时系统缓冲区指针可以随便用,因为其在系统空间,是非分页内存)
- 完成 IRP 之后,对于读请求,IO 管理器把系统缓冲区复制回用户缓冲区
- IO 管理器在 IofCompleteRequest 中通过向线程中插入特殊内核 APC 来实现复制
- IO 管理器释放缓冲区
缓冲 IO 特点:使用简单,涉及复制(适用于小缓冲区)
直接 IO
通过设置设备对象的标志来使用该种 IO:
DeviceObject->Flags |= DO_DIRECT_IO;
该设置也只影响读写请求,直接 IO 的工作流程:
- IO 管理器先确认用户缓冲区是合法的,然后利用页错将其装入物理内存
- IO 管理器将缓冲区锁定在内存,在另行通知之前不会被换出,也就变得像非分页内存了,在任何 IRQL 都能进行访问
- IO 管理器构造内存描述符表 MDL,是将缓冲区映射到物理地址的结构,保存在 IRP 的 MdlAddress 中
- 调用驱动分发例程,驱动里需要将缓冲区 mdl 再映射到系统空间才能使用
- 通过 API MmGetSystemAddressForMdlSafe 完成映射,将 IO 管理器构造的 MDL 传入,返回系统地址
- 驱动程序完成 IRP 之后,IO 管理器移除到系统地址的映射,释放 MDL,并将用户缓冲区解锁,让缓冲区恢复正常
PVOID MmGetSystemAddressForMdlSafe(
[in] PMDL Mdl,
[in] ULONG Priority // 一般使用NormalPagePriority
);
第二个参数是页面优先级,与系统内存不足时是否进行映射有关,一般使用 NormalPagePriority
函数失败返回 NULL,表明系统剩余页表不足,必须用 STATUS_INSUFFICIENT_RESOURCES 这个状态完成 IRP
不要用 MmGetSystemAddressForMdl 这个 API,因为失败了会蓝屏
当不进行 IO 方式的指定时,驱动程序不会得到 IO 管理器的任何帮助
IRP_MJ_DEVICE_CONTROL 的用户缓冲区
IRP_MJ_DEVICE_CONTROL 缓冲区访问方式是基于控制代码的,系统提供的 CTL_CODE 宏:
#define CTL_CODE(DeviceType,Function,Method,Access)(\
((DeviceType) << 16) | ((Access)<<14) | ((Function) << 2) | Method)
#define IOCTL_PRIORITY_BOOSTER_SET_PRIORITY CTL_CODE(0x8000,0x800,METHOD_NEITHER,FILE_ANY_ACCESS)
宏里最重要的部分--Method,表示缓冲区访问方式:
7.6 汇总:Zero 驱动程序
这里的收获有:
- 使用 do-while(false)代替 goto 来实现跳出代码块
- 使用预编译文件加快编译速度