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

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

本文系统总结 Windows WDM 驱动开发中IRP、IoCallDriver、IoCompleteRequest、CompletionRoutine、MDL 生命周期以及所有权管理问题。

重点分析:

  • IRP 创建方式与释放责任
  • CompletionRoutine 返回值语义
  • IoFreeIrp 与 STATUS_MORE_PROCESSING_REQUIRED 的关系
  • IoFreeMdlMmBuildMdlForNonPagedPoolMmProbeAndLockPages 的区别
  • 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

相关推荐
百事牛科技1 小时前
只可读不可改!PDF禁止修改实操方法
windows·pdf
老王的笔记v2 小时前
42期 鼠鼠格式转换工具整合图片文档音视频转换,Windows和Mac免费使用
windows·开源·github·音视频
可爱系程序猿2 小时前
Windows 11 添加共享打印机提示 0x0000011b:从 RPC 到驱动架构排查
windows·rpc·架构
渣渣盟2 小时前
Nginx 从零到上手:Windows & Linux 双环境教程
linux·windows·nginx
魂祈梦2 小时前
windows终端乱码问题
windows
0566462 小时前
python高级——Python 类型提示与 Pydantic
网络·人工智能·windows·python·学习
小桥流水---人工智能3 小时前
Windows下Conda无法创建Python 3.11虚拟环境:从镜像源404到PackagesNotFoundError的完整排查与解决
windows·conda·python3.11
sukalot3 小时前
windows 驱动实例分析系列: wireguard-nt驱动分析-api篇(二)
windows
MNLoser4 小时前
AI agent开发——LangGraph接入持久化
linux·人工智能·windows·python