news 2026/10/19 2:26:31

Windows驱动开发:MDL读写机制与Direct I/O完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows驱动开发:MDL读写机制与Direct I/O完整实现

这块内容,适合那些已经在写基础 Windows 驱动、想进一步把“读和写”搞透彻的开发者。一个很常见的现象是:你在用户态打开一个设备句柄,ReadFile / WriteFile 传了几 MB 数据,结果驱动侧拿到用户缓冲区指针后要么直接访问失败,要么数据对不上。问题根源大多是缓冲区访问方式选错了,而 MDL(Memory Descriptor List,内存描述符列表)恰恰是解决这类问题的标准工具。这篇文章我就以“实现 MDL 驱动读写”为主线,把从 MDL 原理、相关 API 到驱动读写例程的完整写法,以及我踩过的坑都梳理清楚。

1. MDL驱动读写到底在解决什么问题

1.1 什么是MDL驱动读写

先把这个名词拆开理解。MDL 是 Windows 内核里用来描述一块虚拟内存对应的物理内存页集合的数据结构。它记录了起始虚拟地址、长度、所属进程、物理页面数组等信息。所谓“MDL 驱动读写”,指的是驱动在处理 IRP_MJ_READ 和 IRP_MJ_WRITE 的过程中,通过 I/O 管理器传递过来的 MDL,或者自己手动构造的 MDL,来完成用户态缓冲区与内核态缓冲区之间的数据传输。

这里要强调一点:我们不是要绕过什么系统机制,而是要“正确地”读写用户态缓冲区。如果驱动在 IRP 派发函数里直接拿用户态虚拟地址去访问,当进程上下文切换、页面被换出、或者缓冲区被分页时,就会出现不可预期的崩溃。MDL 的价值就在于,它先把用户态缓冲区锁在物理内存里,然后建立一个内核地址映射,驱动在映射后的地址上做读写,安全且高效。

1.2 为什么MDL比直接访问用户缓冲区安全

我在刚接触驱动开发时也犯过“直接访问”的错误。比如用户态传一个字符串指针给驱动,驱动直接 RtlCopyMemory 过去,运气好时能读到,运气不好就是一个蓝屏 0x50。原因很简单:用户态虚拟地址只有在当前进程上下文里才有意义。IRP 到达驱动时,对应的用户线程可能已经被调度出去,或者缓冲区所在页面还没有在物理内存中(被换出到页面文件),又或者页面权限不允许写,这些都会导致非法访问。

而 MDL 方案从设计上规避了这类问题。内核首先把用户地址范围锁定(ProbeAndLock),保证被锁定的页面常驻物理内存;随后通过映射(MapLockedPages)得到一个系统内存地址,驱动只操作这个系统地址。整个过程由内存管理器统一管理,用户缓冲区既不会在 I/O 过程中被换出,也不会因为进程切换而失效。

打个比方,直接把用户指针拿来用,就像街道上没有红绿灯,车辆横冲直撞;用 MDL,等于先修了一条从用户缓冲区到内核缓冲区的专用公路,并且把这段路封起来了,只有你的驱动能“通行”。

1.3 三种I/O模式与MDL出现的时机

Windows 驱动里常见的 I/O 模式有三种,搞清楚它们能避免 90% 的缓冲区访问错误。

I/O 模式设备对象标志缓冲区获取方式数据拷贝开销适用场景
Buffered I/ODO_BUFFERED_IOIrp->AssociatedIrp.SystemBuffer有,需要系统拷贝小型控制命令
Direct I/ODO_DIRECT_IOIrp->MdlAddress无系统拷贝,但需映射大数据块读写
Neither I/O两者都未设置Type3InputBuffer或跳过 UserBuffer由驱动自行处理特殊协议或需要灵活性

Direct I/O 模式下,I/O 管理器会自动把用户缓冲区锁页并构造 MDL,挂到 IRP 的 MdlAddress 字段上。这就是“MDL 驱动读写”最常用的场景。Buffered I/O 虽然写代码简单,但进行大块数据拷贝时,额外多了一次内存拷贝,性能会明显下降。Neither I/O 灵活性最高,但责任也最大,驱动必须自己完成校验和锁页,否则风险极高。

如果你设计的设备主要用于文件传输、批量数据处理,Direct I/O 配合 MDL 是标准方案。如果你的驱动还提供一些 IOCTL 控制命令,那么这些命令通常走 Buffered I/O,而读写走 Direct I/O,两者并不冲突,设备对象可以同时支持。

2. 核心API原理解析

2.1 MDL结构

MDL 的结构定义在 wdm.h 中,关键是这几个字段:

  • Next:指向下一个 MDL,链表结构
  • Size:当前 MDL 结构体总大小,包括后面跟随的物理页数组
  • MdlFlags:标志位,比如 MDL_MAPPED_TO_SYSTEM_VA
  • Process:所属进程的 EPROCESS,如果是内核缓冲则为 NULL
  • MappedSystemVa:已经映射到系统空间的虚拟地址
  • StartVa:起始虚拟地址,通常按页对齐
  • ByteCount:锁定区间的长度
  • ByteOffset:从开始页偏移到缓冲区起始的偏移量

实际访问时,驱动一般不需要手算物理页数组,直接调用系统提供的宏或函数即可。理解 MdlFlags 和 ByteOffset 可以帮助你排查映射出错的问题。

2.2 关键函数分工

围绕 MDL 的 API 主要分成三组,每一组负责一个阶段。

分配与释放:

PMDL IoAllocateMdl( PVOID VirtualAddress, ULONG Length, BOOLEAN SecondaryBuffer, BOOLEAN ChargeQuota, PIRP Irp ); VOID IoFreeMdl(PMDL Mdl);

锁定与解锁:

void MmProbeAndLockPages( PMDL Mdl, KPROCESSOR_MODE AccessMode, LOCK_OPERATION Operation ); void MmUnlockPages(PMDL Mdl);

映射与取消映射:

PVOID MmMapLockedPagesSpecifyCache( PMDL Mdl, KPROCESSOR_MODE AccessMode, CACHE_TYPE CacheType, PVOID RequestedAddress, ULONG Priority, ULONG Flags ); void MmUnmapLockedPages(PVOID MappedAddress, PMDL Mdl);

另外还有一个高频宏:

PVOID MmGetSystemAddressForMdlSafe(PMDL Mdl, MM_PAGE_PRIORITY Priority);

它等价于“如果当前 MDL 尚未映射,则映射;如果已经映射,则直接返回已有系统地址”。在 Direct I/O 处理例程里,我几乎总是优先用这个宏,因为它不会因为内存不足抛异常,而是返回 NULL,便于统一错误处理。

这里有一个很重要的点:MmProbeAndLockPages 必须在 try/except 结构中调用,因为它会对无法锁定的用户地址、越界访问等异常做 SEH 处理。如果直接调用而不包裹 except,出现问题时会直接把系统打崩。

2.3 手动构建MDL的完整流程

手动构建 MDL 一般用于 Neither I/O 设备或者特殊内存访问场景。整个流程可以写成下面几步:

  1. 检查用户传入的虚拟地址和长度是否有效,长度不能超过驱动允许的单次传输上限。
  2. 调用 IoAllocateMdl(用户地址, 长度, FALSE, TRUE, NULL) 分配 MDL。
  3. 在 __try/__except 中调用 MmProbeAndLockPages,AccessMode 使用 UserMode,Operation 根据你是读用户缓冲区还是写用户缓冲区来选择 IoReadAccess / IoWriteAccess / IoModifyAccess。
  4. 调用 MmMapLockedPagesSpecifyCache 得到系统地址。
  5. 读写数据。
  6. 调用 MmUnmapLockedPages 解除映射。
  7. 调用 MmUnlockPages 解锁页面。
  8. 调用 IoFreeMdl 释放 MDL。

这个流程里最容易犯错的点在于:AccessMode 用错。如果是从内核模式入队请求,却传了 UserMode,会因权限判断失败导致锁定失败。相对地,在普通 IRP 派发例程中处理用户请求,必须用 UserMode,因为系统需要按用户模式规则校验缓冲区可访问性。

3. 完整实现:MDL读写驱动的编码

下面我用一个模拟设备驱动来演示完整实现。这个设备不真正访问硬件,而是维护一块内部缓冲区,支持用户态写入和读回。设备对象设置为 Direct I/O,这样 IRP 读写请求会直接带 MDL。另外加一个 METHOD_NEITHER 的 IOCTL,演示手动构建 MDL 访问用户指定内存区域。

3.1 创建设备对象并设置直接I/O

驱动入口里分配设备对象、设置分发函数:

NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject) { UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\MdlDemo"); UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\MdlDemo"); PDEVICE_OBJECT deviceObject = NULL; NTSTATUS status; status = IoCreateDevice(DriverObject, 0, &deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject); if (!NT_SUCCESS(status)) { return status; } // 设置为直接I/O,读写请求会由I/O管理器构建MDL deviceObject->Flags &= ~DO_BUFFERED_IO; deviceObject->Flags |= DO_DIRECT_IO; status = IoCreateSymbolicLink(&symLink, &deviceName); if (!NT_SUCCESS(status)) { IoDeleteDevice(deviceObject); return status; } DriverObject->MajorFunction[IRP_MJ_CREATE] = MdlDemoCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = MdlDemoCreateClose; DriverObject->MajorFunction[IRP_MJ_READ] = MdlDemoRead; DriverObject->MajorFunction[IRP_MJ_WRITE] = MdlDemoWrite; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MdlDemoIoctl; return STATUS_SUCCESS; }

有一个细节:IoCreateDevice 返回后,设备对象的 Flags 默认是既不设 Buffered I/O 也不设 Direct I/O。第一反应可能是直接设 DO_DIRECT_IO,但从严谨角度讲,最好先清掉 DO_BUFFERED_IO 再置位,防止初始化路径里某些意外标志残留。实际驱动里尽可能用“先清后置”的方式写。

3.2 读例程:从内部缓冲区拷贝到MDL映射地址

NTSTATUS MdlDemoRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp); ULONG length = ioStack->Parameters.Read.Length; NTSTATUS status = STATUS_SUCCESS; if (length == 0) { Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } PMDL mdl = Irp->MdlAddress; if (mdl == NULL) { status = STATUS_INVALID_DEVICE_REQUEST; } else { PVOID mappedBuffer = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority); if (mappedBuffer == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; } else { ULONG copyLength = length; if (copyLength > g_internalSize) { copyLength = g_internalSize; } RtlCopyMemory(mappedBuffer, g_internalBuffer, copyLength); Irp->IoStatus.Information = copyLength; } } if (!NT_SUCCESS(status)) { Irp->IoStatus.Information = 0; } Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }

这里没有直接通过 MDL 里的 StartVa 之类的字段去手动计算映射地址,而是用 MmGetSystemAddressForMdlSafe。它兼顾了两种状态:某些情况下 MDL 已经映射过(例如同一个 MDL 被多次利用),该宏会直接返回已有映射地址;如果未映射,则自动映射。这样代码更健壮。

读例程中访问的是“系统映射地址”,而不是用户地址,所以完全不担心进程上下文切换问题。

3.3 写例程:把MDL映射地址的内容拷贝到内部缓冲区

NTSTATUS MdlDemoWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp); ULONG length = ioStack->Parameters.Write.Length; NTSTATUS status = STATUS_SUCCESS; if (length == 0) { Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } PMDL mdl = Irp->MdlAddress; if (mdl == NULL) { status = STATUS_INVALID_DEVICE_REQUEST; } else { PVOID mappedBuffer = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority); if (mappedBuffer == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; } else { ULONG copyLength = length; if (copyLength > INTERNAL_BUFFER_SIZE) { copyLength = INTERNAL_BUFFER_SIZE; } RtlCopyMemory(g_internalBuffer, mappedBuffer, copyLength); g_internalSize = copyLength; Irp->IoStatus.Information = copyLength; } } if (!NT_SUCCESS(status)) { Irp->IoStatus.Information = 0; } Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }

写例程和读例程是对称的。唯一需要注意的是:Write 的 Information 字段表示“实际写入的字节数”,用户态 WriteFile 会把它作为实际写入大小返回。如果驱动要求长度必须对齐扇区或必须达到某个最小值,应该在完成 IRP 前校验,而不是默默截断。这里为了示例清晰,用了截断处理,但真实驱动中应当根据协议明确定义。

3.4 IOCTL手动MDL路径

为了演示手动构 MDL,我增加一个 METHOD_NEITHER 的 IOCTL。它的作用是:用户传入一个缓冲区地址和长度,驱动通过手动 MDL 锁定并映射这块缓冲区,然后把驱动内部数据拷贝到用户指定缓冲区中。

#define IOCTL_MDL_DEMO_COPY CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS) NTSTATUS MdlDemoIoctl(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION ioStack = IoGetCurrentIrpStackLocation(Irp); NTSTATUS status = STATUS_SUCCESS; if (ioStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_MDL_DEMO_COPY) { PVOID userBuffer = ioStack->Parameters.DeviceIoControl.Type3InputBuffer; ULONG userLength = ioStack->Parameters.DeviceIoControl.InputBufferLength; PMDL mdl = NULL; PVOID mappedBuffer = NULL; if (userBuffer == NULL || userLength == 0 || userLength > INTERNAL_BUFFER_SIZE) { status = STATUS_INVALID_PARAMETER; goto complete; } mdl = IoAllocateMdl(userBuffer, userLength, FALSE, TRUE, NULL); if (mdl == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; goto complete; } __try { MmProbeAndLockPages(mdl, UserMode, IoReadAccess); } __except (EXCEPTION_EXECUTE_HANDLER) { status = STATUS_INVALID_USER_BUFFER; goto cleanup_mdl; } mappedBuffer = MmMapLockedPagesSpecifyCache(mdl, UserMode, MmCached, NULL, HighPagePriority, NormalPagePriority); if (mappedBuffer == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; goto unlock_pages; } RtlCopyMemory(mappedBuffer, g_internalBuffer, userLength); status = STATUS_SUCCESS; MmUnmapLockedPages(mappedBuffer, mdl); unlock_pages: MmUnlockPages(mdl); cleanup_mdl: IoFreeMdl(mdl); } else { status = STATUS_INVALID_DEVICE_REQUEST; } complete: Irp->IoStatus.Status = status; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }

这里选择 IoReadAccess,是因为我们要往用户缓冲区里写数据,系统需要用“读取用户缓冲区”的权限校验来锁定页面。如果是想读取用户缓冲区的数据,则应该用 IoWriteAccess 或者 IoModifyAccess。这是新手最容易混的点。

METHOD_NEITHER 路径下,I/O 管理器不会为你锁定缓冲区,也不会创建 MDL,因此驱动必须具备完整的校验能力。如果你不愿意承担这个责任,MEMORY 方法就优先选 Buffered I/O 或 Direct I/O。

3.5 驱动入口与卸载

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NTSTATUS status = CreateDevice(DriverObject); if (!NT_SUCCESS(status)) { return status; } return STATUS_SUCCESS; } VOID DriverUnload(PDRIVER_OBJECT DriverObject) { UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\MdlDemo"); IoDeleteSymbolicLink(&symLink); IoDeleteDevice(DriverObject->DeviceObject); }

别忘了在 DriverEntry 里给 DriverObject->DriverUnload 赋值:

DriverObject->DriverUnload = DriverUnload;

卸载时先删符号链接,再删设备对象,顺序不能反。某些驱动会因为对象树里存在符号链接而导致删除失败。

4. 用户态测试与实测要点

4.1 测试客户端

用户态测试程序可以用 C 或者 PowerShell。C 程序最直接:

#include <windows.h> #include <stdio.h> int main() { HANDLE hDevice = CreateFileW(L"\\\\.\\MdlDemo", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDevice == INVALID_HANDLE_VALUE) { printf("open failed: %lu\n", GetLastError()); return 1; } char buffer[4096] = "hello mddl driver write"; DWORD written = 0; BOOL ok = WriteFile(hDevice, buffer, 4096, &written, NULL); if (!ok) { printf("write failed: %lu\n", GetLastError()); } else { printf("write success: %lu bytes\n", written); } char readBuffer[4096] = {0}; DWORD readLen = 0; ok = ReadFile(hDevice, readBuffer, 4096, &readLen, NULL); if (!ok) { printf("read failed: %lu\n", GetLastError()); } else { printf("read success: %lu bytes: %s\n", readLen, readBuffer); } // 测试 METHOD_NEITHER IOCTL char outBuf[4096] = {0}; DWORD retLen = 0; ok = DeviceIoControl(hDevice, IOCTL_MDL_DEMO_COPY, outBuf, sizeof(outBuf), NULL, 0, &retLen, NULL); if (!ok) { printf("ioctl failed: %lu\n", GetLastError()); } else { printf("ioctl copy ok: %s\n", outBuf); } CloseHandle(hDevice); return 0; }

这里 UseCodeIOCTL 的传参需要注意:因为 IOCTL 是 METHOD_NEITHER,InputBuffer 会被 I/O 管理器作为 Type3InputBuffer 直接传进去,驱动锁定的是这个传入地址。而用户态传入的地址必须是可读写的普通用户堆栈或堆地址,不能是只读常量区,否则 MmProbeAndLockPages 会触发异常。

4.2 实测中的注意事项

第一,测试前先确认驱动已正确加载、设备符号链接已经创建。直接用 CreateFile 打开失败时,常见的错误码是 ERROR_FILE_NOT_FOUND(2),这种大多是符号链接路径不对,或者设备没有创建成功。

第二,读写长度不要一次搞太大。虽然 MDL 理论上能处理大缓冲区,但系统有内存锁定额度和 MDL 长度限制。单次 I/O 过长会触发资源不足。建议先以 4KB、16KB 起步,确认数据一致后再加压。

第三,用 WinDbg 附加内核调试。我在驱动开发中一直保持着“改了驱动代码必须挂内核调试器”的习惯。读写例程中一旦出现 STATUS_INVALID_USER_BUFFER 或缓冲区内容异常,调试器里看 pMdl->ByteCount、pMdl->ByteOffset 和映射后的缓冲区内容,很快就能定位问题。特别是 MmGetSystemAddressForMdlSafe 返回 NULL 时,要优先检查 MDL 是否真的是由 I/O 管理器分配的 MDL,还是你在某个错误字段上强行构造的。

第四,注意 BuildNumber 和系统版本差异。不同 Windows 版本对 MDL 物理页数组和映射算法的内部实现有差异,但公开 API 是稳定的。你只要不手动解析 MDL 内部字段,基本可以做到一套代码跨版本运行。

5. 常见问题与排查技巧实录

5.1 错误速查表

现象可能原因排查方向
WriteFile 返回错误 87IRP 请求长度或缓冲区类型不正确检查 IOCTL 控制码使用方法,确认输入输出缓冲区长度
ReadFile 返回错误 1功能错误,读例程中 Information 字段设置不对检查 IRP 完成状态和 Information 是否等于实际拷贝长度
Blue screen 0x50直接访问未锁定的用户地址确认是否在 Direct I/O 下使用 MdlAddress;不要在无 MDL 时直接访问 Type3InputBuffer
MmMapLockedPagesSpecifyCache 返回 NULL内存不足或锁页配额超限缩短单次传输长度,检查是否有驱动未解锁的 MDL 泄漏
MmProbeAndLockPages 抛异常用户地址非法、越界,或缓冲区是只读却用 IoWriteAccess用 try/except 包住,并检查操作类型 AccessMode、Operation
驱动卸载失败有未释放的 MDL 或设备对象冲突检查所有路径是否成对调用 MmUnmapLockedPages / MmUnlockPages / IoFreeMdl

5.2 独家避坑心得

我在最初实现这个驱动时,遇到过一个非常隐蔽的问题:WriteFile 传一个小缓冲区时,I/O 管理器不会为 MDL 分配完整的物理页面数组,ByteCount 可能小于请求长度?实际情况是,I/O 管理器对 Direct I/O 的处理是完整锁页并填充 MDL,如果出现异常往往是驱动自己改动设备对象标志导致 I/O 路径混乱。

还有一个容易忽略的点是调用 MmMapLockedPagesSpecifyCache 时 CacheType 到底是选 MmCached 还是 MmNonCached。对于普通 RAM 缓冲区,用 MmCached 没问题;如果驱动之后要做 DMA 到设备,要结合硬件能力选择缓存类型,并且注意是否需要在 DMA 前做缓存刷新。示例项目只是内存数据拷贝,不需要特别处理。

另一条经验是:MDL 映射出来的系统地址,在驱动自身的 IRQL 环境下访问有讲究。读写例程通常运行在 PASSIVE_LEVEL,这时候安全。如果你在 DISPATCH_LEVEL 访问 MDL 映射缓冲区,需要用 MmGetSystemAddressForMdlSafe 并且系统会直接帮你映射;但要真正读写数据,仍要注意此时不能调用需要分页的例程,也不能发生页面错误。所以在设计时尽量把耗时拷贝放到 PASSIVE_LEVEL 的工作线程里做,不要在派发例程中硬扛。

最后一个大坑:MDL 的“锁页”会占用系统锁页配额,大量驱动频繁申请大 MDL 却不释放,系统最终会因为内存锁定配额耗尽而出现分配失败。所以驱动里一旦出现 MmGetSystemAddressForMdlSafe 返回 NULL,不要反复重试拿长缓冲区,要检查是否在某条错误分支中遗漏了 MmUnlockPages。建议把所有分配和释放过程封装成函数,并在所有出口统一释放,而不是零零散散写在各个分支里。

如果要做性能压测,用 MmGetSystemAddressForMdlSafe 映射一次后,可以连续多次读写映射地址,完成后再统一解除映射。但是注意,Direct I/O 的 IRP 应该在 IoCompleteRequest 之前解除映射吗?实际上,Direct I/O 设备的 IRP 中的 MDL 由 I/O 管理器在请求完成后统一解锁,驱动不需要也不能主动 MmUnlockPages。这条也是新手最常问的:为什么手动 MDL 里要主动解锁,Direct I/O 里却不用?因为 I/O 管理器已经接管了 MDL 的完整生命周期,驱动只负责访问,不负责收尾。

5.3 关于使用ManualMode 的额外建议

如果你真的需要在 METHOD_NEITHER 下处理大量用户缓冲区,建议对每个缓冲区都走 IoAllocateMdl、MmProbeAndLockPages、MmMapLockedPagesSpecifyCache 的完整链路。不要尝试用 ProbeForRead 或 ProbeForWrite 之后就直接访问用户指针,那样仍然有进程切换风险。ProbeForRead 只验证地址可访问性,并不把页面锁定在物理内存中,所以脱离 MDL 直接访问是不可靠的。

另外,对于设备对象同时开放读写和 IOCTL 的情况,建议把 IOCTL 控制码的读写权限设置清楚。METHOD_NEITHER 要求驱动自带校验逻辑,因此对你的数据结构和边界检查要求更高。如果只是想快速实现控制命令,用 METHOD_BUFFERED 加 SystemBuffer 更稳妥,这样既避免手动 MDL 的复杂度,也避免安全漏洞。

6. 最后再分享一个小技巧

在我的实际开发中,有一招对调试 MDL 读写特别有效:在内核调试器里直接判断 MDL 是否完整,使用 dt _MDL 展开当前 IRP 的 MdlAddress,会看到 ByteCount 和 StartVa。如果 ByteCount 和请求长度对不上,说明 I/O 管理器构造 MDL 时出了问题,多半是设备对象标志被改坏了,或者请求路径异常。如果 ByteCount 是对的,但映射后的地址和用户态看到的地址相差很远,这是正常现象,映射地址是系统地址,不能用来反向比较。

另外,调试时多打 KdPrint 日志,记录请求长度、MDL 地址、映射地址和拷贝长度。一次设备读写请求往往涉及多个页面,日志可以帮助你快速判断数据是否被截断、是否存在越界。我给这类日志里加上了请求序号,这样在多线程并发读写时也能追踪到具体的 IRP 生命周期。

MDL 驱动读写这件事,说到底是让驱动学会“正规地”访问用户态内存。一旦你掌握了 Direct I/O 下的自动 MDL 和 METHOD_NEITHER 下的手动 MDL,后面再做 FIFO、虚拟磁盘、过滤驱动里的缓冲区处理,思路都会清晰很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:25:01

ppt使用笔记(一)

文章目录如何让文档好看纯文字绝对不可行多用流程图和效果图切换动画母版音乐视频色块(重)渐变蒙版渐变填充交替效果渐变光圈渐变的类型渐变最主要的参数色块-色块中的字体颜色如何设置?色块中的文本框如何对齐?取色器(拾色器)在哪里?ppt三段式设计三色原则批量旋转百度实现…

作者头像 李华
网站建设 2026/10/19 2:24:56

SVM支持向量机实战指南:从核心概念到调参与部署

1. 先弄清SVM到底是什么&#xff1a;一次说透核心概念刚接触SVM&#xff08;支持向量机&#xff0c;Support Vector Machine&#xff09;的人&#xff0c;十有八九会被那一堆数学公式劝退。其实SVM的思想特别朴素&#xff0c;一句话就能说清&#xff1a;在两类数据之间找一条分…

作者头像 李华
网站建设 2026/10/19 2:24:39

SpringBoot+Vue足球赛事社区互动网站开发实战

做这个足球赛事社区互动网站之前&#xff0c;我花了不少时间泡在各种球迷群里观察大家平时聊什么、吵什么、分享什么。一个很直接的感受是&#xff1a;比赛数据到处都是&#xff0c;但真正缺的是一个能把数据、观点、情绪都串起来的互动空间。所以这个项目定下的基调很明确——…

作者头像 李华
网站建设 2026/10/19 2:24:37

改进卷积模型的人脸性别与情感分类实践与思考

这题目一出&#xff0c;我当时的直观反应就是&#xff1a;又是一个把人脸识别和情感计算缝合起来的CNN改改进题。真正做进去之后才发现&#xff0c;题目里每一个词都不是白给的——"性别分类"和"情感分类"虽然都发生在同一张脸上&#xff0c;但它们在特征尺…

作者头像 李华
网站建设 2026/10/19 2:24:24

轻量级公式识别模型UniRec-0.1B:架构、9倍速推理与部署实战

公式识别一直是文档数字化里最让人头疼的一环。你平时看论文、转电子教案、整理试卷&#xff0c;文字部分OCR都已经做得挺成熟&#xff0c;但一旦碰到积分公式、矩阵、分段函数&#xff0c;传统引擎基本就废了。最近我刷到一个开源项目&#xff0c;标题写得很直白&#xff1a;轻…

作者头像 李华
网站建设 2026/10/19 2:24:14

K-means聚类MATLAB实现:从算法原理到可视化完整指南

简介&#xff1a;一份帮助Matlab用户快速上手K-means聚类算法的代码详解文档&#xff0c;面向需要进行数据分组、模式识别或课程实验的初学者与科研人员。文档从欧氏距离、曼哈顿距离等常见度量方式切入&#xff0c;逐步展示初始化聚类中心、分配数据点、更新质心的完整迭代流程…

作者头像 李华