ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

WDM PCI驱动开发实战:从设备枚举到IRP分发与INF安装

WDM PCI驱动开发实战:从设备枚举到IRP分发与INF安装 简介面向Windows平台驱动开发者的PCI/PCIe驱动程序开发资料包基于WDMWindows Driver Model模型编写适合需要从零上手WDM驱动、理解PCI设备与系统交互的工程师。资源共20个文件包含头文件h、C源代码cpp、Visual Studio工程文件vcxproj/sln、INF安装配置、DSP/DSW等传统构建脚本以及模拟测试工程可覆盖驱动编写、编译、安装、验证的完整链路。整个压缩包仅110KB内容精炼聚焦核心代码与配置。已有330人学习下载。资料围绕WDM关键机制展开涉及PnP即插即用、电源管理、IRP请求包处理、设备对象与驱动对象创建、PCI配置空间访问、PDO/FDO设备层次等核心概念通过阅读和编译示例读者能掌握使用WDK构建PCIe设备驱动的流程学会设备资源获取、中断处理及I/O操作等底层开发技能适合作为Windows驱动入门实践与项目参考。1. 从设备管理器感叹号说起WDM_PCI_Driver 为什么值得拆开看用户插入一块 PCIe 采集卡系统毫无反应设备管理器多出一个带黄色感叹号的“PCI 简单通讯控制器”。厂商光盘里没有 Windows 驱动只有寄存器手册硬件 ID 是 PCI\VEN_10EEDEV_9128。接到这类任务时最省事的做法是拿一套能用的 WDM 驱动框架作为起点而不是从零去读 WDK 文档。这个压缩包内正好是一份完整的 WDM PCI 驱动样例HelloWDM 驱动工程外加一个 Test.exe 用户态测试程序覆盖了驱动入口、PnP AddDevice、设备控制 IRP 和 INF 安装文件。对做 Windows 底层开发的人读这份源码能理清 PCI 设备从枚举到打开句柄的完整路径对只负责集成的人也能直接改 VID/DID 和配置空间查询逻辑把它变成一块板卡的基础驱动。2. WDM 与 PCI 的接口层配置空间、设备栈和 IRP 分发PCI 设备在 Windows 下并不是由寄存器直接驱动的。PnP 管理器在枚举 PCI 总线时会通过总线驱动读取设备的配置头得到 Vendor ID、Device ID、Class Code、中断引脚和 BAR 地址然后为这个设备创建 PDO。功能驱动收到 PnP 通知后创建设备对象并附加到设备栈上。因此一个 WDM 驱动真正的工作量在于三块向上回应用户态 IRP、向下访问总线配置空间、中间处理好设备生命周期。2.1 配置空间是 PCI 设备的“身份证”枚举之后还得自己读配置空间前 64 字节是设备的关键信息。Vendor ID 和 Device ID 构成硬件 IDInterrupt Line 和 BAR 决定驱动如何与设备通信。枚举阶段读到的数据只能满足 PnP 匹配驱动真正启动后往往需要再次读取配置空间。原因很简单总线驱动只暴露必要的子集而 BAR 地址、扩展能力列表PCIe Capability这些细节需要功能驱动主动查询。WDM 下读取配置空间的常见做法是构造一个发给总线驱动的 IRPMinorFunction 设为 IRP_MN_READ_CONFIG然后 IoCallDriver 把它传下去。下面是一个示例实现NTSTATUS ReadPciConfig( PDEVICE_OBJECT fdo, ULONG offset, ULONG length, PVOID buffer) { PDEVICE_OBJECT lowerDevice; PIO_STACK_LOCATION nextStack; KEVENT event; PIRP irp; NTSTATUS status; lowerDevice IoGetAttachedDeviceReference(fdo); KeInitializeEvent(event, NotificationEvent, FALSE); irp IoAllocateIrp(lowerDevice-StackSize, FALSE); if (irp NULL) { ObDereferenceObject(lowerDevice); return STATUS_INSUFFICIENT_RESOURCES; } irp-IoStatus.Status STATUS_NOT_SUPPORTED; IoSetCompletionRoutine(irp, OnIrpComplete, event, TRUE, TRUE, TRUE); nextStack IoGetNextIrpStackLocation(irp); nextStack-MajorFunction IRP_MJ_PNP; nextStack-MinorFunction IRP_MN_READ_CONFIG; nextStack-Parameters.ReadWriteConfig.Offset offset; nextStack-Parameters.ReadWriteConfig.Length length; nextStack-Parameters.ReadWriteConfig.Buffer buffer; status IoCallDriver(lowerDevice, irp); if (status STATUS_PENDING) { KeWaitForSingleObject(event, Executive, KernelMode, FALSE, NULL); status irp-IoStatus.Status; } ObDereferenceObject(lowerDevice); IoFreeIrp(irp); return status; }这段代码里IoGetAttachedDeviceReference 拿到的是 PDO 所在的总线驱动设备对象。IO_STACK_LOCATION 中的 Parameters.ReadWriteConfig 字段描述要读取的配置空间偏移、长度和目标缓冲区。IRP 完成后由分配它的驱动负责 IoFreeIrp 释放否则会造成内核内存泄露。注意不要在功能驱动里直接用 WRITE_PORT_ULONG 访问 0xCF8/0xCFC 来读配置空间。PCIe 设备使用 ECAM 机制访问配置空间和传统 PCI 的 I/O 端口方式不同而且 Windows 已经对总线地址做了抽象绕开总线驱动容易踩到资源重映射和热插拔之后地址失效的问题。2.2 PDO、FDO 与设备栈驱动如何嵌入 PnP 树WDM 设备模型中总线驱动创建 PDO也就是“物理设备对象”。功能驱动在 AddDevice 回调里创建 FDO并把它挂在 PDO 上面。这串设备对象形成一个栈上层 IRP 从栈顶进入经过功能驱动处理后继续向下传给总线驱动。比如读取配置空间的 IRP 就是从 FDO 传给 PDO 的。NTSTATUS MyAddDevice(PDRIVER_OBJECT driverObject, PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo; NTSTATUS status; UNICODE_STRING deviceName; UNICODE_STRING symLink; PDEVICE_EXTENSION ext; RtlInitUnicodeString(deviceName, L\\Device\\HelloWDM0); RtlInitUnicodeString(symLink, L\\DosDevices\\HelloWDM0); status IoCreateDevice( driverObject, sizeof(DEVICE_EXTENSION), deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, fdo); if (!NT_SUCCESS(status)) { return status; } ext (PDEVICE_EXTENSION)fdo-DeviceExtension; ext-DeviceObject fdo; ext-Pdo pdo; status IoCreateSymbolicLink(symLink, deviceName); if (!NT_SUCCESS(status)) { IoDeleteDevice(fdo); return status; } ext-LowerDeviceObject IoAttachDeviceToDeviceStack(fdo, pdo); fdo-Flags | DO_BUFFERED_IO; fdo-Flags ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }IoCreateDevice 的第四个参数 FILE_DEVICE_UNKNOWN 表示设备类型第七个参数 FALSE 表示非独占设备允许多个应用同时打开句柄。IoAttachDeviceToDeviceStack 的返回值是下层 PDO 对应设备对象后续整个驱动生命周期都要靠它向下发送 IRP。创建 FDO 后必须清除 DO_DEVICE_INITIALIZING 标志否则设备栈无法完成启动。2.3 IRP 分发每一条命令都要有归宿用户态调用 CreateFile、ReadFile 或 DeviceIoControl 时I/O 管理器会构造对应 IRP并调用驱动对象 MajorFunction 表里的分派例程。WDM 驱动至少要处理 Create、Close、DeviceControl 三类非 PnP IRP。PnP 和 Power IRP 如果不能处理也必须打到下个设备对象不能直接返回成功。主功能号用途驱动内常见处理IRP_MJ_CREATE打开设备记录引用计数返回 STATUS_SUCCESSIRP_MJ_CLOSE关闭句柄释放用户态持有的资源IRP_MJ_DEVICE_CONTROLIOCTL 调用解析参数操作硬件IRP_MJ_PNP即插即用事件交给下层或处理 Start/Stop/RemoveIRP_MJ_POWER电源管理交给下层或处理 SetPower分派函数登记一般放在 DriverEntry 里。设备控制 IRP 使用 Method Buffered I/O 时输入输出缓冲区都会映射到 Irp-AssociatedIrp.SystemBuffer 中不需要自己锁定内存。NTSTATUS MyDispatchDeviceControl(PDEVICE_OBJECT devObj, PIRP irp) { PIO_STACK_LOCATION stack IoGetCurrentIrpStackLocation(irp); ULONG code stack-Parameters.DeviceIoControl.IoControlCode; ULONG outLen stack-Parameters.DeviceIoControl.OutputBufferLength; PVOID buffer irp-AssociatedIrp.SystemBuffer; NTSTATUS status STATUS_SUCCESS; ULONG info 0; switch (code) { case IOCTL_HELLOWDM_QUERY_VERSION: { if (outLen sizeof(ULONG)) { status STATUS_BUFFER_TOO_SMALL; break; } *(PULONG)buffer 1; info sizeof(ULONG); break; } default: status STATUS_INVALID_DEVICE_REQUEST; break; } irp-IoStatus.Status status; irp-IoStatus.Information info; IoCompleteRequest(irp, IO_NO_INCREMENT); return status; }IoStatus.Information 表示实际写入系统缓冲的字节数。如果输出缓冲区过小应返回 STATUS_BUFFER_TOO_SMALL并把需要的字节放在 Information 里。用户态 DeviceIoControl 的字节返回参数就是引用的这里。3. HelloWDM 源码工程逐文件拆解代码文件比 README 更有说服力这个包把代码以一个可编译的 DriverDev.sln 组织好。查看压缩包可以发现MyDriver 工程负责内核驱动Test 工程是用户态测试。熟悉项目结构后替换设备 ID 和功能逻辑会快很多。3.1 压缩包里每个文件在干什么先用一张表把主要文件的作用标出来避免拿到源码后到处找入口。文件/目录作用MyDriver.vcxproj / DriverDev.slnVS/WDK 工程文件包含编译配置HelloWDM.cpp / HelloWDM.h驱动入口、分派函数、设备控制逻辑Ioctls.h用户态与内核共用的 IOCTL 定义guid.h设备接口或设备实例 GUID 定义HelloWDM.infINF 安装文件定义硬件匹配和驱动部署MyDriver_Check / Debug编译输出或旧 DDK 构建目录Test / main.cpp / function.cppTest.exe 用户态程序源码MyDriver.dsp 是旧版 Visual C 工程文件用来兼容老 DDK 环境。现代 WDK 推荐直接用 vcxproj编译命令更清晰也不依赖 VC6 那套环境。3.2 DriverEntry、Unload 与设备扩展生命周期起点HelloWDM.cpp 里 DriverEntry 的逻辑很短但缺一不可。首先要给每个 IRP 主功能设置默认处理函数再覆盖 Create/Close/DeviceControl最后挂上 AddDevice 和 DriverUnload。extern C NTSTATUS DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) { ULONG i; (void)registryPath; driverObject-DriverUnload HelloWDMUnload; driverObject-DriverExtension-AddDevice HelloWDMAddDevice; for (i 0; i IRP_MJ_MAXIMUM_FUNCTION; i) { driverObject-MajorFunction[i] HelloWDispatchPassDown; } driverObject-MajorFunction[IRP_MJ_CREATE] HelloWDispatchCreateClose; driverObject-MajorFunction[IRP_MJ_CLOSE] HelloWDispatchCreateClose; driverObject-MajorFunction[IRP_MJ_DEVICE_CONTROL] HelloWDispatchDeviceControl; return STATUS_SUCCESS; }HelloWDispatchPassDown 是一个把 IRP 原封不动往下传的函数对 PnP 和 Power IRP 尤其重要。如果不处理而直接返回成功会破坏 PnP 设备栈的状态导致设备在枚举后直接停止。DriverUnload 则必须释放 DriverEntry 阶段申请的资源比如删除设备对象和符号链接。设备扩展结构体定义在 HelloWDM.h 里包含 FDO、PDO、下层设备对象、自旋锁或中断对象等。WDM 驱动不能使用全局变量保存设备实例状态因为同一个驱动可能同时服务多个设备。设备扩展可以理解为“附着在 FDO 上的私有上下文”。3.3 INF 文件系统如何匹配到你的驱动INF 是安装文件不是编译器产物。Windows 在枚举到 PCI 设备后会根据硬件 ID 查系统驱动库。INF 里的匹配节指定了这个硬件应该由哪个 .sys 驱动服务。[Version] Signature $WINDOWS NT$ Class System ClassGuid {4d36e97d-e325-11ce-bfc1-08002be10318} Provider %ProviderName% DriverVer 06/12/2024,1.0.0.0 [Manufacturer] %ProviderName%DeviceList,NTamd64 [DeviceList.NTamd64] %DeviceDesc%HelloWDM_DDI, PCI\VEN_1234DEV_5678SUBSYS_00000000REV_01 [HelloWDM_DDI.NT] CopyFiles HelloWDM_Files [HelloWDM_Files] HelloWDM.sys [HelloWDM_DDI.NT.Services] AddService HelloWDM, 0x00000002, HelloWDM_Service [HelloWDM_Service] DisplayName %DeviceDesc% ServiceType 1 StartType 3 ErrorControl 1 ServiceBinary %12%\HelloWDM.sys硬件 ID 行中的 PCI\VEN_1234DEV_5678 就是设备在配置空间里的身份。开发时把硬件 ID 改成自己板卡的 Vendor ID / Device ID。StartType3 表示由 PnP 管理器按需启动不能设成 0否则驱动会进入启动驱动列表。ServiceBinary 的 %12% 是驱动目录的替代路径实际指向 C:\Windows\System32\drivers。注意如果 INF 里的 ClassGuid 和 Class 对不上设备可能被安装到错误分类出现“系统设备”下的怪异节点。复制到标准 PCI 设备类时Class 常设为 System 或 Net视板卡功能而定。4. 从源码到可运行驱动WDK 构建、签名加载与 Test 程序验证拿到源码并不等于驱动能装进系统。从编译、签名到安装每一步都可能以蓝屏或者“设备无法启动”收场。先在一个干净环境里练熟流程再换成真实硬件 ID。4.1 用命令行构建工程脱离 Visual Studio 界面安装 WDK 后系统里有“开发人员命令提示符”它已经设好了 INCLUDE 和 LIB 环境变量。直接对 DriverDev.sln 或 MyDriver.vcxproj 执行 msbuild 即可msbuild MyDriver.vcxproj /p:ConfigurationDebug /p:Platformx64 /m/m 允许并行编译编译速度更快。Debug 还是 Release 会影响优化等级和调试符号生成内核驱动建议先用 Debug 构建方便抓崩溃地址和变量值。编译完成后输出目录里会有 HelloWDM.sys 和 PDB 文件。若工程配置有问题可以追加 /t:Rebuild 强制重编译避免旧对象干扰。如果是从旧 DDK 时代带过来的 .dsp不用强行转 vcxproj。老方法是用 build.exe sources/dirs 文件但新 WDK 已经不鼓励这条路。建议直接把源码导入新工程DDK 的 build 环境在 Windows 10 之后的驱动开发里已经不是主流。4.2 测试签名让 64 位系统接受你的驱动64 位 Windows 要求内核模块有签名。开发阶段没有 EV 证书可以启用测试签名模式bcdedit /set TESTSIGNING ON重启后桌面右下角出现“测试模式”水印。接着用 WDK 提供的 Inf2Cat 和 SignTool 生成目录文件并签名Inf2Cat /driver:C:\wdm_driver\x64\Debug /os:10_X64 signtool sign /v /s PrivateCertStore /n WDMTestCert /t http://timestamp.digicert.com C:\wdm_driver\x64\Debug\HelloWDM.catInf2Cat 会扫描 INF 文件中的 CopyFiles 和 AddService生成对应的 .cat 安全目录文件。signtool 的 /s 参数指定证书存储/n 指定证书名称。如果没有现成的测试证书先创建自签名证书再把它导入“受信任的根证书颁发机构”。这一步不做设备管理器会提示“数字签名错误”即使硬件 ID 匹配也装不上驱动。4.3 Test 程序从用户态进入内核态Test 工程里的 main.cpp 打开设备路径 \\.\HelloWDM这和 INF 安装后创建的符号链接一致。程序通过 DeviceIoControl 把控制码发送给驱动和驱动内的 IRP_MJ_DEVICE_CONTROL 对应。#include windows.h #include stdio.h #include Ioctls.h int main() { HANDLE handle CreateFile( L\\\\.\\HelloWDM, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL ); if (handle INVALID_HANDLE_VALUE) { printf(CreateFile failed: %d\n, GetLastError()); return 1; } ULONG version 0; DWORD bytesReturned 0; BOOL ok DeviceIoControl( handle, IOCTL_HELLOWDM_QUERY_VERSION, NULL, 0, version, sizeof(version), bytesReturned, NULL ); if (ok) { printf(Driver version: %lu\n, version); } else { printf(DeviceIoControl failed: %d\n, GetLastError()); } CloseHandle(handle); return 0; }CreateFile 的打开动作会触发 IRP_MJ_CREATE关闭句柄触发 IRP_MJ_CLOSE。DeviceIoControl 的参数依次是设备句柄、IOCTL 码、输入缓冲区、输入长度、输出缓冲区、输出长度、返回字节数和 OVERLAPPED 结构。最后一个参数传 NULL 表示同步等待驱动完成 IRP 后才返回。如果驱动里没有把 IoStatus.Information 设置正确bytesReturned 会返回 0但调用状态仍然是成功。4.4 用 DebugView 查看内核日志驱动内使用 DbgPrint 或 KdPrint 打印的信息不会直接出现在终端。在调试器没连接的情况下可以用 DebugView 的 System 模式捕获 OutputDebugString 输出。启动 DebugView勾选 Capture Kernel 和 Enable Verbose Kernel Output插入硬件后就能看到驱动在 AddDevice 和 Dispatch 里打印的时间点。这个技巧在驱动加载失败时尤其管用能看到是进入 DriverEntry 还是 AddDevice 崩掉的。5. 从 HelloWDM 到 PCIe 板卡的实战边界中断、DMA 和资源验证HelloWDM 示范了如何枚举设备、接收 IOCTL但它对真实板卡还有一个缺口没有中断处理也没有 DMA 传输。如果目标是网卡、数据采集卡这类需要持续搬运数据的 PCIe 设备还需要补三块内容。5.1 中断从 ISR 到 DPC 的完整链路WDM 驱动通过 IoConnectInterruptEx 连接中断中断处理后通常还要插一个 DPC 做延迟处理。因为 ISR 运行在 DIRQL 上只能做最少的操作比如读中断状态寄存器、清中断、提交 DPC。如果 ISR 里调用 IoCallDriver 或者操作页面化内存系统会立刻蓝屏。很多从裸机程序转过来的人喜欢在中断里跑复杂逻辑这是 WDM 驱动最常见的坑。5.2 DMA必须使用 DMA 适配器PCIe 板卡和主机内存之间的数据传输不能直接在驱动里写一个 memcpy 到 BAR 地址。正确做法是调用 IoGetDmaAdapter 分配适配器对象再用 MapTransfer 建立散列/聚集列表。只有这样总线主控 DMA 才能跨 4GB 边界寻址也才能被 IOMMU 正确翻译。没有 DMA 适配器而强行操作物理地址在启用 VT-d 或 ACPI DMAR 的机器上会悄然失败。5.3 安装后第一时间验证的内容驱动装完先别急着跑功能用设备管理器和 WinDbg 确认三件事设备状态是否为“此设备工作正常”、中断是否出现在资源里、访问 BAR 地址能否读回设备 ID。WinDbg 连接内核后可以查看设备栈命令检查内容!devobj FDO 地址FDO 标志设备栈状态!devstack 设备对象设备对象层次是否正确!irp 地址IRP 未被完成的执行路径!pci 100当前 PCI 设备总线资源分配如果设备对象标志里 DO_DEVICE_INITIALIZING 还没清除说明 AddDevice 没走到最后。IRP 卡在 IoCallDriver 说明下层总线驱动没有响应通常和配置空间读写长度不对有关。我在处理一块 PCIe 桥片时遇到过配置空间只能按 4 字节读否则后面挂死的情况这类细节只能在调试器里观察。把 HelloWDM 的 IOCTL 框架保留替换成寄存器读写和中断DPC 实现就是一套可用的 PCIe 板卡驱动雏形。本文还有配套的精品资源点击获取
返回列表