Windows驱动开发:IRP、完成例程与 MDL 生命周期深度解析
本文系统总结 Windows WDM 驱动开发中IRP、IoCallDriver、IoCompleteRequest、CompletionRoutine、MDL 生命周期以及所有权管理问题。
重点分析:
- IRP 创建方式与释放责任
- CompletionRoutine 返回值语义
IoFreeIrp与 STATUS_MORE_PROCESSING_REQUIRED 的关系IoFreeMdl、MmBuildMdlForNonPagedPool、MmProbeAndLockPages的区别- TDI 场景中 PFN_LIST_CORRUPT (0x4E) 的根因分析
1. IRP
1.1 IRP 基本概念
IRP(I/O Request Packet)是 Windows I/O Manager 描述一次 I/O 操作的核心结构。
典型流程:
上层驱动
|
v
IRP 创建
|
v
IoCallDriver
|
v
下层驱动处理
|
v
IoCompleteRequest
|
v
CompletionRoutine
|
v
I/O Manager 后续处理
|
v
IRP 释放
核心原则:
IRP 的释放责任取决于创建方式以及是否由 CompletionRoutine
接管生命周期。
不能简单认为所有 IRP 都由创建者调用 IoFreeIrp。
1.2 一般IRP
对于IoAllocateIrp或者IoBuildAsynchronousFsdRequest创建的必须提供 IoCompletion 例程的 IRP。
例如:
c
PIRP Irp = IoAllocateIrp(StackSize, FALSE);
IoCallDriver(DeviceObject, Irp);
特点:
- 必须为创建的 IRP 设置 IoCompletion 例程。
- IoCompletion 例程也必须调用 IoFreeIrp 来释放该 IRP。
- 必须使用 STATUS_MORE_PROCESSING_REQUIRED 返回控制权,阻止 I/O 管理器对 IRP 进行完成处理。
这里存在一个疑问,为什么必须提供 IoCompletion 例程?
从上文中可以得出结论, IoCompletion 例程的作用是为了由驱动自身管理和释放 IRP;
那么不使用完成例程,或者完成例程中不调用 IoFreeIrp 直接返回 STATUS_CONTINUE_COMPLETION,完全由 I/O 管理器对 IRP 进行完成处理存在什么问题?
参考链接: IoAllocateIrp
参考链接: Implementing an IoCompletion Routine
1.3 同步IRP
对于IoBuildSynchronousFsdRequest或者IoBuildDeviceIoControlRequest创建的用于同步处理的 I/O 请求的 IRP。
特点:
- 与当前线程关联
- I/O Manager 负责后续释放
- 通常不能直接 IoFreeIrp
因此:
IoAllocateIrp
|
v
驱动负责释放
IoBuildDeviceIoControlRequest
|
v
I/O Manager 负责释放
两者生命周期不能混用。
2. MDL
2.1 MDL作用
MDL 描述虚拟地址对应的物理页:
Virtual Address
|
v
MDL
|
v
Physical Pages
IRP 中:
c
Irp->MdlAddress
保存 MDL 链。
2.2 IoAllocateMdl
IoAllocateMdl 只负责创建 MDL:
c
PMDL Mdl = IoAllocateMdl(
Buffer,
Length,
FALSE,
FALSE,
NULL
);
它不会:
- 锁定页面
- 建立 PFN 信息
后续必须根据 Buffer 类型处理。
2.3 MmBuildMdlForNonPagedPool
用于:
NonPaged Pool
|
v
MmBuildMdlForNonPagedPool
特点:
- 页面本身已经不会换出
- 不需要锁页
- 不允许调用 MmUnlockPages
生命周期:
IoAllocateMdl
|
MmBuildMdlForNonPagedPool
|
使用 MDL
|
IoFreeMdl
2.4 MmProbeAndLockPages
用于需要锁定的内存:
MmProbeAndLockPages
|
v
Pages Locked
|
v
使用
|
v
MmUnlockPages
规则:
Lock 次数 == Unlock 次数
否则可能触发:
PFN_LIST_CORRUPT (0x4E)
2.5 IoFreeMdl
IoFreeMdl:
- 释放 MDL 结构
- 不释放 Buffer
- 不自动释放锁定页面
例如:
c
IoFreeMdl(Mdl);
如果之前调用:
c
MmProbeAndLockPages()
需要:
c
MmUnlockPages(Mdl);
IoFreeMdl(Mdl);
3. Completion Routine
3.1 CompletionRoutine作用
CompletionRoutine 在下层驱动完成 IRP 后执行。
注册:
c
IoSetCompletionRoutine(
Irp,
CompletionRoutine,
Context,
TRUE,
TRUE,
TRUE
);
关键点:
- 完成例程运行在发起完成的那个线程上下文,不一定是原始发起 IRP 的线程;中断级别一般是 DISPATCH_LEVEL。
- 返回值决定 IRP 后续走向。
- IoSetCompletionRoutineEx 相比 IoSetCompletionRoutine,增加了引用计数,防止设备对象在完成例程执行前被卸载,更安全。
3.2 返回值含义
3.2.1 STATUS_CONTINUE_COMPLETION
表示:
继续向上传递完成
适用于:
- 不需要接管 IRP
- 不需要修改资源生命周期
3.2.2 STATUS_MORE_PROCESSING_REQUIRED
表示:
停止 I/O Manager 后续完成流程
当前驱动接管 IRP
常用于:
- 自己创建的 IRP
- 需要异步等待
- 需要自行释放资源
| IRP 类型 || MdlAddress != NULL | MdlAddress == NULL |
| 本地 IRP | 同步 IRP | 返回`STATUS_MORE_PROCESSING_REQUIRED`,在派遣线程或者完成例程中释放MDL和IRP; 也可以返回`STATUS_CONTINUE_COMPLETION`,但必须在完成例程中将MDL释放同时设置 MdlAddress = NULL | 正常情况下返回`STATUS_CONTINUE_COMPLETION`; 也可以返回`STATUS_MORE_PROCESSING_REQUIRED`,在派遣线程或者完成例程中释放IRP。 |
| 本地 IRP | 普通 IRP | 返回`STATUS_MORE_PROCESSING_REQUIRED`,在派遣线程或者完成例程中释放MDL和IRP。 | 返回`STATUS_MORE_PROCESSING_REQUIRED`,在派遣线程或者完成例程中释放IRP。 |
| 派遣 IRP | - | 返回`STATUS_CONTINUE_COMPLETION`即可。 |
|---|
4. TDI 驱动中 0x4E 解析
4.1 问题分析
示例代码:
c
// 初始化事件,创建IRP
KeInitializeEvent(&event,NotificationEvent,0);
newirp=TdiBuildInternalDeviceControlIrp(TDI_QUERY_INFORMATION,plowerdev,file,&event,&iosb);
// 分配内存,创建并设置 MDL
pooladdr=ExAllocatePool(NonPagedPool,4096);
pmdl=IoAllocateMdl(pooladdr,4096,0,0,0);
MmBuildMdlForNonPagedPool(pmdl);
// 设置IRP
TdiBuildQueryInformation(newirp,plowerdev,file,0,0,TDI_QUERY_ADDRESS_INFO,pmdl);
// 下层调用
stat=IoCallDriver(plowerdev,newirp);
if(stat==STATUS_PENDING){
KeWaitForSingleObject(&event, Executive,KernelMode,0,0);
}
stat=iosb.Status;
以上代码在xp上执行时会出现0x4e() BSOD。
text
PFN_LIST_CORRUPT (4e)
Typically caused by drivers passing bad memory descriptor lists (ie: calling
MmUnlockPages twice with the same list, etc). If a kernel debugger is
available get the stack trace.
Arguments:
Arg1: 00000007, A driver has unlocked a page more times than it locked it
Arg2: 000061fd, page frame number
Arg3: 00000002, current share count
Arg4: 00000000, 0
从蓝屏代码分析可能存在未知的MmUnlockPages调用,因此选择对函数断点,获得以下函数调用栈。
text
nt!MmUnlockPages
nt!IopfCompleteRequest
tcpip!TCPDataRequestComplete
tcpip!TCPQueryInformation
tcpip!TCPDispatchInternalDeviceControl
...
从调用栈中可以看到IopfCompleteRequest内部自动调用了MmUnlockPages,而MmBuildMdlForNonPagedPool构建的MDL是不允许调用MmUnlockPages的。
通过反汇编查看IopfCompleteRequest的实现,可以看到当IRP存在MDL时,会自动对所有MDL执行 MmUnlockPages。
c
for (i = Irp->MdlAddress; i; i = i->Next)
MmUnlockPages(i);
于是:
text
NonPaged Pool
│
▼
MmBuildMdlForNonPagedPool
│
▼
MDL 没有 PAGES_LOCKED
│
▼
Irp->MdlAddress = Mdl
│
▼
TCP/IP
│
▼
IoCompleteRequest
│
▼
IopfCompleteRequest
│
▼
MmUnlockPages(Mdl)
│
▼
PFN_LIST_CORRUPT
│
▼
0x4E / Arg1 = 7
Arg1 = 7 表示驱动对页面执行了超过对应 lock 次数的 unlock。
4.2 自己创建 IRP 的典型模式
概念代码:
c
PIRP Irp = IoAllocateIrp(
DeviceObject->StackSize,
FALSE
);
if (!Irp)
return STATUS_INSUFFICIENT_RESOURCES;
PMDL Mdl = IoAllocateMdl(
Buffer,
Length,
FALSE,
FALSE,
Irp
);
if (!Mdl)
{
IoFreeIrp(Irp);
return STATUS_INSUFFICIENT_RESOURCES;
}
MmBuildMdlForNonPagedPool(Mdl);
IoSetCompletionRoutine(
Irp,
CompletionRoutine,
Context,
TRUE,
TRUE,
TRUE
);
IoCallDriver(DeviceObject, Irp);
如果 CompletionRoutine 接管生命周期:
c
NTSTATUS
CompletionRoutine(
PDEVICE_OBJECT DeviceObject,
PIRP Irp,
PVOID Context
)
{
UNREFERENCED_PARAMETER(DeviceObject);
UNREFERENCED_PARAMETER(Context);
if (Irp->MdlAddress)
{
IoFreeMdl(Irp->MdlAddress);
Irp->MdlAddress = NULL;
}
IoFreeIrp(Irp);
return STATUS_MORE_PROCESSING_REQUIRED;
}
这里最重要的组合是:
text
IoFreeIrp()
+
STATUS_MORE_PROCESSING_REQUIRED