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

相关推荐
ITHAOGE1530 分钟前
下载 | Win11 26H2 正式版发布!(ISO映像、2026年度版本、26300.9457、Windows 11)
windows·科技·微软·电脑
YCOSA20252 小时前
雨晨 Windows 11 IoT 企业版 Edge 轻装 VCDX 29680.1000
windows·物联网
笨笨努力中3 小时前
2002年Windows XP系统下载视频资源迁移云盘
windows
l1t4 小时前
修复WSL CreateInstance/E_UNEXPECTED和 mounted read-only 错误
linux·windows·wsl
kakakahahahaha5 小时前
Windows电脑卡死恢复流程:任务管理器、Ctrl Alt Delete、explorer与DWM重启
windows·电脑·笔记本电脑·软件需求·电脑卡死
kakakahahahaha5 小时前
Windows鼠标失灵排查:Ctrl Alt Delete、Mouse Keys、USB控制器与驱动修复
windows·驱动开发·电脑·笔记本电脑·软件需求
Vex2une6 小时前
Windows 资源监视器发展史:从任务管理器附件到系统级诊断利器
windows·系统监控·任务管理器·诊断工具·etw·resmon·资源监视器
Vex2une6 小时前
Windows 事件查看器发展史:从 NT 3.1 黑匣子到企业级安全日志平台
windows·事件日志·事件查看器·event viewer·etw·eventvwr·系统诊断
sukalot7 小时前
Windows 驱动实例分析系列:libwdi 驱动分析 - libwdi 篇(七)
windows
傲世仙尊7 小时前
TCP报头全解-序号确认应答与可靠性是一个准数
驱动开发·网络协议·tcp/ip