简介:这份资源是面向Windows底层驱动开发学习者的WDM鼠标驱动源代码包,适合已具备C/C++基础、希望理解设备驱动模型与HID设备交互机制的开发者参考。压缩包共13个文件,约12KB,以4个h头文件、2个cpp实现文件为主,另有rc资源脚本、makefile与sources构建配置、dsp/dsw工程文件、def导出定义及inf安装信息文件,覆盖从接口声明、核心逻辑到工程构建与驱动安装的完整环节。源代码围绕设备初始化、IRP请求处理、HID协议解析与设备枚举加载等关键流程展开,可帮助读者理清函数驱动与筛选器驱动的分工,掌握驱动与硬件通信及向系统传递数据的方式。目前已有922人学习下载,对于想通过实例理解WDM架构、积累驱动调试与定制经验的开发者具有较高参考价值。
1. 鼠标驱动程序源代码与 WDM 开发:从一份 zip 到能跑的最小驱动
很多人第一次拿到「鼠标驱动程序源代码,windows下WDM开发.zip」这类压缩包时,会下意识以为里面是一份能直接编译、装上就能让鼠标动起来的完整工程。实际打开后往往只有几个.c、.h、.inf和一份.vcxproj,甚至只有零散的源码片段。这不是坑,而是 WDM 驱动开发的常态——它本来就是一套需要你自己搭环境、配签名、调 IRP 的底层工程。WDM(Windows Driver Model)是微软在 Win98 到 Win10 时代主推的驱动框架,鼠标这类 HID 设备驱动、过滤驱动、功能驱动都能用它写。这份源码真正能帮到你的,是让你看清一个鼠标驱动从 DriverEntry、AddDevice 到 IRP_MJ_READ 的完整骨架,而不是给你一个成品。适合谁?适合已经会写 C、懂点内核对象、想搞明白鼠标数据怎么从硬件进到系统的那批人。如果你只是想改个鼠标灯效,那这份源码会让你失望;但如果你想做输入设备过滤、按键重映射、宏录制这类底层活儿,它就是绕不开的起点。
2. WDM 鼠标驱动的骨架:DriverEntry、AddDevice 与设备栈
2.1 为什么鼠标驱动要挂在设备栈上而不是自己造轮子
Windows 的输入设备走的是分层驱动模型。一个物理鼠标插上后,系统会依次加载总线驱动(USB 或 i8042)、HID 类驱动、以及可能的过滤驱动。你拿到的这份 WDM 源码,通常是一个过滤驱动或功能驱动的模板,它不会去直接操作硬件端口,而是挂到已有设备栈上,拦截或转发 IRP。这是 WDM 最核心的设计:每个驱动只负责一层,通过DEVICE_OBJECT的AttachedDevice指针串成栈。鼠标数据最终以IRP_MJ_READ的形式从下层往上冒,你的驱动可以在DispatchRead里拿到MOUSE_INPUT_DATA结构。理解这一点,你就明白为什么源码里没有直接读 0x60 端口的代码——那是 i8042 端口驱动的事,不是你的。
选型上,如果你要写的是鼠标过滤驱动,WDM 和 KMDF 都能做。KMDF 更现代、代码更少,但这份源码是 WDM 的,所以你得按 WDM 的规矩来:手动创建DEVICE_OBJECT、手动挂载、手动处理IRP_MN_REMOVE_DEVICE。好处是控制粒度细,坏处是内存管理和即插即用状态机得自己兜底。常见做法是先用 WDM 模板跑通,再考虑要不要迁到 KMDF。
2.2 用 Visual Studio 建一个能编译的 WDM 空工程
这份 zip 里的.vcxproj往往缺配置,直接打开会报「找不到 wdm.h」或「无法解析的外部符号」。正确姿势是自己建一个 Empty WDM Driver 工程,再把源码文件拖进去。步骤如下:
# 1. 安装 Visual Studio 2022,勾选「使用 C++ 的桌面开发」和「Windows 驱动工具包 (WDK)」 # 2. 确认 WDK 版本与 SDK 匹配,命令行验证: where wdk # 输出类似 C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\km # 3. 新建项目 -> 选择 "Empty WDM Driver" 模板 # 4. 把 zip 里的 .c/.h 复制到项目目录,在解决方案资源管理器里「添加现有项」逻辑说明:WDK 提供wdm.h、ntddk.h等内核头文件,以及BufferOverflowK.lib等内核库。VS 模板会自动配好Target Platform和Driver Type。参数上,Configuration Properties -> Driver Settings -> Target OS Version建议选Windows 10或Windows 11,Target Platform选Desktop。如果你在 Win11 上开发但目标机是 Win10,把Minimum Required Version设成10.0.17763.0能兼容更多机器。编译前记得在C/C++ -> Preprocessor里加上_WIN32_WINNT=0x0A00和WINVER=0x0A00,否则某些 API 声明会被条件编译掉。
2.3 DriverEntry 里必须做的三件事与源码对照
打开源码的DriverEntry,你会看到类似这样的结构:
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { // 1. 注册派遣函数 for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) { DriverObject->MajorFunction[i] = DispatchPassThrough; } DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead; DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp; DriverObject->DriverUnload = DriverUnload; // 2. 设置 AddDevice 回调 DriverObject->DriverExtension->AddDevice = AddDevice; // 3. 返回成功 return STATUS_SUCCESS; }逻辑说明:MajorFunction数组是 IRP 分发的入口,没注册的 IRP 会返回STATUS_INVALID_DEVICE_REQUEST。鼠标过滤驱动必须处理IRP_MJ_READ才能拿到数据,IRP_MJ_PNP用来响应设备插拔。AddDevice是 WDM 特有的,系统发现新设备时调用它来创建设备对象。参数上,RegistryPath是驱动在注册表中的服务键路径,一般用ZwOpenKey读配置,但鼠标驱动很少需要。注意DriverUnload只在非 PnP 驱动里可靠,WDM 驱动卸载靠IRP_MN_REMOVE_DEVICE,所以DriverUnload里别做重活。
2.4 AddDevice 与设备栈挂载:一行代码决定成败
AddDevice是 WDM 驱动最容易翻车的地方。源码里通常长这样:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject) { PDEVICE_OBJECT FilterDevice; NTSTATUS status = IoCreateDevice( DriverObject, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_MOUSE, FILE_DEVICE_SECURE_OPEN, FALSE, &FilterDevice ); if (!NT_SUCCESS(status)) return status; PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)FilterDevice->DeviceExtension; ext->LowerDevice = IoAttachDeviceToDeviceStack(FilterDevice, PhysicalDeviceObject); if (!ext->LowerDevice) { IoDeleteDevice(FilterDevice); return STATUS_UNSUCCESSFUL; } FilterDevice->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE; FilterDevice->Flags &= ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }逻辑说明:IoCreateDevice创建你的过滤设备对象,DeviceExtension用来存私有数据,比如下层设备指针。IoAttachDeviceToDeviceStack把过滤设备挂到物理设备之上,返回下层设备对象。DO_BUFFERED_IO让系统自动处理缓冲区,鼠标数据量小,用缓冲 IO 最省事。DO_DEVICE_INITIALIZING必须清掉,否则设备无法接收 IRP。参数上,FILE_DEVICE_MOUSE是设备类型,FILE_DEVICE_SECURE_OPEN是特性掩码,一般照抄。常见错误是忘了清DO_DEVICE_INITIALIZING,现象是驱动装上后鼠标直接不动,事件查看器里一堆STATUS_INVALID_DEVICE_STATE。
3. 鼠标数据怎么读:IRP_MJ_READ 与 MOUSE_INPUT_DATA 解析
3.1 DispatchRead 里拿到的到底是什么
鼠标过滤驱动的核心价值在DispatchRead。当上层请求鼠标数据时,IRP 会先到你的驱动,再到下层。源码里通常这样写:
NTSTATUS DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, ReadCompletion, DeviceObject, TRUE, TRUE, TRUE); return IoCallDriver(ext->LowerDevice, Irp); } NTSTATUS ReadCompletion(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context) { if (NT_SUCCESS(Irp->IoStatus.Status)) { PMOUSE_INPUT_DATA data = (PMOUSE_INPUT_DATA)Irp->AssociatedIrp.SystemBuffer; ULONG count = Irp->IoStatus.Information / sizeof(MOUSE_INPUT_DATA); for (ULONG i = 0; i < count; i++) { // data[i].ButtonFlags, data[i].LastX, data[i].LastY } } if (Irp->PendingReturned) IoMarkIrpPending(Irp); return Irp->IoStatus.Status; }逻辑说明:IoCopyCurrentIrpStackLocationToNext把当前栈位置复制给下层,保证下层能看到正确的参数。IoSetCompletionRoutine注册完成例程,等下层处理完 IRP 后回调你。在完成例程里,SystemBuffer指向MOUSE_INPUT_DATA数组,IoStatus.Information是字节数,除以结构大小就是条目数。ButtonFlags里MOUSE_LEFT_BUTTON_DOWN、MOUSE_RIGHT_BUTTON_DOWN等标志位表示按键状态,LastX/LastY是相对位移。参数上,IoSetCompletionRoutine的后三个布尔值分别控制成功、失败、取消时是否调用,全设TRUE最稳。注意完成例程运行在DISPATCH_LEVEL,不能调KeWaitForSingleObject这类会阻塞的函数。
3.2 过滤驱动里改数据:按键重映射的最小实现
如果你想做按键重映射,比如把中键改成左键,就在完成例程里改ButtonFlags:
if (data[i].ButtonFlags & MOUSE_MIDDLE_BUTTON_DOWN) { data[i].ButtonFlags &= ~MOUSE_MIDDLE_BUTTON_DOWN; data[i].ButtonFlags |= MOUSE_LEFT_BUTTON_DOWN; } if (data[i].ButtonFlags & MOUSE_MIDDLE_BUTTON_UP) { data[i].ButtonFlags &= ~MOUSE_MIDDLE_BUTTON_UP; data[i].ButtonFlags |= MOUSE_LEFT_BUTTON_UP; }逻辑说明:MOUSE_MIDDLE_BUTTON_DOWN和UP是成对的,只改 DOWN 不改 UP 会导致按键卡住。参数上,ButtonFlags是USHORT,位定义在ntddmou.h里。注意改数据只影响上层看到的结果,不影响硬件本身,所以其他程序读到的也是改过的数据。常见坑是忘了处理UP事件,现象是鼠标左键一直处于按下状态,拖拽窗口时松不开。
3.3 即插即用与电源管理:IRP_MN_REMOVE_DEVICE 必须处理
WDM 驱动不处理 PnP 会在设备拔出时蓝屏。源码里DispatchPnp通常这样:
NTSTATUS DispatchPnp(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PDEVICE_EXTENSION ext = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); switch (stack->MinorFunction) { case IRP_MN_REMOVE_DEVICE: IoDetachDevice(ext->LowerDevice); IoDeleteDevice(DeviceObject); Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; default: IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(ext->LowerDevice, Irp); } }逻辑说明:IRP_MN_REMOVE_DEVICE是设备移除的最终通知,必须先IoDetachDevice再IoDeleteDevice,顺序反了会留下悬空指针。其他 PnP 子功能直接透传给下层。参数上,IoSkipCurrentIrpStackLocation不复制栈位置,适合透传场景。注意IoDeleteDevice之后不能再访问DeviceObject,所以完成例程里别引用它。
4. 编译、签名与安装:WDM 驱动在 Win10/Win11 上的避坑清单
4.1 测试签名模式不是万能药
Win10 1607 之后,内核驱动必须签名才能加载,测试签名模式(bcdedit /set testsigning on)只对测试证书签的驱动有效,而且会在桌面右下角显示水印。常见做法是:
# 管理员命令行 bcdedit /set testsigning on # 重启后,用 makecert 或 signtool 签驱动 signtool sign /v /s TestCertStore /n "MyTestCert" /t http://timestamp.digicert.com mouse.sys逻辑说明:testsigning on让系统接受测试证书签名的驱动。signtool的/s指定证书存储,/n是证书名,/t是时间戳服务器。参数上,Win11 22H2 之后测试签名模式仍然可用,但部分安全策略会拦截。注意正式发布必须用 EV 证书加微软硬件开发者中心签名,测试证书只能自己机器用。
4.2 用 devcon 或 pnputil 安装 INF
源码里的.inf文件负责把驱动和设备关联。安装命令:
# 查看设备 ID pnputil /enum-devices /class Mouse # 安装驱动 pnputil /add-driver mouse.inf /install # 或者用 devcon devcon update mouse.inf HID\VID_1234&PID_5678逻辑说明:pnputil是系统自带工具,/add-driver把驱动加入驱动库,/install立即安装到匹配设备。devcon需要从 WDK 里拷出来。参数上,HID\VID_xxxx&PID_xxxx是硬件 ID,在设备管理器里「属性 -> 详细信息 -> 硬件 ID」能看到。注意 INF 里的CatalogFile必须和签名后的.cat文件名一致,否则安装报「哈希值不在指定目录中」。
4.3 蓝屏排查:WinDbg 双机调试的最小配置
驱动一加载就蓝屏是常态。配置双机调试:
# 目标机(被调试) bcdedit /debug on bcdedit /dbgsettings net hostip:192.168.1.100 port:50000 key:1.2.3.4 # 主机(调试机)打开 WinDbg windbg -k net:port=50000,key=1.2.3.4逻辑说明:bcdedit /debug on开启内核调试,/dbgsettings net配置网络调试。主机 WinDbg 连上后,蓝屏时会自动断下,用!analyze -v看堆栈。参数上,hostip是主机 IP,port随便选,key是任意四段数字。注意虚拟机里调试要把网络设成桥接或 Host-Only,NAT 模式下主机连不上。
5. 常见问题与排查:鼠标驱动加载失败的 5 个血泪坑
5.1 现象:驱动装上后鼠标完全不动,设备管理器显示黄色感叹号
原因:AddDevice里忘了清DO_DEVICE_INITIALIZING,或者IoAttachDeviceToDeviceStack返回 NULL 后没删设备对象。解决:检查FilterDevice->Flags &= ~DO_DEVICE_INITIALIZING;是否执行,确认PhysicalDeviceObject非空。用!devobj看设备对象状态。
5.2 现象:编译报错「无法打开源文件 wdm.h」
原因:VS 工程没装 WDK,或者 SDK 和 WDK 版本不匹配。解决:在 VS Installer 里勾选「Windows 驱动工具包」,确保 WDK 版本号与 SDK 一致。检查VC++ Directories -> Include Directories里有没有$(WDKContentRoot)\Include\$(TargetPlatformVersion)\km。
5.3 现象:安装时报「驱动未签名」或「哈希值不在指定目录中」
原因:测试签名没开,或者.cat文件和 INF 里的CatalogFile名字不一致。解决:bcdedit /set testsigning on后重启,用signtool重新签.sys和.cat,确保 INF 里CatalogFile=mouse.cat和实际文件名一致。
5.4 现象:鼠标数据读到了但坐标乱跳
原因:MOUSE_INPUT_DATA的LastX/LastY是相对位移,不是绝对坐标,而且可能包含多个条目。解决:累加LastX/LastY再使用,或者直接透传给上层。检查IoStatus.Information是否被正确除以sizeof(MOUSE_INPUT_DATA)。
5.5 现象:设备拔出时蓝屏,错误码DRIVER_IRQL_NOT_LESS_OR_EQUAL
原因:IRP_MN_REMOVE_DEVICE里先IoDeleteDevice再IoDetachDevice,或者完成例程里引用了已删除的设备对象。解决:严格按IoDetachDevice->IoDeleteDevice顺序,完成例程里用Context传数据而不是DeviceObject。
6. 进阶:用 WDM 鼠标过滤驱动做宏录制与回放
跑通基础过滤后,你可以把MOUSE_INPUT_DATA存下来做宏。核心思路是在完成例程里把数据写进环形缓冲区,再用一个IRP_MJ_DEVICE_CONTROL接口让用户态读取。具体做法:在DeviceExtension里加KSPIN_LOCK和LIST_ENTRY,完成例程里KeAcquireSpinLock后插入节点,用户态用DeviceIoControl发IOCTL_GET_MOUSE_LOG取数据。注意完成例程在DISPATCH_LEVEL,只能用非分页内存,ExAllocatePoolWithTag要加NonPagedPool标志。
回放时,用户态发IOCTL_REPLAY_MOUSE,驱动在DispatchDeviceControl里构造MOUSE_INPUT_DATA并调用IoCallDriver往下层发。这里有个坑:下层驱动可能不认你伪造的 IRP,常见做法是直接调MouseClassServiceCallback,但这个函数未文档化,版本间可能变。更稳的是用SendInput在用户态回放,驱动只负责录制。
验证方法:用DebugView看KdPrint输出,或者写个用户态程序读IOCTL并打印时间戳和位移。我一般会在完成例程里加一个计数器,每 1000 条打一次日志,避免KdPrint刷屏导致系统卡顿。参数上,环形缓冲区大小建议 4096 条,每条MOUSE_INPUT_DATA约 24 字节,总共不到 100KB,非分页内存完全扛得住。
最后说个习惯:每次改完驱动,先在本机testsigning模式下装一遍,再用verifier开标准验证跑 10 分钟。verifier /standard /driver mouse.sys能揪出大部分内存泄漏和 IRQL 错误。别嫌麻烦,蓝屏一次浪费的时间够你跑十遍验证。希望帮到你。
本文还有配套的精品资源,点击获取