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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <ntddk.h>

typedef struct _WORK_ITEM_CONTEXT {
PVOID DataBuffer; // 用于传递数据的分页内存指针
ULONG DataSize;
} WORK_ITEM_CONTEXT, *PWORK_ITEM_CONTEXT;

VOID
WorkerThreadRoutine(
_In_ PVOID Context
)
{
PWORK_ITEM_CONTEXT workItemContext = (PWORK_ITEM_CONTEXT)Context;

// 在 PASSIVE_LEVEL 运行的工作线程中,可以安全地访问分页内存
if (workItemContext && workItemContext->DataBuffer) {
// 处理扫描码或其他数据
DbgPrint("Processing data in worker thread...\n");

// 假设我们只是简单打印出数据
for (ULONG i = 0; i < workItemContext->DataSize; i++) {
DbgPrint("Data[%lu]: %x\n", i, ((PUCHAR)workItemContext->DataBuffer)[i]);
}

// 完成后释放内存
ExFreePool(workItemContext->DataBuffer);
ExFreePool(workItemContext);
}

PsTerminateSystemThread(STATUS_SUCCESS); // 终止工作线程
}

NTSTATUS
MyCompletionRoutine(
_In_ PDEVICE_OBJECT DeviceObject,
_In_ PIRP Irp,
_In_opt_ PVOID Context
)
{
// 分配一个工作项上下文
PWORK_ITEM_CONTEXT workItemContext = (PWORK_ITEM_CONTEXT)ExAllocatePool(NonPagedPool, sizeof(WORK_ITEM_CONTEXT));
if (!workItemContext) {
return STATUS_INSUFFICIENT_RESOURCES;
}

// 将数据复制到非分页内存以供工作线程访问
workItemContext->DataSize = 10; // 假设数据大小为10
workItemContext->DataBuffer = ExAllocatePool(PagedPool, workItemContext->DataSize);

if (!workItemContext->DataBuffer) {
ExFreePool(workItemContext);
return STATUS_INSUFFICIENT_RESOURCES;
}

// 假设在此处将扫描码数据复制到分页内存中
RtlCopyMemory(workItemContext->DataBuffer, Irp->AssociatedIrp.SystemBuffer, workItemContext->DataSize);

// 创建工作线程
HANDLE threadHandle;
NTSTATUS status = PsCreateSystemThread(&threadHandle, THREAD_ALL_ACCESS, NULL, NULL, NULL, WorkerThreadRoutine, workItemContext);
if (!NT_SUCCESS(status)) {
ExFreePool(workItemContext->DataBuffer);
ExFreePool(workItemContext);
return status;
}

// 关闭线程句柄,线程会自行清理
ZwClose(threadHandle);

// 返回 STATUS_MORE_PROCESSING_REQUIRED 来避免 IRP 被系统自动完成
return STATUS_MORE_PROCESSING_REQUIRED;
}

返回 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
2
if (Irp->PendingReturned)
IoMarkIrpPending(Irp);

这是为了设置当前堆栈的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
NTSTATUS
MyIoCompletion(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context
)
{

if (Irp->PendingReturned == TRUE)
{
//设置事件
KeSetEvent((PKEVENT)Context,IO_NO_INCREMENT,FALSE);
}

return STATUS_MORE_PROCESSING_REQUIRED;
}

#pragma PAGEDCODE
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
KdPrint(("DriverB:Enter B HelloDDKRead\n"));
NTSTATUS ntStatus = STATUS_SUCCESS;
//将自己完成IRP,改成由底层驱动负责

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;

//本层堆栈也可能会有一个完成函数,如果直接使用本层堆栈的话,设置完成函数的时候回覆盖掉.所以都是先把堆栈复制到下一层,然后设置完成函数到下一层堆栈中
IoCopyCurrentIrpStackLocationToNext(pIrp);

KEVENT event;
//初始化事件
KeInitializeEvent(&event, NotificationEvent, FALSE);

//设置完成例程 最后三个参数指明了 在 成功 发生错误 或者取消的情况下都会调用完成例程.
IoSetCompletionRoutine(pIrp,MyIoCompletion,&event,TRUE,TRUE,TRUE);

//调用底层驱动
ntStatus = IoCallDriver(pdx->TargetDevice, pIrp);

if (ntStatus == STATUS_PENDING)
{
KdPrint(("IoCallDriver return STATUS_PENDING,Waiting ...\n"));
KeWaitForSingleObject(&event,Executive,KernelMode ,FALSE,NULL);
ntStatus = pIrp->IoStatus.Status;
}

//虽然在底层驱动已经将IRP完成了,但是由于完成例程返回的是
//STATUS_MORE_PROCESSING_REQUIRED,因此需要再次调用IoCompleteRequest!
IoCompleteRequest (pIrp, IO_NO_INCREMENT);

KdPrint(("DriverB:Leave B HelloDDKRead\n"));

return ntStatus;
}