简介:Windows WDM模型鼠标驱动程序完整源代码,面向驱动开发初学者、底层系统开发者和需要定制HID设备的工程师。压缩包内共13个文件,以C++源文件、头文件、INF安装配置、Visual Studio工程文件及makefile构建脚本为主,覆盖WDM驱动开发的关键流程,包括设备对象创建与初始化、即插即用与电源管理、IRP请求处理、HID协议解析及数据上报。资源包体仅12KB,结构紧凑,适合逐模块精读;配套的INF文件详细说明了驱动安装与硬件匹配机制,可帮助理解Windows如何加载和绑定驱动。目前已有921人浏览学习,对照实际代码学习WDM架构远比单纯阅读理论更容易上手;同时这份源码也可作为开发自定义鼠标、键盘或其他HID过滤驱动时的基础模板,具备较强的工程参考价值。通过源码可以直观看到设备扩展、派遣函数、完成例程等WDM驱动特有概念的落地写法,对进一步学习过滤器驱动和总线驱动也有帮助。
1. 鼠标驱动源码切入:WDM 模型里最容易复现的驱动样本
你在设备管理器里看到“未知设备”的黄叹号,或者拿到“Windows 无法加载这个设备的驱动程序(代码 31)”的状态,通常不是鼠标硬件坏了,而是驱动的 INF 没有匹配上设备实例 ID,或是设备对象没有正确挂进设备栈。这套鼠标驱动程序源代码恰好是 WDM 开发里最完整的切片:vhidmou.inf 负责安装匹配,vmoudev.cpp 实现设备对象创建与 IRP 分发,vhidmou.cpp 封装 HID 协议,vhidmou.def 导出内核符号,外加 sources 和 dsp/dsw 工程文件构成了一个可编译可加载的完整驱动。它不做总线枚举,而是扮演“把输入数据搬到系统输入栈”的函数驱动,这个角色能把 WDM 的 PnP 机制、IRP 处理、HID 报告描述符三块核心知识串成一条线。对刚入门的开发者,这是除 hello world 之外第一个能看到硬件响应的实现;对有经验的工程师,也能用它校准自己对设备栈和 HID 协作关系的理解。
2. 从 vhidmou.inf 拆解 PnP 驱动的安装与匹配机制
2.1 INF 在 WDM 加载流程中的位置
WDM 驱动和 NT 式驱动在代码层面的差异并不大,真正的分水岭在于设备对象的关系:总线驱动枚举硬件后创建一个物理设备对象(PDO),函数驱动通过 AddDevice 回调把自己的设备对象挂到这个 PDO 上。PnP 管理器在中间做两件事,一是根据设备实例 ID 找到匹配的 INF,二是解析 INF 里的安装指令和服务注册信息,决定复制哪个 sys、注册什么服务。vhidmou.inf 就是这份“简历”,它决定了整个驱动能否进入系统。
INF 的解析发生在设备首次连接或手动更新驱动时。设备管理器把硬件 ID 与 INF 里各个模型节中的条目比对,命中的那一条会跳转到对应的安装节执行。下面是我对照这个项目精简后的核心片段:
[Version] Signature = "$WINDOWS NT$" Class = Mouse ClassGuid = {4d36e96f-e325-11ce-bfc1-08002be10318} Provider = %PROVIDER% DriverVer = 06/20/2024,1.0.0.0 CatalogFile = vhidmou.cat [Manufacturer] %MfgName% = VHIDMOU,NTx86 [VHIDMOU.NTx86] %VHIDMOU.DeviceDesc% = VHIDMOU_DDI, USB\VID_1234&PID_5678&MI_00 [VHIDMOU_DDI.NT] CopyFiles = VHIDMOU_CopyFiles [VHIDMOU_CopyFiles] vhidmou.sys [VHIDMOU_DDI.NT.AddService] DisplayName = %VHIDMOU.SvcDesc% ServiceType = 1 StartType = 3 ErrorControl = 1 ServiceBinary = %12%\vhidmou.sysSignature = "$WINDOWS NT$"声明了驱动面向 Windows NT 家族;Class = Mouse与ClassGuid对应系统鼠标类,这个 ClassGuid 决定设备最终显示在设备管理器的“鼠标和其他指针设备”分类下。CatalogFile指向驱动签名目录文件,在开启了驱动签名强制的 64 位 Windows 10/11 上,缺失有效签名文件会导致安装被直接拒绝,设备管理器状态往往就是代码 31 或代码 52。
硬件匹配发生在[VHIDMOU.NTx86]节:USB\VID_1234&PID_5678&MI_00是目标设备的硬件 ID,VHIDMOU_DDI是安装指令节的名字。如果你手上的鼠标 VID/PID 与之不同,改成自己的组合即可,甚至可以追加&REV_0100这样的版本字段来缩小匹配范围。这里的NTx86后缀限定了 x86 架构,64 位系统需要另写一个NTamd64条目,否则在 64 位机器上 PnP 管理器会直接跳过这个 INF。
2.2 安装指令节与 AddService 的语义
[VHIDMOU_DDI.NT]节里的CopyFiles = VHIDMOU_CopyFiles指定了文件拷贝任务,实际的[VHIDMOU_CopyFiles]节把 vhidmou.sys 复制到%12%这个系统目录,它对应的物理路径是\SystemRoot\System32\drivers,也就是内核驱动默认的存放位置。设备对象每个驱动文件名的最后一部分会被 I/O 管理器用于构造驱动对象名,比如 vhidmou.sys 对应\Driver\vhidmou,这在后续 WinDbg 调试时要直接用到。
[VHIDMOU_DDI.NT.AddService]在服务控制管理器(SCM)里注册一个内核服务。ServiceType = 1表示内核驱动服务,StartType = 3表示由 PnP 管理器在设备出现时动态启动,而不是开机时自启。ErrorControl = 1表示加载失败时仅记录错误到事件日志,不触发系统重启——对鼠标这类非 boot 关键驱动这是正确选择,如果你把它写成0x0之外的严重级别,反而会让排错变得更加困难。
这里有一个容易踩的坑:修改 INF 后必须回到设备管理器执行“更新驱动程序”,而不是仅仅把新 INF 复制到C:\Windows\INF目录。PnP 管理器对 INF 有缓存和信任级别判断,未签名的 INF 在 64 位系统上即便能被解析,匹配阶段也可能被降权。遇到代码 31 时,我一般先确认设备实例 ID 与 INF 中的硬件 ID 是否完全一致,再看事件查看器里Microsoft-Windows-Kernel-PnP/Configuration日志中是否记录了“未找到驱动程序”或“签名被拒绝”的条目。
2.3 INF 关键参数速查
| INF 字段/节 | 作用 | 排错时关注点 |
|---|---|---|
Class/ClassGuid | 决定设备在设备管理器中的分类 | 与设备类型不符时安装过程会弹兼容性警告 |
DriverVer | 驱动版本,供 PnP 比较新旧 | 版本号低于系统缓存中的驱动时不会安装 |
HardwareID | 设备枚举时用的匹配串 | 注意大小写、&REV_xxxx后缀是否一致 |
AddService | 注册驱动服务 | StartType=3是 PnP 驱动的标准值 |
CopyFiles | 定义 sys 文件去向 | %12%是 drivers 目录,别误写成%SystemRoot% |
NTx86/NTamd64 | 限定目标架构 | 架构不匹配时 PnP 管理器直接跳过该 INF |
现实里很多二次开发者不改代码,只把供应商的原版 INF 拷贝过来改几个字段,结果设备枚举正常但驱动始终不加载。这类问题多半出在架构限定节缺失或DriverVer低于系统内置驱动。对照这份 vhidmou.inf 的写法,可以看到它把架构限定写进了[Manufacturer]节,这是一种更符合 WDM 安装规范的表达方式,也便于后续扩展 amd64 条目。
3. vmoudev.cpp 中的设备对象创建与 IRP 分发骨架
3.1 DriverEntry 与驱动对象初始化
驱动的入口函数在系统加载驱动时被调用,它要做三件事:注册卸载例程、填充分发函数表、初始化必要的同步对象。vmoudev.cpp 中对应的实现骨架如下:
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { DriverObject->DriverUnload = VhIdmouUnload; DriverObject->MajorFunction[IRP_MJ_CREATE] = VhIdmouCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = VhIdmouCreateClose; DriverObject->MajorFunction[IRP_MJ_READ] = VhIdmouRead; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = VhIdmouDeviceControl; DriverObject->MajorFunction[IRP_MJ_PNP] = VhIdmouPnp; return STATUS_SUCCESS; }MajorFunction数组是 I/O 管理器分发 IRP 的依据,每个下标对应一类 IRP 主功能号,处理函数通过注册到数组里被回调。鼠标驱动至少要处理IRP_MJ_CREATE(打开句柄)、IRP_MJ_CLOSE(释放句柄)、IRP_MJ_READ(读取输入数据)和IRP_MJ_PNP(即插即用事件)。没有填充分发函数的 IRP 主功能号会由 I/O 管理器直接返回STATUS_INVALID_DEVICE_REQUEST,所以设计分发表时宁多勿缺。
DriverUnload在系统卸载驱动时被调用,注意它不等同于设备移除,IRP_MN_REMOVE_DEVICE处理逻辑与 unload 例程是两个路径。正式项目里我会把可分配资源的初始化放在 AddDevice 而不是 DriverEntry,因为 DriverEntry 返回失败后驱动对象未必会被清理,在入口里分配资源却无法在 DriverUnload 之外释放,很容易造成内核内存泄漏。
3.2 AddDevice 与设备栈挂载
PnP 管理器完成硬件匹配后调用驱动的AddDevice,这是 WDM 驱动真正创建设备对象的地点。在 vmoudev 里逻辑可以归纳成四步:创建设备对象、初始化设备扩展、清除初始化标志、挂到设备栈:
NTSTATUS VhIdmouAddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject) { PDEVICE_OBJECT deviceObject = NULL; PDEVICE_EXTENSION devExt; NTSTATUS status = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject); if (!NT_SUCCESS(status)) { return status; } devExt = (PDEVICE_EXTENSION)deviceObject->DeviceExtension; devExt->DeviceObject = deviceObject; devExt->PhysicalDeviceObject = PhysicalDeviceObject; deviceObject->Flags |= DO_BUFFERED_IO; deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; devExt->LowerDeviceObject = IoAttachDeviceToDeviceStack(deviceObject, PhysicalDeviceObject); if (devExt->LowerDeviceObject == NULL) { IoDeleteDevice(deviceObject); return STATUS_DEVICE_REMOVED; } return STATUS_SUCCESS; }IoCreateDevice的参数中,DeviceType用FILE_DEVICE_UNKNOWN即可,鼠标不是磁盘或串口这类有固有类型语义的设备;Characteristics传 0 表示没有特殊兼容需求,如果写成了FILE_DEVICE_SECURE_OPEN,打开设备时就要额外校验 ACL,可能导致服务账户打不开句柄。Exclusive参数必须传 FALSE,鼠标设备需要被终端服务、屏幕键盘等多个组件同时打开,设成独占会让系统输入栈初始化失败。
DeviceExtension是驱动私有的数据结构,I/O 管理器只负责分配和释放内存,不关心内容;这里用它保存上下层设备对象指针、暂停状态和电源状态。DO_BUFFERED_IO标志决定 READ 请求以系统缓冲区方式传给驱动。对鼠标这种单次几字节、低频的输入设备,这是最优选择,比DO_DIRECT_IO少一次内存映射开销,也避免用户缓冲区在内核态被直接解引用带来的安全问题。
后续的IoAttachDeviceToDeviceStack把驱动新建的设备对象挂到物理设备对象的设备栈上,返回值是栈中下一个设备对象的指针。所有驱动不处理的 IRP 都要转发给它,忘记保存或错误转发,IRP 就会在设备栈里悬空,设备管理器会报异常。注意 AddDevice 返回前必须清除DO_DEVICE_INITIALIZING标志,否则系统认为设备尚未完成初始化,后续所有打开请求都会被拒绝。
3.3 IRP 的分发与完成路径
以 READ 请求为例。应用层通过ReadFile读鼠标输入时,I/O 管理器构造 IRP 并调用驱动注册的处理函数。对这个 HID 鼠标的例子,设备驱动的 READ 处理逻辑是准备好一帧数据,然后完成 IRP:
NTSTATUS VhIdmouRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PDEVICE_EXTENSION devExt = DeviceObject->DeviceExtension; PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp); ULONG bytesToRead = irpSp->Parameters.Read.Length; NTSTATUS status; status = VhIdmouGetInputReport(devExt, Irp->AssociatedIrp.SystemBuffer, &bytesToRead); Irp->IoStatus.Status = status; Irp->IoStatus.Information = NT_SUCCESS(status) ? bytesToRead : 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }IoGetCurrentIrpStackLocation返回当前堆栈单元,IRP 每向下一层转发一次就有一个对应的IO_STACK_LOCATION,驱动必须基于自己的堆栈单元读取请求参数,而不是直接翻别人栈里的数据。Irp->IoStatus.Information对 READ 请求是实际读取的字节数,I/O 管理器用这个值更新应用层ReadFile返回的lpNumberOfBytesRead。填 0 会导致上层拿到空包却认为调用成功,而这种失败通常是静默的,最容易被误判成设备无数据。
对 PnP IRP 的处理略有不同。IRP_MJ_PNP有子功能码,如IRP_MN_START_DEVICE、IRP_MN_STOP_DEVICE、IRP_MN_REMOVE_DEVICE。WDM 驱动不是每个子功能都要自己处理,标准做法是只关心自己需要的子功能,未处理的通过IoSkipCurrentIrpStackLocation加IoCallDriver转发给下一层。处理 REMOVE_DEVICE 时,先 detach 再删除自己的设备对象,顺序写反会在系统日志里看到设备未正确删除的警告。
读取请求还要注意挂起语义。鼠标这类设备在硬件没有新数据时,常见做法是把 IRP 挂到队列并返回STATUS_PENDING,而不是立刻完成。挂起后必须有另一个中断或 DPC 来摘除 IRP 并完成它,否则进程会永远阻塞。如果驱动在 IRQL 为 DISPATCH_LEVEL 的 DPC 里调用了IoCompleteRequest,需要关注完成时是否会引发页面错误,这是鼠标驱动蓝屏的一个隐藏来源。
3.4 分发函数职责速查
| 主功能号 | 处理函数职责 | 常见失败表现 |
|---|---|---|
IRP_MJ_CREATE | 校验设备状态,允许共享打开 | 上层打开设备返回STATUS_ACCESS_DENIED |
IRP_MJ_READ | 取一帧输入报告并完成 IRP | 返回长度恒为 0,应用层报读取失败 |
IRP_MJ_DEVICE_CONTROL | 处理 HID 属性查询与报告读取 | 状态码 31 之外出现“参数错误” |
IRP_MJ_PNP | 透传非核心子功能,处理停止/移除 | 热插拔后设备节点残留 |
IRP_MJ_POWER | 处理系统电源状态转换 | 待机唤醒后设备不可用 |
设计分发表时,我建议把真正关心的主功能号注册一遍,未填写的保持 NULL。I/O 管理器遇到 NULL 分发函数会返回STATUS_INVALID_DEVICE_REQUEST,这在很多老驱动里是找不到 bug 的根源,因为新手通常以为“没注册就等于自动完成”。
4. HID 鼠标层:报告描述符与 vhidmou.cpp 的实现路径
4.1 为什么鼠标要走 HID 协议层
基于 USB 的鼠标天然是 HID 设备,即便这套源码里的鼠标是虚拟设备、不直接挂在 USB 总线上,在 Windows 输入体系里它仍然要按 HID 协议与 hidclass.sys 协作。HID 协议的价值在于用一套统一的“报告”描述设备能力,而不是为每种硬件发明接口。鼠标、键盘、游戏手柄共用同一套上报框架,上层只负责解析报告里的数据位。
vhidmou.cpp 这个模块的角色,是把底层采集到的位移和按键状态,组装成符合 HID 规范的 Input Report 并交给类驱动层。hidmouse.h 中定义的则是这些数据位、缓冲区大小的约定。从源码文件划分能看出设计意图:vmoudev 处理设备对象和 IRP 生命周期,vhidmou 处理 HID 协议转换,如果这个鼠标真的挂在 USB 总线上,上层逻辑几乎可以原封不动地复用。
4.2 鼠标报告描述符解析
报告描述符是 HID 设备向主机描述“数据长什么样”的字节序列。标准三键带滚轮鼠标的报告描述符,核心部分可以写成如下形式:
const UCHAR MouseReportDescriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x95, 0x03, // Report Count (3) 0x75, 0x01, // Report Size (1) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (Button 1) 0x29, 0x03, // Usage Maximum (Button 3) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x81, 0x02, // Input (Data, Var, Abs) 0x95, 0x01, // Report Count (1) 0x75, 0x05, // Report Size (5) 0x81, 0x03, // Input (Const, Var, Abs) 0x95, 0x03, // Report Count (3) 0x75, 0x08, // Report Size (8) 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x09, 0x38, // Usage (Wheel) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x81, 0x06, // Input (Data, Var, Rel) 0xC0, // End Collection 0xC0 // End Collection };HID short item 的格式是头字节加若干数据字节:头字节的低四位是标签类型和数据长度,高四位对应全局项、局部项或主项。0x05, 0x01中0x05表示全局项的 Usage Page,数据长度 1,后跟的0x01是 Generic Desktop 页;0x09是局部项 Usage,0x02在这个页里代表 Mouse 用途。真正定义报告布局的是0x81这个 Input 主项,后面单字节0x02表示Data | Variable | Absolute,而滚轮那一行用的是0x06,即Data | Variable | Relative。
这里有个经常搞混的点:按键在逻辑最小值 0、最大值 1 下用 Absolute 模式,X/Y 在 -127 到 127 范围内用 Relative 模式。如果把 X/Y 的0x06误写成0x02,系统会把位移解释成绝对坐标,鼠标指针会直接跳到屏幕某个位置,而不是按相对距离移动,这是报告描述符写错后最典型的现象。
Report Count 和 Report Size 共同决定一帧报告的字节长度。上面描述符里前 3 个 bit 是按键状态,5 个 bit 补齐成一个字节,后 3 个字节分别是 X、Y、Wheel,所以每帧 Input Report 是 4 字节。实际的 vhidmou.cpp 实现就是从设备读回原始数据后填充这 4 个字节:bit0 到 bit2 对应左右中键,第 2 字节是 X 偏移,第 3 字节是 Y 偏移,第 4 字节是滚轮增量。
4.3 HID 类驱动与 vhidmou.def 导出定义
与裸的 WDM 鼠标驱动不同,HID 设备在 Windows 里要跟 hidclass.sys 协作。hidclass.sys 负责管理 HID 集合和上层应用通信,底层函数驱动需要响应IOCTL_HID_GET_DEVICE_DESCRIPTOR、IOCTL_HID_GET_REPORT_DESCRIPTOR等 IOCTL。vhidmou.def 文件的作用是约束导出符号,保证这些处理函数能被 hidclass.sys 通过导出表解析到:
LIBRARY vhidmou EXPORTS DriverEntry_PRIVATE VhIdmouGetHidDescriptor VhIdmouGetReportDescriptorLIBRARY指定模块名,EXPORTS列表声明的符号如果与 sources 里的TARGETNAME不一致,链接器不会报错,但 Driver Verifier 会在加载时提示符号解析异常。对 WDM 驱动,导出表不是给应用层用的,而是给 hidclass.sys 这类内核组件动态解析,所以导出名要保持稳定,改了名字就要同步改上层调用方。
vhidmou 项目里保留着.dsp.dsw工程文件,这是 Visual C++ 6.0 时代的格式,现代 VS 打开时往往需要转换或者干脆手工建 CMake。sources 文件则是 WDK 的 nmake 编译脚本参数,TARGETNAME、TARGETTYPE=DRIVER、MSC_WARNING_LEVEL=/W4决定了生成 sys 的方式。在较新的 WDK 环境里,可以保留 sources 和 makefile,在命令行里直接调用build生成二进制,比对 VS 动辄附加一堆中间文件要稳定得多。
4.4 HID 报告在上层如何被消费
报告描述符只是静态声明,真正的数据流动是每帧 Input Report 从驱动发往 hidclass.sys。hidclass.sys 收到报告后,根据报告描述符里的 Usage 信息把数据映射成 Windows 的输入消息。鼠标的 X 偏移会被换算成屏幕上的像素位移,滚轮增量变成WM_MOUSEWHEEL消息里的滚动行数。这个链路决定了驱动侧的数据精度直接影响到系统鼠标速度,如果驱动把 X 偏移限制在 ±1,即使控制面板里把指针速度调到最高,鼠标移动依然很慢。
这套源码里 vhidmou 与 vmoudev 的分层还有一个好处:vmoudev 可以独立测试设备对象和 IRP 生命周期,vhidmou 的数据处理不依赖特定硬件。调试时我可以先给 VhIdmouGetInputReport 返回一组写死的坐标和按键值,确认上层能收到消息,再反过来调底层采集逻辑,把问题隔离到某一层。
| HID 层组件 | 职责 | 关联文件 |
|---|---|---|
| 底层采集 | 读取硬件寄存器或模拟数据源 | vmoudev.cpp |
| 协议封装 | 组装 4 字节 Input Report | vhidmou.cpp |
| 类驱动通信 | 响应 HID IOCTL,与 hidclass.sys 协作 | vhidmou.def |
| 安装绑定 | 让 PnP 识别并加载驱动 | vhidmou.inf |
5. 驱动构建与内核态调试的验证清单
5.1 用 WinDbg 断言驱动加载路径
拿到这套源码后,第一步不是通读代码,而是把驱动编出来装到测试机上,确认它真的被加载。我习惯先把 sys 和 inf 放到同一个目录,右键 INF 执行安装,然后立刻查看设备管理器状态。代码 31 基本锁定是 INF 匹配或签名问题,代码 10 则可能是设备初始化失败。连接内核调试器后,用!drvobj检查驱动对象是否注册成功:
kd> !drvobj \Driver\vhidmou Driver object (ffffaa0f12345678) is for: \Driver\vhidmou DriverEntry: fffff800`12340000如果DriverEntry地址全是问号,说明驱动根本没有被内核加载,此时先回到 INF 和签名检查,不要浪费时间去分析 IRP。!devobj输出里要确认设备对象挂到了正确的设备栈上,DeviceExtension 中的 LowerDeviceObject 应该是一个非空且类型为 Device 的对象指针。
没有内核调试环境时,另一个低成本方案是启用 Driver Verifier,只勾选 vhidmou.sys 一个驱动,并打开“强制 IRQL 检查”和“I/O 验证”。这类小数据量设备很少触发 IRQL 问题,但在 DISPATCH_LEVEL 里调用分页函数一定会被验出来,比我逐行审查代码快得多。注意 Driver Verifier 一旦启用无法按进程粒度关闭,测试完成后要手动删除验证器设置。
5.2 关注驱动签名与设备安装日志
驱动加载和安装过程会写入Microsoft-Windows-Kernel-PnP/Configuration管理通道。重点看事件 ID 411(设备配置)和 420(设备启动失败),其中status字段如果出现0xC0000428,说明签名属性校验失败,对应的就是未启用测试签名模式时加载自签驱动的典型错误。
| 排查目标 | 使用的命令/工具 | 预期结果 |
|---|---|---|
| 驱动是否注册为服务 | sc query vhidmou | STATE 显示 RUNNING 或 STOPPED 且无错误 |
| 签名是否放行 | signtool verify /pa vhidmou.sys | 输出 Signed With SHA256,或提示未签名 |
| 设备节点是否建立 | 设备管理器“查看→显示隐藏的设备” | 能看到鼠标节点且无黄色感叹号 |
| 报告描述符是否可读 | 用户态 HID 客户端读取描述符 | 字节数与源码定义的一致 |
调试的最后一步通常是打开 DbgPrint 输出,动几下鼠标看是否有数值变化。如果 Read 请求里的报告长度与描述符算出的 4 字节不符,hidclass.sys 会直接丢弃报告,表现为设备管理器一切正常、鼠标毫无响应。这类问题靠翻 IRP 处理代码查不出来,必须逐字节对比描述符和数据帧头。
一个值得保留的检查习惯:改动报告描述符后,先在用户态把整个描述符读出来,与源码里的字节数组做一次逐字节 diff,再编译进驱动。这样可以避免每次修改都走一遍“编译、安装、重启、看指针动不动”的循环,能把排错时间压缩一个数量级。
本文还有配套的精品资源,点击获取