多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

WDM PCIe驱动开发详解:从WDK环境搭建到DMA与中断调试

WDM PCIe驱动开发详解:从WDK环境搭建到DMA与中断调试 简介一套基于WDM模型开发PCI/PCIe设备驱动的工程资料面向Windows底层硬件驱动开发者尤其适合正在学习WDK驱动框架、希望掌握PCI配置空间访问与中断处理的人群。压缩包共20个文件以C头文件与源文件、Visual Studio项目文件、INF安装脚本以及测试程序为主辅以DSP等旧工程文件整体约110KB结构紧凑方便对照编译和调试。内容覆盖PnP即插即用、电源管理、IRP请求处理、设备对象与驱动对象创建、PCI配置空间读取、PDO/FDO过滤设备等关键机制并配有设备枚举与探测的示例代码源码中的驱动主体和配套测试工程展示了如何读取设备ID、中断请求线、内存映射地址等配置信息设置中断处理并完成从初始化到资源获取的完整流程。已有331人学习下载。通过分析驱动源码和测试代码可以理清WDM驱动从初始化、资源获取到中断响应和IRP处理的完整流程为后续自主开发其他PCI/PCIe设备驱动打下扎实基础。1. WDM_PCI_Driver.zip为什么这个驱动包值得你重新写一遍拿到一个名为 WDM_PCI_Driver.zip 的压缩包多半是前辈留下的 WDM 驱动源码也可能只是某个芯片厂商 SDK 里的参考例程。它解决的问题很具体让 Windows 识别你的 PCIe 板卡分配中断、映射 BAR 空间、搬 DMA 数据。但这里有个反直觉的结论——这个 zip 包通常不能直接用甚至直接编不过更常见的归宿是当参考资料然后你自己基于 WDK 重写一版。原因后面细说先给结论WDM 是 Windows 驱动的最底层模型WDK 是微软提供的开发工具链PCIe 开发则是这两者结合的典型硬核场景。这篇文章适合三种人给自研 PCIe 板卡写 Windows 驱动的嵌入式工程师、刚接手老驱动项目需要维护的软件工程师、以及想搞懂 Windows 下 DMA 和中断机制的学生。我会按「原理 → 环境 → 骨架 → 硬件操作 → 避坑 → 验证」的顺序把一套可落地的 PCIe WDM 驱动开发路径讲清楚。2. 先搞清三件事WDM、WDK 和 KMDF 在 PCIe 驱动里各管什么很多新手把 WDM 和 WDK 混为一谈实际上它们是两个层面的东西。WDMWindows Driver Model是一种驱动架构定义了驱动对象、设备对象、IRP 处理、PnP 和电源管理这些基本规则WDKWindows Driver Kit是微软提供的一整套开发工具包含头文件、库、构建工具、调试器和文档。而 KMDFKernel-Mode Driver Framework是建立在 WDM 之上的封装框架帮你处理了大量模板代码。做 PCIe 开发时这三者的分工经常让人困惑。2.1 WDM 的分层模型从总线驱动到功能驱动Windows 下任何 PCIe 设备都不是只靠一个驱动工作。硬件插入 PCIe 插槽后系统自带的 PCI 总线驱动pci.sys负责枚举设备、读取配置空间、分配资源然后通知 PnP 管理器找到你的功能驱动。功能驱动负责具体硬件操作这就是你在 WDM 项目里写的那部分代码。这里要理解一个关键点你的驱动不直接操作 PCIe 配置寄存器而是向 PCI 总线驱动发 IRPI/O Request Packet请求资源。总线驱动把 BAR 地址、中断向量这些信息转成CM_RESOURCE_LIST在启动设备时交给你。WDM 模型的精髓就是这个——驱动之间是分层的功能驱动永远通过 IRP 与底层驱动通信而不是暴力访问物理地址。对于 PCIe 设备开发中实际要面对的是三个层次的代码DriverEntry 里创建驱动对象、AddDevice 里创建设备对象并与设备栈绑定、以及各种 Dispatch 例程里处理读写请求。多数人把注意力放在最后一个但前两个才是 WDM 和 KMDF 差异最大的地方。2.2 WDK 装完之后的开发闭环WDK 目前的安装方式是通过 Visual Studio Installer 勾选组件装完后你得到的是构建工具MSBuild 任务、内核头文件wdm.h、ntddk.h、wdf.h、库文件wdmsec.lib、WdfDriverEntry.lib、以及 inf2cat、signtool 这些签名工具。调试环节容易被忽略。WDK 集成的不是 Visual Studio 自带的调试器而是 WinDbg——这是内核调试的核心工具用双机调试时目标机需要开启内核调试模式主机的 WinDbg 通过 COM 口或网络连接调试目标。构建、部署、调试这个闭环打通了驱动开发才算真正开始。2.3 用 KMDF 还是纯 WDM先看中断和 DMA 需求标题里写了 WDM_PCI_Driver但有一个现实问题2024 年的今天纯 WDM 项目越来越少WDK 自带的模板默认都是 KMDF。如果你接手的是老代码纯 WDM 可能已经编译不过新版 WDK如果是从零起步KMDF 是更稳妥的选择。我一般建议分情况讨论如果硬件只是简单的 PCIe 转串口、转 GPIOKMDF 足够代码量能减少 40% 左右如果要做高性能 DMA、多队列、MSI-X 中断KMDF 的事务模型反而成了限制这时候纯 WDM 更容易控制细节。但注意一个折中方案——KMDF 驱动里可以混合调用 WDM 函数WdfDeviceWdmGetDeviceObject就能拿到 WDM 的 DEVICE_OBJECT需要用底层接口时随时下探。无独有偶这套选型逻辑跟 Linux 下的驱动开发是相反的Linux 那边是字符设备驱动更简单而 Windows 这边框架越高级入门越容易。GPU 驱动开发几乎都是纯 WDM因为 KMDF 抽象的太多性能关键路径上你希望每个字节都自己控制。如果你未来要做的板卡接近 GPU 的工作模式最好直接学纯 WDM。3. 搭建 WDK 环境并生成 PCI 驱动骨架从 INF 到第一个 DriverEntry环境搭错了后面全是坑。我见过太多人装完 WDK 后找不到头文件或者编译时报wdf.h不存在——基本都是 Visual Studio 版本和 WDK 版本不匹配造成的。WDM 驱动开发最常见的环境组合是 Visual Studio 2022 WDK 10.0.22621 或更高版本注意 WDK 版本必须与 Windows SDK 版本对应。3.1 安装 WDK 并用 KMDF 模板创建项目安装后打开 Visual Studio新建项目时选择「Kernel Mode Driver (KMDF)」模板。值得说明的是KMDF 的模板生成的是 PnP 驱动骨架而不是传统 WDM 的只写 DriverEntry。这正好因为 PCIe 设备必然是 PnP 设备。// DriverEntry.c - KMDF 驱动的入口点 #include ntddk.h #include wdf.h DRIVER_INITIALIZE DriverEntry; EVT_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd; NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { WDF_DRIVER_CONFIG config; WDFDRIVER driver; NTSTATUS status; // 初始化 WDF_DRIVER_CONFIG并指定 AddDevice 回调 WDF_DRIVER_CONFIG_INIT(config, EvtDriverDeviceAdd); status WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, config, driver); if (!NT_SUCCESS(status)) { return status; } return STATUS_SUCCESS; }这段代码与传统 WDM 的 DriverEntry 最大区别是WDM 里你要自己填 DriverObject-DriverExtension-AddDevice并且设置 DriverUnload 回调KMDF 里一行WDF_DRIVER_CONFIG_INIT就完成了这些。参数RegistryPath指向驱动在注册表服务键的位置一般不用动WDF_NO_OBJECT_ATTRIBUTES表示不自定义驱动对象属性够用了。3.2 INF 文件里声明 PCI 硬件 ID这个文件卡掉了无数人。INF 不仅是安装脚本它决定了 Windows 能不能找到你的设备、能不能正确加载驱动。PCI 设备的 INF 核心是[Manufacturer]段和[Models]段里的硬件 ID 匹配规则。; pci_driver.inf - 硬件 ID 匹配示例 [Version] Signature $WINDOWS NT$ Class System ClassGuid {4D36E97D-E325-11CE-BFC1-08002BE10318} Provider %ProviderName% DriverVer 04/15/2024,1.0.0.0 [Manufacturer] %ProviderName% DeviceList, NTamd64 [DeviceList.NTamd64] ; VID 和 DID 按你的 PCIe 设备实际值替换 %DeviceDesc% InstallSection, PCI\VEN_10EEDEV_9038 [InstallSection.NTamd64] CopyFiles DrvCopyFiles [DrvCopyFiles] driver.sys [SourceDisksFiles] driver.sys 1PCI\VEN_10EEDEV_9038里的 VEN 是 Vendor IDDEV 是 Device ID先通过设备管理器或 PCIe 配置空间读取工具确认设备真实 ID再填进来。ClassGuid 决定设备在设备管理器里归属的分类PCIe 采集卡一般用 System 类或自定义类不要照抄网卡的 Guid。这个文件写错最常见的报错是「设备的驱动程序未安装」且没有任何错误码。3.3 跑通 AddDevice 和硬件准备回调KMDF 的AddDevice只是创建设备对象真正的硬件操作在EvtDevicePrepareHardware里。这个回调是 KMDF 最核心的硬件入口系统保证在这里已经拿到了分配好的资源列表。// Device.c - 创建设备对象并准备硬件 EVT_WDF_DEVICE_PREPARE_HARDWARE EvtDevicePrepareHardware; EVT_WDF_DEVICE_D0_ENTRY EvtDeviceD0Entry; NTSTATUS EvtDriverDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit) { WDFDEVICE device; WDF_PNPPOWER_EVENT_CALLBACKS pnpCallbacks; WDF_OBJECT_ATTRIBUTES deviceAttributes; // 注册 PnP/电源回调重点是 PrepareHardware 和 D0Entry WDF_PNPPOWER_EVENT_CALLBACKS_INIT(pnpCallbacks); pnpCallbacks.EvtDevicePrepareHardware EvtDevicePrepareHardware; pnpCallbacks.EvtDeviceD0Entry EvtDeviceD0Entry; WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, pnpCallbacks); WDF_OBJECT_ATTRIBUTES_INIT(deviceAttributes); return WdfDeviceCreate(DeviceInit, deviceAttributes, device); }注意WdfDeviceCreate成功后DeviceInit指针会被置空不能再引用。这是 KMDF 的规矩跟 WDM 里IoCreateDevice后 DeviceObject 自己握着完全不同。D0Entry 是设备从低功耗状态恢复时要做的寄存器重初始化——很多硬件在 D3 后寄存器内容会丢。这个骨架跑通后设备管理器里应该能看到设备且无黄色感叹号但此时驱动还什么都没干。下一步才是真正操作硬件。4. 把 PCIe 设备真正用起来配置空间、BAR 映射和中断从这个章节开始驱动开始触碰真实硬件。我强烈建议你手边备一个逻辑分析仪或者至少是调试串口因为接下来的代码一个寄存器的错位就可能导致系统蓝屏没有任何用户态程序能替你挡。4.1 从配置空间读 Vendor ID / Device IDKMDF 提供了WdfDeviceGetBusData来读取 PCI 配置空间这个函数封装了向 PCI 总线驱动发 IRP 的过程。// 读取配置空间的 VID/DID验证驱动与硬件的匹配关系 #define PCI_CONFIG_OFFSET_VID 0x00 // Vendor ID 寄存器偏移 #define PCI_CONFIG_OFFSET_DID 0x02 // Device ID 寄存器偏移 VOID ReadPciConfigSpace(WDFDEVICE Device) { ULONG data 0; // 第一个参数 0 表示从配置空间头部开始读 // 第二个参数指定读取偏移单位是字节 // 第三个参数是缓冲区第四个是读取长度 WdfDeviceGetBusData(Device, 0, PCI_CONFIG_OFFSET_VID, data, sizeof(USHORT) * 2); UINT16 vendorId (UINT16)(data 0xFFFF); UINT16 deviceId (UINT16)((data 16) 0xFFFF); }参数含义第二个参数0是 bus data 的类型配置空间读用 0第三个参数是字节偏移第四个是缓冲区第五个是读多少字节。注意WdfDeviceGetBusData一次最多读多少取决于 PCI 总线驱动的限制常见做法是分多次读不要一把梭读 256 字节。配置空间的 VID/DID 是 16 位的所以用USHORT长度但函数返回值是实际读取的字节数要做判断。这个接口虽然简单但有一个明显的天坑它只能在EvtDevicePrepareHardware、EvtDeviceD0Entry这类 PnP 回调里调用普通 Dispatch 例程里调用会返回 0。原因是设备可能处于 D1/D2 状态配置空间访问被 PCI 总线驱动切断。如果你需要在任意时刻读配置空间得先保证设备在 D0。4.2 映射 BAR 空间的三种方式PCIe 设备给主机暴露的寄存器区间就是 BARBase Address Register。拿到CM_RESOURCE_LIST后里面每段 Memory 或 Port 类型的资源对应一个 BAR。映射方式有三条路线直接索引CM_PARTIAL_RESOURCE_DESCRIPTOR、用WdfCmResourceListGetDescriptor遍历、以及启用资源翻译后的WdfDeviceGetVirtualAddress。// 遍历资源列表找到 Memory 类型的资源并映射为虚拟地址 NTSTATUS MapBar0(WDFDEVICE Device) { PCM_PARTIAL_RESOURCE_LIST rawList NULL; ULONG bar0Phys 0; ULONG bar0Len 0; PVOID bar0Va NULL; rawList WdfCmResourceListGetList(Device, WdfCmResourceListGetCount(Device) - 1); for (ULONG i 0; i rawList-Count; i) { PCM_PARTIAL_RESOURCE_DESCRIPTOR desc rawList-PartialDescriptors[i]; if (desc-Type CmResourceTypeMemory) { bar0Phys desc-u.Memory.Start.LowPart; bar0Len desc-u.Memory.Length; break; } } // Start.LowPart 只是物理地址低 32 位64 位地址要用 QuadPart PHYSICAL_ADDRESS physAddr; physAddr.QuadPart (LONGLONG)bar0Phys; bar0Va MmMapIoSpace(physAddr, bar0Len, MmNonCached); if (bar0Va NULL) { return STATUS_INSUFFICIENT_RESOURCES; } return STATUS_SUCCESS; }这里MmMapIoSpace是 WDM 层面的老函数KMDF 里仍然可以直接用它把物理地址映射到内核虚拟地址空间。第三个参数MmNonCached指不启用 CPU 缓存——对寄存器来说必须这样否则读到的可能是 CPU 缓存里的旧值。映射成功后读写寄存器就像访问普通内存指针一样但要注意BAR 空间的长度不一定和你请求的资源长度一致有些设备 BAR 寄存器只实现了部分空间超出部分访问会触发总线错误蓝屏。4.3 中断从 INTx 到 MSI/MSI-X 的注册差异PCIe 中断有传统的 INTx 和现代化的 MSI/MSI-X 两种Windows 驱动注册方式完全不同。KMDF 标准做法是WdfInterruptCreate策略通过WdfInterruptSetPolicy设置。// 创建 WDF 中断对象支持 MSI/MSI-X NTSTATUS CreateInterrupt(WDFDEVICE Device) { WDF_INTERRUPT_CONFIG intrConfig; WDFINTERRUPT interrupt; WDF_OBJECT_ATTRIBUTES intrAttr; WDF_INTERRUPT_CONFIG_INIT(intrConfig, EvtInterruptIsr, EvtInterruptDpc); intrConfig.PassiveHandling FALSE; // 主动处理ISR 在 DIRQL 执行 intrConfig.CanWakeDevice FALSE; // 不用于唤醒设备 WDF_OBJECT_ATTRIBUTES_INIT(intrAttr); intrAttr.ParentObject Device; // 关键设置中断分配策略为 MSI NTSTATUS status WdfInterruptCreate(Device, intrConfig, intrAttr, interrupt); if (NT_SUCCESS(status)) { WdfInterruptSetPolicy(interrupt, WdfInterruptPolicyAll, 0, 0); } return status; }WdfInterruptSetPolicy的作用是告诉即插即用管理器这个驱动优先使用 MSI 还是 MSI-X。第一个参数WdfInterruptPolicyAll表示先尝试 MSI/MSI-X 再回落 INTx如果硬件只支持 MSI-X用WdfInterruptPolicyMultiDirectional配合详细设置。注意 MSI-X 需要逐向量配置如果你的设备有多个中断向量KMDF 的自动分配可能不够灵活这时需要手动用WdfInterruptCreate逐个创建并绑定 MSI-X 表。纯 WDM 路线则是IoConnectInterruptEx用INTERRUPT_CONNECT_TYPE区分连接方式。两者混用时注意KMDF 创建的中断在设备移除时会自动删除而 WDM 需要你在IRP_MN_REMOVE_DEVICE里手动IoDisconnectInterrupt。4.4 DMA 的 Buffer 分配与映射要点PCIe 驱动绕不开 DMA。WDM 时代的 DMA 是IoGetDmaAdapter 构建 scatter/gather 列表KMDF 封装成了WdfDmaEnablerCreate和WdfDmaTransactionCreate。这里只说最常见的一个坑——缓冲区物理地址的连续性。// 分配 DMA 缓冲区并获取物理地址 PVOID AllocDmaBuffer(WDFDMAENABLER DmaEnabler, SIZE_T Size, PHYSICAL_ADDRESS *PhysAddr) { WDFCOMMONBUFFER buffer; WDF_OBJECT_ATTRIBUTES attr; PVOID virtualAddr NULL; WDF_OBJECT_ATTRIBUTES_INIT(attr); attr.ParentObject DmaEnabler; NTSTATUS status WdfCommonBufferCreate(DmaEnabler, Size, attr, buffer); if (!NT_SUCCESS(status)) { return NULL; } // 获取虚拟地址和物理地址 virtualAddr WdfCommonBufferGetAlignedVirtualAddress(buffer); *PhysAddr WdfCommonBufferGetAlignedPhysicalAddress(buffer); return virtualAddr; }WdfCommonBufferCreate分配的是连续物理内存并且自动对齐到设备的最大传输长度。这块内存在驱动生命周期内固定存在不随 IRP 变化。但它的缺点是分配的是非分页内存大块分配容易失败——一个常见方案是驱动启动时分配 4KB 对齐的多个小块用分页内存做数据缓存DMA 前再拷贝到公共缓冲区。这个中转拷贝损耗是很多新手没预料到的实际传输性能往往输在这里。5. 避坑PCIe 驱动调试中我反复踩的 6 个坑驱动开发没有玄学每个蓝屏背后都有一个具体的寄存器或指针问题。下面这几条是我这些年做 PCIe 驱动踩过最深的六个坑每条按「现象 → 原因 → 解决」给你拆开。5.1 安装驱动报错 577 或 31签名问题现象设备管理器安装驱动时显示「数字签名无法验证」或错误代码 31。原因64 位 Windows 强制加载签名驱动WDK 编译的测试签名驱动也需要在测试模式下才能安装。事件查看器里通常能看到内核事件 ID 219。解决启动测试模式并安装测试证书。# 管理员权限下执行开启测试签名模式 bcdedit /set testsigning on # 然后用 WDK 自带工具签名 signtool sign /v /s TestCertStore /n 你的证书名 driver.sys注意testsigning开启后系统安全中心会有水印这是正常现象。驱动调试完了要记得bcdedit /set testsigning off恢复。5.2 一访问 BAR 寄存器就蓝屏现象驱动加载正常但只要READ_REGISTER_ULONG就死机蓝屏代码是BUS_ADDRESS_ERROR。原因MmMapIoSpace映射的长度超过了设备实际 BAR 大小或者物理地址算错了。很多 PCIe 设备的 BAR 实现不完整比如 BAR0 申请的 4KB 但硬件只实现了前 256 字节。解决先通过配置空间读取 BAR0 的原始值看 bit[3:0] 是 0Memory 类型还是 1IO 类型然后核对映射长度只映射硬件实际支持的长度。再用 WinDbg 的!pte命令检查映射后的虚拟地址是否有效。5.3 设备正常但中断从来不触发现象MSI 使能了中断状态寄存器在轮询时能看到 pending 位但 ISR 不执行。原因CM 资源列表里你拿到的是 MSI 的消息地址和数据如果硬件的中断掩码寄存器里有未清除的 pending 中断MSI 消息发不出来。另外一个常见原因是中断在EvtDevicePrepareHardware里创建的太早设备处于 D0 之前就使能了中断。解决在EvtDeviceD0Entry里最后再WdfInterruptEnable并在 ISR 开头先读设备中断状态寄存器判断是不是自己的中断不是就直接返回FALSE不要让框架继续处理。另外如果用了 MSI注意 PCIe 的 MSI 是 32 位消息地址配置空间里必须正确写入。5.4 驱动卸载时死锁或蓝屏现象设备可以正常工作但拔设备或禁用设备时系统卡死WinDbg 里看到当前线程卡在IoDeleteDevice或WdfObjectDelete。原因典型的引用计数泄漏。常见于 Dispatch 例程里拿到了设备对象指针但没有ObDereferenceObject或者异步 IRP 还没有 complete 时设备被移除。解决所有非 PnP 回调里获取 WDFDEVICE 都要用WdfDeviceGetTypedContext并且确认有引用保护。在EvtDeviceReleaseHardware里打印所有 IRP 是否有未完成的确保在释放资源前全部 complete。5.5 DMA 数据错位或全零现象FPGA 端收到的主机内存数据全是零或者地址错乱但 DMA 请求已经完成。原因最大传输长度设置不对。PCIe 的 TLP 最大载荷MPS和WdfDmaEnablerCreate里的最大传输长度必须匹配。如果你声明支持 256 字节 MPS但实际 PCIe 链路配置的是 128 字节硬件会只传一半数据就丢了。解决驱动启动时读 PCIe 配置空间的 Device Control 寄存器偏移 0x78实际是 0x78 的 bit[7:5] 和 bit[4:2]获取实际 MPS 值再传给 DMA 配置。不要自己瞎填 4096 这种大值。5.6 设备休眠唤醒后驱动瘫痪现象电脑睡眠再唤醒后驱动能加载但硬件操作全部失败读寄存器返回 0xFFFFFFFF。原因设备进入 D3 后BAR 映射还在但硬件寄存器内容丢失PCIe 链路也可能重新训练过。驱动没有处理EvtDeviceD0Entry回调没做寄存器重初始化。解决在EvtDeviceD0Entry里重新做一遍硬件初始化重新读配置空间、重新写 BAR 寄存器、重新配置中断和 DMA。千万别只在EvtDevicePrepareHardware里做初始化D0Entry 和 PrepareHardware 是两个完全不同的时机前者是每次从低功耗恢复都会调用。6. 验证驱动不是玄学从设备管理器到 WinDbg 的一串证据驱动的正确性不是看一眼设备管理器没感叹号就算验证过了。我的习惯是建立一条完整的证据链从上到下逐层确认每一步都有可抓取的事实。最外层是设备管理器——看到设备状态「这个设备工作正常」表示 PnP 和资源分配成功但这只证明 INF 匹配成功。第二层是 WinDbg 用!wdfkd.wdfdevice看 KMDF 设备对象的状态确认PrepareHardware是否返回成功再用!wdfkd.wdfinterrupt看中断对象的连接情况——这里能直接看到中断是 MSI 还是 INTx、绑定在哪个处理器上。第三层是功能性验证写一个用户态测试程序用 CreateFile 打开设备发 DeviceIoControl 触发一次寄存器读写回环比如写一个已知值到某个 scratch 寄存器再读回来比对。这一步能证明从应用层到内核层、再到 PCIe 总线的完整链路都通。有一个技巧我认为价值很高利用 WPP 软件跟踪WPP Software Tracing。在代码里用WPP_INIT_TRACING和DoTraceMessage埋点调试时用tracelog抓取可以在不改代码的情况下开关日志。我第一次把 WPP 用起来的那一周调试效率提升了一倍不止——从此不再靠 KdPrint 和 WinDbg 的!dbgprint人肉抓日志。WPP 日志还能记录时间戳分析中断延迟和 DMA 延迟时直接有数据支撑。最后一个建议给所有公开的接口例程写一个统一入口函数每个 IRP 进来时记录 IRP 的 MajorFunction 和设备上下文指针这样出问题时能立刻知道是不是有 IRP 堆积没有 complete。这套代码的调试价值远超那几行日志的消耗。我见过一个同事因为在 Dispatch 例程里返回了STATUS_PENDING但没把 IRP 存起来导致设备移除时 IRP 泄漏查了三天。后来把统一入口配上 WPP 日志十分钟定位。这是我的个人教训现在也成了我写驱动的默认习惯——基础设施先搭好再写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
返回列表