IRP的深入理解(持续更新)
IRP的深入理解
IRP是什么
从概念上讲IRP类似于windows应用程序的消息。我们知道在windows中应用程序是由消息驱动的。
IRP的全名是I/O Request Package,即输入输出请求包,它是Windows内核中的一种非常重要的数据结构。上层应用程序与底层驱动程序通信时,应用程序会发出I/O请求,操作系统将相应的I/O请求转换成相应的IRP,不同的IRP会根据类型被分派到不同的派遣例程中进行处理。
IRQL等级
在写驱动(软件)我们能接触到的的IRQL等级只有三种
1.无中断:PASSIVE_LEVEL(0)
:
在此级别,所有中断都可以被处理,没有任何屏蔽。线程可以被调度和抢占,因此用户模式代码大部分时间都运行在这个级别。在此级别下可以执行各种内核 API 和用户模式操作,比如文件 I/O、内存分配、访问分页内存、使用等待和同步对象等。
DriverEntry、 AddDevice、 Reinitialize 和 Unload 例程也PASSIVE_LEVEL运行,驱动程序创建的任何系统线程也是如此。
2.软中断:
APC_LEVEL(1)
与PASSIVE_LEVEL(0)
的唯一区别: APC_LEVEL与PASSIVE_LEVEL的唯一区别是,在APC_LEVEL执行的进程无法获得 APC 中断。 但两个 IRQL 都表示线程上下文,两者都意味着代码可以分页。
DISPATCH_LEVEL(2)
DISPATCH_LEVEL
用于调度时钟中断、调度线程执行、DPC(延迟过程调用)等的 IRQL 级别。代码会屏蔽 APC 和其他低级别的中断,但不会屏蔽更高级别的硬件中断。 不能进行等待操作,也不能访问分页内存。 适合时间敏感的短期操作,以避免线程调度或上下文切换。
典型应用:常用于 DPC(Delayed Procedure Call) 和调度器的代码执行,例如驱动程序处理 I/O 完成时的一些清理和后续处理会通过 DPC 在 DISPATCH_LEVEL
上执行
调用源 | 一般的运行中断级别 |
---|---|
DriverEntry,DriverUnload | Passive |
各种IRP分发函数 | Passive |
完成函数 | Dispatch |
各种NDIS回调函数 | Dispatch |
另外RtlCopyUnicodeString
虽然说是any level,但是它的Destination和SourceString必须是常驻内存,否则蓝屏。
还有一个场景:
在写键盘过滤驱动的时候,如果我们要拿键盘扫描码,但是因为在完成例程函数是运行在Dispatch level
上,我们无法直接在完成例程函数直接把扫描码写入到一个内存(分页内存),那么解决方法可以是:
1.写入非分页内存
在 DISPATCH_LEVEL
上不能访问分页内存,因为这种内存可能会被换出到磁盘,导致访问超时或系统崩溃。写入非分页内存可以解决这个问题,因为非分页内存始终驻留在物理内存中。 1.写入非分页内存,2.开一个工作线程,3.return STATUS_MORE_PROCESSING_REQUIRED ,在IRP分发函数处理
2.开一个工作线程
工作线程运行在 PASSIVE_LEVEL
,因此可以安全地访问分页内存。在完成例程中,可以将扫描码存储在临时的非分页缓冲区,然后在工作线程中处理,或将其复制到分页内存中。
示例代码:
1 |
|
返回 STATUS_MORE_PROCESSING_REQUIRED
,在 IRP 分发函数中处理
STATUS_MORE_PROCESSING_REQUIRED
会使完成例程不继续完成 IRP,控制权返回给分发函数。可以在分发函数中完成对扫描码的处理,然后手动完成 IRP。
PENING的情况
参考文章 总结:使用IoMarkPending的原因及原理_iomarkirppending-CSDN博客
当驱动程序返回 STATUS_PENDING
时,系统做的不仅仅是设置一个标志,它还会改变 IRP 的处理流程。 更确切地说,系统会暂停对该 IRP 的处理,并将其置于一个等待完成的状态。在驱动程序返回STATUS_PENDING
之前,一般我们会去单独起一个线程,或者DCP例程去异步的完成IRP(直到调用IoCompleteRequest)
再细究一下说一下IoMarkPending
什么时候用
根据微软的官方文档,它的作用是这样的:
IoMarkIrpPending 例程标记指定的 IRP,指示驱动程序的调度例程随后返回STATUS_PENDING,因为其他驱动程序例程需要进一步处理。
除非驱动程序的调度例程通过调用 (IoCompleteRequest) 完成 IRP(或将 IRP 传递给较低的驱动程序,否则它必须使用 IRP 调用 IoMarkIrpPending 。 否则,一旦调度例程返回控制权,I/O 管理器就会尝试完成 IRP。
注意注意,调用完IoMarkIrpPending,驱动程序一定要返回STATUS_PENDING
即使某些例程通过在调用 IoMarkIrpPending 的调度例程返回之前调用 IoCompleteRequest完成 IRP
如果没有调用IoMarkIrpPending
,而只返回STATUS_PENDING
, IRP 可能会取消排队,由另一个驱动程序例程完成,并在调用 IoMarkIrpPending 之前由系统释放,从而导致崩溃。
调用IoMarkPending
的作用是设置当前堆栈的SL_PENDING_RETURNED
标志 ,这往往在最底层的驱动被设置,当此IRP被完成之后(IoCompleteRequest),就会启动完成例程函数。
我们经常在完成例程函数里面看到这段代码:
1 | if (Irp->PendingReturned) |
这是为了设置当前堆栈的SL_PENDING_RETURNED
标志位,那不禁让人有了疑问,这玩意不是已经在IRP处理例程设置过了吗?在完成例程函数为什么也要设置嘞??
这是查阅到的资料
某个能处理IRP的派遣例程返回STATUS_PENDING前会首先调用IoMarkIrpPending函数在当前的堆栈单元中设置一个名为SL_PENDING_RETURNED的标志;当被推迟的操作在将来某个时候结束时,该派遣例程中的IoCompleteRequest函数会检查到SL_PENDING_RETURNED被设置,同时它会检查上层驱动是否安装了完成例程,若未设置,则它会把当前堆栈单元的SL_PENDING_RETURNED标志复制到上一层堆栈单元中,若设置了,则在调用完成例程前它不会把当前堆栈单元的SL_PENDING_RETURNED标志复制到上一层堆栈单元中,而仅仅在IRP中设置PendingReturned标志为SL_PENDING_RETURNED标志(Windows NT就是这么设计的…),因此上层驱动的完成例程就有义务在自己的堆栈单元中设置SL_PENDING_RETURNED标志。
过程是从堆栈下层开始逐步向上层执行的,随着SL_PENDING_RETURNED逐步向上层堆栈传递,最终最顶层堆栈单元中的SL_PENDING_RETURNED标志会被设置,从而可以保证IoCompleteRequest函数会调度APC来使IRP发起者等待的事件变为通知状态。
调度APC的作用:该APC会做很多清理工作,比如调用IoFreeIrp释放IRP、调用KeSetEvent使IRP发起者等待的事件为通知状态等等。
所以我的理解是,需要释放掉每一层的IRP堆栈,因为如果是挂起状态(返回STATUS_PENDING),IO设备管理器不会自动释放,除非检测到又被设置过SL_PENDING_RETURNED
标志位,所以我们要确保SL_PENDING_RETURNED
这个标志位能层层上传
关于完成例程函数
参考文章: 完成例程-CSDN博客
触发条件
只有当前IRP请求被调用了IoCompleteRequest
,完成例程函数才会被触发,和驱动的返回值没有关系(管它返回STATUS_SUCCESS
还是什么东东)
STATUS_MORE_PROCESSING_REQUIRED
当IRP被IoCompleteRequest完成时,IRP就会沿着一-层层的设 备堆栈向上回卷。如果途经遇到某设备堆栈的完成例程,则进入该完成例程。完成例程如果返回STATUS_MORE PROCESSING_ REQUIRED,则停止向上回卷。这时的本层堆栈又重新获得IRP的控制,并且该IRP 从完成状态又变成了未完成状态,需要再次完成,即需要再次执行IoCompleteRequest。
以下给出了使用完成例程的例子,该完成例程返回STATUS_MORE_PROCESSING_REQUIRED。在调用loCallDriver 之后,当前设备想等待底层驱动将设备完成后再继续执行。因此,loCallDriver 初始化了个事件,并将事件指针传递给了完成例程。IRP 被完成后进入完成例程,并触发事件。这种技巧被广泛应用于驱动程序的编写中,代码如下:
1 | NTSTATUS |