ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows XP下PCI设备WDM驱动开发实战:从DDK环境到中断、踩坑与调试

Windows XP下PCI设备WDM驱动开发实战:从DDK环境到中断、踩坑与调试 简介面向 Windows XP 平台的 PCI 设备驱动程序开发资料包适合需要掌握 WDM 驱动模型、内核模式编程以及硬件中断/DMA 交互的驱动开发者与系统程序员。内容覆盖 PCI 设备接口与 ID 识别、驱动层次划分、PnP 设备枚举和资源分配、IRP 分发处理、ISR/DPC 中断响应、DMA 传输管理也涉及 Driver Studio/DDK 等驱动开发与调试工具的使用以及签名安装流程能帮助读者从框架搭建到部署排错建立完整思路。压缩包为 RAR 格式共 100 个文件大小约 1.53MB主要文件包括 15 个 cpp 源码、18 个 h 头文件、34 个 bin 二进制数据配合 dsp/dsw 工程文件、inf 安装配置和编译中间产物基本构成一套可对照学习的 PCI 驱动示例工程。已有 419 人学习浏览适合作为 Windows XP 下 WDM/PCI 驱动开发的学习模板和排错参考。对初次接触者而言这些源码、头文件与工程配置能直接提供代码层面的参照帮助减少从零搭建环境与试错的时间。1. 一块带PCI接口的板卡凭什么能在Windows XP里被你的程序看到Windows XP早被微软扫进历史可工厂里那些带PCI接口的采集卡、运动控制卡、国产加密卡还在昼夜跑很多只认XP和32位。要让它工作你不能改硬件只能写一段内核态的设备驱动程序去配置PCI配置空间、申请BAR内存、接住中断。这篇文章讲的就是这条老路怎么走通从DDK环境搭建到可编译的WDM驱动骨架再到资源分配和踩坑记录照着做能在一周内让一块自研PCI板卡在XP下被你自己的程序控制。适合还在维护工控机、老设备上位机的工程师也适合想补课内核驱动但只有XP环境的学生。2. 开发环境与驱动模型选型XP时代的PCI驱动两条路线2.1 WDM还是KMDF老设备驱动不再纠结的选择先别急着写代码选模型。XP下给PCI设备做驱动主流是WDMWindows Driver Model和KMDFKernel-Mode Driver Framework两种路线。理论上XP可以跑KMDF但实际维护里你会发现KMDF运行库是个二进制补丁版本不匹配就会让INF加载失败而且它要求你在安装驱动的同时把wmflib.sys打个底现场老设备的装机流程往往会漏这一步。相比之下WDM不依赖运行库所有IRP分发和PnP逻辑都写在你的sys文件里代码量大但成败完全自己可控。我一般直接选WDM还有一个现实原因很多厂里现有板卡驱动都是2005到2010年的WDM源码你可能要读懂、魔改它而不是重来一套。WDM的核心理念是“分层即插即用”系统通过IRP包跟驱动对话驱动要处理热插拔、电源状态和IO请求。对PCI设备来说你要做的事情看起来很少——系统说“设备来了”你建个对象系统说“开始干活”你去拿资源之后实现一两个读写函数。可少不代表容易光是资源类型和IRQL就够新手熬三个通宵。如果你手上是全新设计的板卡并且只在XP上跑那我建议也别碰KMDF。微软当年给XP的KMDF版本号停留在一个比较老的状态遇到新版WDK编译的KMDF驱动反而需要额外装库收益远小于折腾成本。2.2 把DDK装好最小可编译环境XP下的内核驱动必须用老版DDK编译推荐Windows Server 2003 SP1 DDK版本2600内核结构和XP SP2差不多。别再装WDK 8/10那套它默认生成基于NT6.x结构的驱动虽然可以配置Target OS Version但对XP支持很糟糕生成的东西容易在XP机上“无法找到入口”。安装DDK时选“Build Environment”完成后开始菜单里会出现一个“Windows XP DDK 2600 Free Build Environment”。这个控制台自带一套INCLUDE和LIB环境变量指向DDK的ntddk头文件与link库。驱动必须在这个环境里用build命令编译别拿Visual C 6.0直接建工程——不是不行但你会被宏定义、调用约定和驱动入口的extern C问题折磨到怀疑人生。如果你用VC6写应用测试程序那只负责用户态部分驱动本体还是要在DDK环境里编。我见过有人在同一个工程里混着编驱动和MFC程序最后链接出的sys带上了C运行时初始化一加载蓝屏代码3都算轻的。2.3 Sources文件和makefile第一次build的完整配置在DDK的build环境里一个驱动项目目录至少要有三个文件sources、makefile、driver.c。makefile内容固定大多是这副面孔# # DDK build system 入口内容固定 # !INCLUDE $(NTMAKEENV)\makefile.def到这里很多新手会问“makefile里什么都没写怎么编译”。其实是$(NTMAKEENV)\makefile.def引入了DDK预设的全部规则sources文件才是你的配置主战场下面是一份迷你版TARGETNAMEpci_demo TARGETPATHobj TARGETTYPEDRIVER INCLUDES..\inc;$(DDK_INC_PATH) SOURCESdriver.c pci_config.c INFdriver.inf # 如果你要用到系统未导出的函数可以加链接库 # TARGETLIBS$(DDK_LIB_PATH)\hal.lib # 调试版本定义方便在源码里条件编译 C_DEFINES-DPCI_DEMO_DEBUG1参数说明TARGETNAME决定生成的.sys文件名pci_demo.sysTARGETTYPEDRIVER告诉构建系统这是个驱动程序而不是EXESOURCES按顺序列出所有.c文件INF指定随驱动安装的信息文件C_DEFINES里加PCI_DEMO_DEBUG1让源码里#ifdef PCI_DEMO_DEBUG的调试代码生效。这里的INCLUDES是处理你自有头文件的搜索路径DDK自带路径用$(DDK_INC_PATH)占位。第一次build你在控制台里执行cd /d 项目目录 build正常情况下你会看到它先跑预处理、再编译、再链接最后输出到obj\chk或obj\fre目录。chk是检查版checked带调试信息和断言检查速度慢很多但适合开发前期fre是免费版free和发布版等价只输出精简代码。你在开发阶段一定要用“Free Build Environment”不要用“Checked Build Environment”。开始菜单里有两个控制台一个叫free一个叫checked选check那个否则KdPrint全被剪掉调试输出毛都看不到。如果中途改了.h头文件build依赖不识别常常出现“明明改了但行为不变”的诡异现象这时候执行build -c -b -z-c清理-b重建-z临时禁用增量编译。我平均每改三次头文件就会碰到一次需要全量重建不清理的话链接器会拿旧.obj硬怼出个新的sys那种“黑匣子”般的排错体验实在不值得经历。2.4 编译时最常见的三分钟报错解决符号和调用约定新手第一次build几乎必看这两个错第一是“unresolved external symbol _IoCallDriver8”这是没包含ntddk.h或者链接库路径不对第二是“error C2059: syntax error : string”通常是你在DriverEntry用了Unicode字符串但Windows.h没包含。老实说这些都能靠多读两遍官方示例绕过去。真正让人头疼的是调用约定驱动函数默认用_stdcall编译器如果被DK的全局选项覆盖你的AddDevice回调就会变成_cdecl系统找不到对应入参导致蓝屏。这个问题现在少见因为DDK的sources里隐式带了约定声明我提醒一句只是心疼那些还在VC6里手搓驱动的老伙计。3. 一个能跑起来的PCI驱动骨架从DriverEntry到AddDevice3.1 DriverEntry里除了登记回调什么都别做写WDM驱动启动入口叫DriverEntry它跟普通C程序的main完全不同。系统加载驱动后DriverEntry拿到的是一堆内核数据结构你的任务只是把这些数据结构填好把处理函数挂到驱动的分发表上然后立刻返回成功。所有“设备来了没有”的判断都轮不到你因为此刻设备还不存在PnP管理器还没枚举。看一段最简入口#include ntddk.h // 前置声明 NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo); NTSTATUS PnpDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS PowerDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS IoControlDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS DispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS DispatchClose(PDEVICE_OBJECT DeviceObject, PIRP Irp); VOID OnUnload(PDRIVER_OBJECT DriverObject); NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath ) { // 添加即插即用设备回调 DriverObject-DriverExtension-AddDevice AddDevice; // 卸载回调 DriverObject-DriverUnload OnUnload; // 注册各类IRP的分发函数 DriverObject-MajorFunction[IRP_MJ_PNP] PnpDispatch; DriverObject-MajorFunction[IRP_MJ_POWER] PowerDispatch; DriverObject-MajorFunction[IRP_MJ_DEVICE_CONTROL] IoControlDispatch; DriverObject-MajorFunction[IRP_MJ_CREATE] DispatchCreate; DriverObject-MajorFunction[IRP_MJ_CLOSE] DispatchClose; return STATUS_SUCCESS; }逻辑说明DriverObject里每个MajorFunction槽位对应一类IRP。你没设置处理的槽位系统就按默认方式处理——对PNP而言默认往往是直接失败所以PnP和Power必须显式挂。DriverUnload用于卸载时做清理但WDM驱动真正的清理大多在RemoveDevice里做这里只放一个空函数。RegistryPath参数能拿到驱动在注册表里的服务路径比如\Registry\Machine\System\CurrentControlSet\Services\pci_demo你会发现自己的驱动配置键就在那里。新手常想在DriverEntry里读注册表最好别做——你的设备还可能没安装服务路径里的数据不一定存在。3.2 AddDevice创建设备对象把设备信息挂进设备扩展当PnP管理器枚举到匹配的PCI设备就会调用AddDevice。你得在这里创建功能设备对象FDO并把设备变量全挂到一个名为DEVICE_EXTENSION的结构里。这个结构特意不给你任何默认定义就是为了让你自由发挥。先定义自己的设备扩展typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT fdo; PDEVICE_OBJECT lowerDevice; // 设备栈下层 UNICODE_STRING devName; // 调试用名称 PHYSICAL_ADDRESS memBar0; ULONG memBar0Len; PVOID mappedBar0; ULONG intVector; ULONG intLevel; KAFFINITY intAffinity; PKINTERRUPT interruptHandle; PIO_WORKITEM workItem; BOOLEAN started; } DEVICE_EXTENSION;然后在AddDevice里分配和初始化NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo; PDEVICE_EXTENSION dx; NTSTATUS status; UNICODE_STRING name; RtlInitUnicodeString(name, L\\Device\\PciDemo_0); status IoCreateDevice( DriverObject, sizeof(DEVICE_EXTENSION), name, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, fdo); if (!NT_SUCCESS(status)) return status; dx (PDEVICE_EXTENSION)fdo-DeviceExtension; RtlZeroMemory(dx, sizeof(DEVICE_EXTENSION)); dx-fdo fdo; dx-started FALSE; RtlInitUnicodeString(dx-devName, LPciDemo0); // 把FDO挂到设备栈 dx-lowerDevice IoAttachDeviceToDeviceStack(fdo, pdo); if (!dx-lowerDevice) { IoDeleteDevice(fdo); return STATUS_NO_SUCH_DEVICE; } // 用户态使用IOCTL时采用缓冲IO方式 fdo-Flags | DO_BUFFERED_IO; fdo-Flags ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }逻辑说明IoCreateDevice创建FDO第二个参数sizeof(DEVICE_EXTENSION)告诉系统给设备扩展预留空间。设备扩展是驱动里唯一能存放每个设备私有数据的地方因为你可能有多个相同PCI设备插入绝不能把它们塞进全局变量。IoAttachDeviceToDeviceStack把FDO挂到已有设备栈上返回下层设备对象往后你不处理的IRP得转交给它。最后清掉DO_DEVICE_INITIALIZING标志PnP管理器才知道初始化已完成——一旦你忘记驱动永远等不到IRP_MN_START_DEVICE。这个DEVICE_EXTENSION里的lowerDevice非常重要几乎是所有PnP驱动转发IRP的命根子。它得在AddDevice阶段就存下来之后你那几个分发函数全靠IoSkipCurrentIrpStackLocation然后IoCallDriver把IRP送下去。你还要理解一件事pdo是物理设备对象由总线驱动创建你创建的fdo挂它上面组成设备栈。3.3 用build命令编译并安装INF与设备管理器上一章已经写好了Sources和makefile这一节把INF补全并说明安装步骤。INF是驱动对外的身份证里面的硬件ID必须跟PCI设备的VEN/DEV完全匹配。[Version] Signature $WINDOWS NT$ Class System ClassGUID {4D36E97D-E325-11CE-BFC1-08002BE10318} Provider %Vendor% DriverVer 04/04/2024,1.0.0.0 [Manufacturer] %Vendor%DeviceList, NT.5.1 [DeviceList] %DeviceDesc%InstallSection, PCI\VEN_1234DEV_5678 [DeviceList.NT.5.1] %DeviceDesc%InstallSection, PCI\VEN_1234DEV_5678 [InstallSection.NT] CopyFilesDriverFiles [DriverFiles] pci_demo.sys [InstallSection.NT.Services] AddServicepci_demo,2,ServiceInstallSection [ServiceInstallSection] ServiceType 1 StartType 3 ErrorControl 1 ServiceBinary %12%\pci_demo.sys [Strings] VendorPciDemo DeviceDescPci Demo Board逻辑说明PCI\VEN_1234DEV_5678必须替换成你设备实际的硬件ID可到设备管理器→设备属性→详细信息→硬件ID里查看。NT.5.1表示该节适用于Windows XPNT 5.1。StartType3是“按需启动”不要在开机时自动载入一个还没被枚举到设备老老实实等PnP触发。%12%代表系统盘\System32\drivers目录ServiceBinary就指向被复制的驱动文件。安装时在设备管理器里找到带问号的PCI设备右键“更新驱动程序”选择“从列表或指定位置”指向包含INF的目录。如果安装后设备仍显示黄色感叹号多半是硬件ID不匹配或驱动文件没复制过去。你可以在设备管理器右键看状态系统会给源码级别的错误提示比如“Windows 无法加载这个硬件的设备驱动程序。驱动程序可能已损坏或不见了。 (代码 3)”——这我在第5章里会展开说。装好之后你可以立刻用之前写的DispatchCreate做一次最简单的测试在应用层用CreateFile打开设备的\\\\.\\PciDemo_0路径看能不能成功。如果CreateFile返回有效句柄证明驱动加载和设备的I/O路径已经通了一半。4. PCI资源怎么拿配置空间、BAR映射与中断处理4.1 读取PCI配置空间交出BAR地址的两种惯用姿势PnP启动设备时系统已经帮你把所有资源分配好了BAR地址、中断向量、等等。驱动最规范的做法是在IRP_MN_START_DEVICE里从资源列表取现成的值而不是自己去配置空间扒地址。但调试时你也许想确认BIOS有没有给这个板卡分配BAR两种姿势都值得会。第一种姿势从PnP的资源表拿最终分配NTSTATUS OnStartDevice(PDEVICE_OBJECT fdo, PIRP Irp) { PIO_STACK_LOCATION irpSp IoGetCurrentIrpStackLocation(Irp); PDEVICE_EXTENSION dx (PDEVICE_EXTENSION)fdo-DeviceExtension; PCM_RESOURCE_LIST raw irpSp-Parameters.StartDevice.AllocatedResources; if (raw) { for (ULONG i 0; i raw-Count; i) { PCM_PARTIAL_RESOURCE_LIST list raw-List[i]; for (ULONG j 0; j list-PartialResourceList.Count; j) { PCM_PARTIAL_RESOURCE_DESCRIPTOR rd list-PartialResourceList.PartialDescriptors[j]; if (rd-Type CmResourceTypeMemory) { dx-memBar0 rd-u.Memory.Start; dx-memBar0Len rd-u.Memory.Length; } else if (rd-Type CmResourceTypeInterrupt) { dx-intVector rd-u.Interrupt.Vector; dx-intLevel rd-u.Interrupt.Level; dx-intAffinity rd-u.Interrupt.Affinity; } } } } dx-started TRUE; return STATUS_SUCCESS; }逻辑说明AllocatedResources是操作系统最终拍板给硬件的资源包含Memory、Interrupt、DMA等描述符。你要在这里把需要的项存进设备扩展留给后面的映射和中断注册用。注意RequirementsList不是给驱动用的那是问驱动“这些备选能不能凑合”别拿错拿错你会得到一个不同机器上表现不一致的驱动。第二种姿势通过HalGetBusDataByOffset硬读配置空间BUS_SPECIFIC_DATA busData; busData.Number 1; // 总线号常为0 busData.Slot 0x10; // 设备插槽号由PCI ID推导或从BIOS看到 busData.Function 0; PCI_COMMON_CONFIG config; ULONG length HalGetBusDataByOffset( PCIConfiguration, busData.Number, busData.Slot, busData.Function, config, 0, // 偏移0开始 sizeof(config)); if (length 64) { // config.u.type0.BaseAddresses[0] 即BAR0 }逻辑说明PCIConfiguration是总线类型常量从偏移0读取64字节标准配置头。这个方法需要自己计算bus.device.function如果电脑有多条PCI总线很容易搞错。在实际项目中我只在驱动开发早期用它打印BAR值验证PnP给的地址对不对正式驱动里一律用资源表。4.2 把BAR映射到虚拟地址MmMapIoSpace的教训拿到BAR物理地址还不能直接拿它当指针对读写。内核态必须在另一张页表里建立虚拟地址映射标准API是MmMapIoSpace。要注意物理地址是PHYSICAL_ADDRESS也就是64位结构别丢高32位。dx-mappedBar0 MmMapIoSpace(dx-memBar0, dx-memBar0Len, MmNonCached); if (!dx-mappedBar0) { DbgPrint(PCI_DEMO: map BAR0 failed\n); // 不能在这里直接失败不返回先转交下层 IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(dx-lowerDevice, Irp); }逻辑说明第三个参数MmNonCached强制映射为非缓存。PCI设备寄存器基本都是“读一次变一次”的CPU缓存会把你害惨——你会读到陈旧的状态中断掉了都发现不了。这也是新手翻车最多的点BAR映射没问题但板卡寄存器咋刷新都读不出新值。选MmNonCached之后所有对该区域的读写都是直通PCI总线代价是性能纹丝不动但这正是寄存器该有的行为。memBar0Len建议只映射实际用到的部分。比如BAR0报2MB映射空间但你只用前面4KB就别整个映射——虽然真映射也行但会多占系统PTE非分页池对工控机这种常年不重启的环境是种浪费。映射完成后可以做个快速自检volatile ULONG val; val READ_REGISTER_ULONG((PULONG)((UCHAR *)dx-mappedBar0 REG_VERSION)); DbgPrint(PCI_DEMO: version reg 0x%08X\n, val);逻辑说明READ_REGISTER_ULONG和直接解引用的区别在于它告诉你这是IO映射的寄存器会做总线屏障避免编译器乱序优化。读一次之后你至少能确认地址对不对、硬件有没有反应。如果读出来还是0xFFFFFFFF那多半是板卡没上电或者BAR映射根本不对立刻回头查配置空间。4.3 中断服务例程与DPC减少ISR里工作的门道PCI设备的中断在XP下通常通过INTx线请求系统把中断向量分配给驱动。驱动要在StartDevice阶段注册中断服务例程也叫ISR。操作顺序先连接中断后撤掉映射不应该先映射BAR再连中断因为ISR要读你的中断状态寄存器没有映射就是历史大坑。注册中断的代码如下NTSTATUS status IoConnectInterrupt( dx-interruptHandle, PciSampleIsr, // ISR例程 dx, // 服务例程上下文 NULL, // 同步自旋锁可不给 dx-intVector, dx-intLevel, dx-intAffinity, FALSE); // 允许共享 if (!NT_SUCCESS(status)) { DbgPrint(PCI_DEMO: IoConnectInterrupt failed %08X\n, status); // 失败也要继续让IRP往下走由上层决定怎么擦屁股 }ISR里面写什么决定了驱动是稳如狗还是热重启BOOLEAN PciSampleIsr(PKINTERRUPT InterruptObject, PVOID Context) { PDEVICE_EXTENSION dx (PDEVICE_EXTENSION)Context; ULONG status READ_REGISTER_ULONG((PULONG) ((UCHAR *)dx-mappedBar0 INT_STATUS_OFFSET)); if (!(status 0x1)) { return FALSE; // 不是这个卡的中断交给别的ISR } // 写中断清除寄存器撤销请求。引脚一般低电平有效这里写1清 WRITE_REGISTER_ULONG((PULONG) ((UCHAR *)dx-mappedBar0 INT_CLEAR_OFFSET), 1); // 重活交给DPC工作项 IoQueueWorkItem(dx-workItem, PciProcessInterrupt, DelayedWorkQueue, dx); return TRUE; }逻辑说明ISR运行在你的DIRQL级别它抢占了系统当前所有线程所以只能做两件事读中断状态、清中断请求。任何锁、内存分配、调用其他驱动都严禁出现在这里。返回TRUE的意思是“这个中断我认领了”系统不会再把相同中断号发给别的驱动返回FALSE则继续往下传递。共享中断多网卡环境下返回错误会让另一块卡的中断被你的ISR不断误读性能肉眼可见地掉。IoQueueWorkItem把耗时的处理转给DPC在低优先级下执行。工作项在设备扩展里维护卸载时必须取消。ISR里的InterruptObject参数在连接时会绑定到KINTERRUPT对象这里其实用不到但保留有个好处如果后续想在DPC里确认是否同一个中断对象方便。中断处理和PnP时序要拧紧螺丝映射BAR、注册ISR、初始化工作项这三步全部成功才算StartDevice完成任何一个失败都应该进入一套清理回滚。否则系统给你发来一个“电源管理”IRP你在ISR还没注册的情况下收到设备暂停那蓝屏的速度快得你截不上图。5. XP PCI驱动避坑5个让人熬夜的经典问题5.1 “Windows 无法加载这个硬件的设备驱动程序。驱动程序可能已损坏或不见了。 (代码 3)”先事件日志再怀疑代码现象设备管理器里PCI设备黄色感叹号属性显示“Windows 无法加载这个硬件的设备驱动程序。驱动程序可能已损坏或不见了。 (代码 3)”。这条热词对应的提示是XP和后来系统最常见的驱动加载失败代码。原因代码3在XP语境里往往不是驱动文件损坏而是系统加载服务时找不到sys、入口点错误或INF服务名对不上。我自己就出过这种丑编译出的是pci_demo.sysINF里AddServicepci_demo没错但CopyFiles节把sys复制到了临时目录而系统从ServiceBinary指向的绝对路径找不到。解决打开事件查看器开始→运行→eventvwr看系统日志里Service Control Manager的报错它会明确给出哪个模块加载失败。如果报“找不到PciDemo.sys”检查ServiceBinary的%12%到底解析到哪个目录如果报“入口点DriverEntry无法位于PciDemo.sys”说明你的驱动链接了C运行时或调用约定不对回到DDK build环境重编。小技巧把inf改成Includendis.inf之类的纯语法错误让设备管理器在“手动更新”时给你更精确的提示。5.2 在AddDevice里访问硬件必蓝屏现象驱动装上后系统刚启动就蓝屏甚至还没进设备管理器就重启。事件日志里能找到IRQL_NOT_LESS_OR_EQUAL或UNEXPECTED_KERNEL_MODE_TRAP。原因AddDevice阶段PnP设备对象刚建出来设备还没进入“开始”状态——Bar没分配、电源没起来。这时候去读配置寄存器或IO端口会得到乱码更糟的是你的驱动还持有自旋锁去访问总线硬件没响应直接总线错误。解决铁律是AddDevice只建对象挂设备栈所有跟硬件相关的动作放到IRP_MN_START_DEVICE。如果板卡要求初始化时序必须在“挂栈”之前那也该通过IoCallDriver先把电源IRP发下去而不是自己莽。我写第一块驱动时就在AddDevice里读了BAR结果没一次能撑过启动后来把那段代码挪到StartDevice一切正常。那些古早源码里把配置读取写进DriverEntry的写法很多是NT4时代的遗产在XP下并不安全。5.3 IRQL_NOT_LESS_OR_EQUAL端口访问被IRQL挡了现象驱动跑几十分钟后蓝屏崩溃栈指向你读寄存器的那一行。参数经常是一个非法地址很多人以为是野指针其实是IRQL太高不能访问分页内存。原因XP里访问内存有IRQL限制。你在ISRDIRQL里调用MmMapIoSpace、IoGetCurrentIrpStackLocation这种需要IRQL≤DISPATCH_LEVEL的函数立刻炸雷。更常见的场景是你把BAR映射的缓冲区当成可分页内存访问IRQL一高就触发核态分页错误。解决先把IRQL拆清楚。你的ISR必须只能读写映射的寄存器地址这些RAM是非分页的其他所有事情丢进DPC或工作项。MmMapIoSpace本身返回值就是非分页空间但你要是在ISR里第一次通过该地址去访问一个尚未进入TLB的页面可能触发页错误——解决办法是在StartDevice时先对整个映射做一次预读强制把页表填上之后ISR访问就稳了。还有一类隐藏雷是READ_REGISTER_ULONG的地址没有按4字节对齐指令本身能工作但总线错误照样蓝屏。建议用__declspec(align(4))或让BAR偏移量对齐4的倍数。5.4 内存映射后页面属性不清读不到BAR寄存器现象映射后读BAR的寄存器同一地址反复读到固定值比如0x00000000或0xFFFFFFFF。板卡实测是正常换到Linux下读就故事翻转。原因这是老PCI驱动最玄学的坑之一。两块不同的主板BIOS对PCI BAR空间设置的MTRR缓存类型不一致或者你的驱动在MmMapIoSpace选错了缓存属性。虽然你写了MmNonCached但有些BIOS强制把某段地址设为回写CPU缓存了你的寄存器值。我有一次在A公司工控机上调好的驱动到B公司同型号主板上就“死机式读旧值”逼得我调半天才发现是BIOS的PCI资源回写设置导致的。解决用KeInvalidateRange刷新缓存区域或在StartDevice里做一次MmFlushTlb再访问。更省心的做法是强制把整个BAR区域用MmMapIoSpace映射成MmNonCached后再写几遍脏字节把TLB扇出去。如果你的板卡控制寄存器不多还可以考虑直接用READ_PORT_ULONG访问IO端口PCI IO空间避免所有缓存问题。注意BAR不一定是内存类型也可能是IO类型你要在配置空间里看BAR0的最低bitbit01表示IO空间bit00表示内存空间。5.5 卸载崩溃IRP_MN_REMOVE_DEVICE没处理彻底现象驱动在设备管理器中“卸载”或拔出PCI板卡时系统蓝屏有时还伴随“PAGE_FAULT_IN_NONPAGED_AREA”。原因RemoveDevice例程里你只调用IoDeleteDevice,但之前注册的中断还在ISR仍可能被硬件触发去访问已经释放的BAR映射。另一个原因是你在Remove前没取消DPC工作项工作项还在队列里等执行Context指向已释放的设备扩展就会悬空引用。解决RemoveDevice按这个顺序来1) 告诉硬件关中断或回库2)IoDisconnectInterrupt3) 取消所有挂起的DPC工作项并等待4)MmUnmapIoSpace释放BAR映射5)IoDetachDevice从设备栈摘掉6)IoDeleteDevice。千万别反序。为了确保工作项不再复活在Remove前把dx-startedFALSEISR里先判断这个标志不满足直接返回FALSE这样等中断切走之后就不会新产生工作项。还有就是要小心热插拔XP对PCI热插拔支持并不完善设备栈的PnP IRP处理顺序如果不对卸载时即使你的代码没错系统也会因为下层驱动的引用计数没清零而崩溃。6. 验证驱动的有效手段DebugView、WinDbg与驱动验证器6.1 用KdPrint给驱动装“摄像头”驱动没有printf但可以用KdPrint输出调试信息。XP下最好用的捕获工具是Sysinternals的DebugView开启“Capture Kernel”即可看到CPU0、CPU1等内核输出。最方便的是它支持本地捕获不用连串口线只要你的驱动是checked build编译。把必现路径都埋上输出#define DBG_LVL(level, fmt, ...) \ do { if (debugLevel (level)) \ KdPrint((PCI_DEMO: fmt \n, __VA_ARGS__)); \ } while (0) ULONG debugLevel 1; DBG_LVL(0, Entry DriverEntry); DBG_LVL(1, AddDevice on %wZ, dx-devName); DBG_LVL(2, StartDevice BAR0%08X len%d, dx-memBar0.LowPart, dx-memBar0Len);逻辑说明KdPrint在free build里会被完全移除所以开发期必须用checked环境。debugLevel这个变量可以做成注册表可配我习惯在DriverEntry里读一次服务键值省得每次改等级都要重编。DebugView的好处是低干扰但缺点是不能下断点、不能查内存——遇到蓝屏还得上WinDbg。6.2 驱动验证器主动让隐藏故障现形Windows XP自带Driver Verifier驱动验证器它是内核驱动黑匣子中的黑匣子。开启后系统会像偏执狂一样检查驱动的内存分配、IRQL切换、DPC延迟、对象引用把大多数常规运行测不出来的悬空指针提前引爆。开启命令verifier /standard /driver pci_demo.sys它会提示你重启生效。重启后如果驱动有违反规则系统会蓝屏并生成转储文件。设置转储位置verifier /flags 0x1 /driver pci_demo.sys /reboot分析转储用WinDbg加载驱动符号后执行!analyze -v可以看到崩溃代码所在函数名和参数。很多新手对verifier是又爱又恨——它把那些“偶发”、无法复现的bug变成必然让半夜被设备的崩溃日志吓醒成为过去式。我的经验是驱动开发到收尾阶段开verifier把机器放在那跑一晚第二天看结果只要没崩这块驱动在XP里的存活概率就从玄学变成了统计学。最后说个我自己的习惯内核里每个可能出错的分支我一定会先写一行KdPrint再写返回的代码。崩溃转储里最后一条输出往往就是钓出深水里那条大鱼的地点。XP下的PCI驱动开发调试手段比现代Windows差了不少可用的工具还是那几样绕不过去。希望你少熬几个夜早点把板卡调通——希望这篇能帮到你。本文还有配套的精品资源点击获取
返回列表