Windows驱动开发:IRP、完成例程与 MDL 生命周期深度解析

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
);

关键点:

  1. 完成例程运行在发起完成的那个线程上下文,不一定是原始发起 IRP 的线程;中断级别一般是 DISPATCH_LEVEL。
  2. 返回值决定 IRP 后续走向。
  3. 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

相关推荐
Sakura_j_h_3 小时前
Python 在定时任务里输出 emoji,UnicodeEncodeError 从哪来
开发语言·字符编码·windows·python·定时任务
隔窗听雨眠6 小时前
PostgreSQL单表从Windows迁移到Linux:差异剖析、方案对比与全流程实战
linux·windows·postgresql
Helix2509 小时前
Ansible混合环境自动化:Windows与Linux统一配置管理实战
windows·自动化·ansible·playbook·配置管理·winrm·混合环境
Java开发追求者10 小时前
windows快速安装docker
windows·docker·安装·1024程序员节
Helix25013 小时前
Windows ETW与KQL实战:终端性能诊断与遥测数据分析指南
windows·性能诊断·kql·etw·azure monitor·遥测数据·tracert
陈葛杰15 小时前
Adobe Premiere Pro2025安装教程
windows·adobe·开源软件·1024程序员节
承渊政道16 小时前
iOS远程控制电脑用什么好?iPhoneiPad远程控制Windows电脑最佳方案2026横评
windows·ios·远程控制·1024程序员节·iphone ipad
易点云徐源16 小时前
PDF 默认打开方式刚修好,过一分钟又变成“未知应用”?使用 ProcMon 成功抓出后台程序
windows·pdf
我是大猴子17 小时前
py how claude work项目分析
运维·服务器·windows·1024程序员节
软件工程师文艺1 天前
AI 编程新范式:Spec Kit 的规格驱动开发到底在解决什么
人工智能·驱动开发