
简介鼠标驱动程序源代码Windows下WDM开发面向Windows驱动开发人员与底层硬件编程学习者。压缩包共13个文件以C源码、头文件为主辅以Visual Studio工程文件、inf安装配置文件、def导出定义、makefile与sources构建描述整体按标准驱动项目结构组织便于从源码层面拆解设备初始化、IRP请求分发、HID鼠标协议封装等核心环节。资源体积仅12KB虽然精简却覆盖了驱动程序从代码编写、编译配置到安装部署的完整链路适合快速定位关键实现。已有921人学习下载。对希望理解即插即用、电源管理以及读写I/O在真实鼠标设备上如何落地的读者可直接对照源码梳理驱动入口、设备对象创建、设备扩展初始化与卸载清理逻辑减少自己搭框架和排错的时间也可作为驱动开发课程设计或入门实践的参考。1. 鼠标驱动程序源代码为什么还在聊WDM当你从项目归档包里打开“鼠标驱动程序源代码windows下WDM开发.zip”这类包会发现里面没有繁琐的USB描述符解析也不需要你理解中断。WDM鼠标过滤驱动没那么神秘Windows把鼠标事件包成IRP过滤驱动插入在mouclass设备栈上就能看见、拦截或者改写这些IRP。即使现在是WDF时代老代码还是用WDM模式运作而且这类驱动的“设备栈挂接”思想在WDM和KMDF里一脉相承。这个方向适合三类人一类是维护以前鼠标厂商发布过的老工具一类是搞安全或自动化、要在输入层截获特定事件的人还有一类是把Windows驱动当面试考点、想在几天内看懂一份真实鼠标源代码的工程师。下面我按“设备栈—源码—编译—调试—改数据”的顺序展开示例会落到一个可直接编译的迷你过滤驱动再讲清楚参数和调试方法。2. WDM鼠标驱动基础设备栈、IRP与驱动绑定方式2.1 鼠标输入设备栈mouclass如何与底层通讯在Windows驱动模型里“鼠标”不是一个像NTFS那样单一的设备。PS/2鼠标挂在i8042prt或sermouse端口驱动之下USB鼠标则由hidclass和hidusb提供协议支持但最终都合并到同一个“鼠标类驱动程序”mouclass让上层面对统一接口。mouclass再往上是用户态和内核态的输入子系统包括win32k最终把坐标和按键喂给当前活动窗口。WDM鼠标过滤驱动一般不会自己去碰中断寄存器而是挂在mouclass的上面或下面。常见手段是创建一个属于自己的过滤设备对象然后用IoAttachDevice绑定到\Device\PointerClass0。PointerClass0正是鼠标类驱动创建的设备实例对应键盘类设备的KeyboardClass0。绑定后你的过滤设备会被插入到类设备之上鼠标坐标数据流经你的设备对象时你就有机会观察和修改IRP。这个绑定动作是理解整份鼠标驱动源代码的关键。很多初学者把时间花在阅读端口驱动上以为要控制寄存器才能改鼠标行为。实际上对上层过滤驱动来说真正的数据通道是IRP_MJ_READ而不是I/O端口。2.2 过滤驱动绑定方式IoCreateDevice与IoAttachDevice的组合不采用PnP模式的过滤驱动可以用很简单的结构。DriverEntry里创建设备对象命名比如\Device\MouFilter0再绑定到PointerClass0最后把IRP分发函数填好。相比PnP驱动这种做法的优点是加载和卸载时机可控不需要为每个具体鼠标设备生成多个实例特别适合用来做“一插全接管”的系统级过滤。这里的关键函数是IoAttachDevice(FilterDevice, targetName, lowerDevice)。它有两层作用第一找到目标设备栈的最上层设备把FilterDevice挂上去第二返回一个lowerDevice用来向原来的设备栈转发IRP。挂接后你的设备对象会出现在设备栈的顶层而IoCallDriver要调用的是LowerDevice不是系统给出的设备对象。需要注意参数TargetDeviceName是UNICODE_STRING。若绑定失败常见原因是PointerClass0尚未创建此时驱动加载报错0xC0000035另一个常见错误是把设备名写成了\Device\PointerClass1那只会绑到第二个鼠标类设备上主鼠标完全不受过滤。2.3 IRP读懂鼠标数据的关键对象系统与鼠标驱动沟通通过IRP_MJ_READ请求。简单理解mouclass向上层提供读取接口当用户态请求输入数据时系统分配一块缓冲区构建一个IRP沿设备栈往下发。最底层是端口驱动或HID栈它把真实鼠标硬件产生的数据按格式填入缓冲区然后返回IRP。过滤驱动挂在这条路径上就相当于在数据线和系统之间加了个“采样点”。鼠标数据在WDM里通常用MOUSE_INPUT_DATA结构表示。常见过滤驱动只关心IRP_MJ_READ其他IRP例如IRP_MJ_PNP和IRP_MJ_POWER没有必要碰直接透传即可。下表是过滤驱动重点关心的IRP类型。IRP 主功能典型处理方式为什么IRP_MJ_CREATE / CLOSE透传或返回成功打开与关闭设备句柄过滤驱动一般不需要干预IRP_MJ_READ设置完成例程后再下发鼠标数据的承载IRP过滤逻辑放在完成例程IRP_MJ_WRITE透传鼠标几乎没有写命令无需处理IRP_MJ_PNP / POWER透传状态与电源变化交给底层和系统处理IRP_MJ_INTERNAL_DEVICE_CONTROL透传底层协议命令不做协议改造就不要动这个表同时也是判断驱动代码质量的清单如果一份鼠标WDM源码把IRP_MJ_PNP彻底忽略往往会在拔插时蓝屏。3. 实现一份WDM鼠标过滤驱动源码骨架和编译参数3.1 DriverEntry创建设备对象并绑定鼠标类设备我们写的迷你驱动采用非PnP模式直接绑定到PointerClass0。先给出入口代码#include ntddk.h #include ntddmou.h #define MOUFILTER_DEVICE_NAME L\\Device\\MouFilter0 typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT LowerDevice; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS MouFilterRead(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS MouFilterPassThrough(PDEVICE_OBJECT DeviceObject, PIRP Irp); VOID MouFilterUnload(PDRIVER_OBJECT DriverObject); NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { PDEVICE_OBJECT filterDevice NULL; PDEVICE_EXTENSION ext; UNICODE_STRING deviceName; NTSTATUS status STATUS_SUCCESS; ULONG i; for (i 0; i IRP_MJ_MAXIMUM_FUNCTION; i) { DriverObject-MajorFunction[i] MouFilterPassThrough; } DriverObject-MajorFunction[IRP_MJ_CREATE] MouFilterPassThrough; DriverObject-MajorFunction[IRP_MJ_CLOSE] MouFilterPassThrough; DriverObject-MajorFunction[IRP_MJ_READ] MouFilterRead; DriverObject-DriverUnload MouFilterUnload; RtlInitUnicodeString(deviceName, MOUFILTER_DEVICE_NAME); status IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, filterDevice); if (!NT_SUCCESS(status)) { return status; } ext (PDEVICE_EXTENSION)filterDevice-DeviceExtension; RtlZeroMemory(ext, sizeof(DEVICE_EXTENSION)); RtlInitUnicodeString(deviceName, L\\Device\\PointerClass0); status IoAttachDevice(filterDevice, deviceName, ext-LowerDevice); if (!NT_SUCCESS(status)) { IoDeleteDevice(filterDevice); return status; } filterDevice-Flags ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }参数解析IoCreateDevice第5个参数Exclusive设为FALSE避免独占鼠标设备导致系统无法打开。设备扩展里只保存一个LowerDevice它就是转发IRP的目标。RtlInitUnicodeString两次使用相同局部变量第二次绑定PointerClass0名称不可写反。MajorFunction全部先设为透传再单独覆盖READ其他IRP不会因未初始化而空指针。说明如果你要把这个驱动编译成64位源文件里不要对整个文件加#pragma pack(1)否则MOUSE_INPUT_DATA长度错位会导致按键读取全部错乱。3.2 AddDevice与边界情况为何这里没有用PnP过滤驱动很多网上流传的键盘过滤源码会额外写AddDevice回调那是因为它有PnP用法。本示例用的是“非PnP驱动”所以没有AddDevice回调也不需要暴露HardwareID。优点是指定绑定的对象简单可控缺点是必须在鼠标类驱动已经启动之后加载否则IoAttachDevice无法找到目标。如果你的目标是“适配某种特定鼠标芯片”就要改用PnP过滤驱动。常见做法是注册DriverObject-DriverExtension-AddDevice然后在AddDevice里调用IoAttachDeviceToDeviceStack这样每次新插入鼠标都会生成新的过滤实例。两种方式在IRP处理部分完全一样区别只在设备生命周期管理。手头这份源码如果是WDM模式建议先确认它是否注册了AddDevice这决定了它的启动方式。还有一种做法是上层过滤驱动通过注册表挂接到鼠标设备栈。在设备管理器里把驱动加到鼠标设备的UpperFilters系统会在每次设备启动时加载它。这种模式更“正规”但调试时受设备实例影响不如直接绑定PointerClass0直观。3.3 处理IRP_MJ_READ和完成例程过滤驱动要读数据不能自己在READ IRP里直接产生结果而是要把IRP继续传给下层。为了在数据返回时拦截在调下层之前设置一个完成例程。完成例程会在IRP完成后被调用。NTSTATUS MouFilterRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PDEVICE_EXTENSION ext (PDEVICE_EXTENSION)DeviceObject-DeviceExtension; IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp, MouFilterReadCompletion, DeviceObject, TRUE, TRUE, TRUE); return IoCallDriver(ext-LowerDevice, Irp); } NTSTATUS MouFilterReadCompletion(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context) { PDEVICE_EXTENSION ext (PDEVICE_EXTENSION)DeviceObject-DeviceExtension; if (Irp-IoStatus.Status STATUS_SUCCESS Irp-IoStatus.Information sizeof(MOUSE_INPUT_DATA)) { PMOUSE_INPUT_DATA data (PMOUSE_INPUT_DATA)Irp-AssociatedIrp.SystemBuffer; DbgPrint(MouFilter: buttons0x%x x%d y%d\n, >VOID MouFilterUnload(PDRIVER_OBJECT DriverObject) { PDEVICE_OBJECT filterDevice DriverObject-DeviceObject; PDEVICE_EXTENSION ext; if (filterDevice ! NULL) { ext (PDEVICE_EXTENSION)filterDevice-DeviceExtension; if (ext-LowerDevice ! NULL) { IoDetachDevice(ext-LowerDevice); } IoDeleteDevice(filterDevice); } }IoDetachDevice负责拆掉过滤设备到下层设备的连接IoDeleteDevice删除自身设备对象顺序不能颠倒。非PnP驱动通常用sc命令或INF安装属“内核服务”。INF里可以按下面的方式声明。[Version] Signature $WINDOWS NT$ Class Mouse ClassGuid {4d36e96f-e325-11ce-bfc1-08002be10318} Provider %MfgName% [Manufacturer] %MfgName%MfgList [MfgList] %DeviceDesc%MouFilter [MouFilter.NT] CopyFilesmoufilter.sys [DestinationDirs] DefaultDestDir12 [MouFilter.NT.Services] AddService MouFilter, 0x00000002, ServiceInst [ServiceInst] ServiceType 1 StartType 3 ErrorControl 1 ServiceBinary %12%\moufilter.sys [Strings] MfgName Example Driver DeviceDesc Example Mouse Filter直接用WDK的Build环境编译时需要创建SOURCE文件。TARGETNAMEmoufilter TARGETTYPEDRIVER TARGETPATHobj SOURCESmoufilter.c参数解释TARGETTYPEDRIVER输出内核驱动不是EXE或DLL。TARGETPATHobj指中间产物目录不影响最终驱动。若使用Visual Studio新建“Empty WDM Driver”项目把.c文件拖进去然后编译即可。项目属性里的Target OS选Windows 10或Windows 11这份WDM源码从XP到Win11都能编译只不过签名策略不同。4. 让Windows加载你的WDM鼠标驱动安装、签名和调试4.1 测试签名开发机必做的准备WDM内核驱动在64位Windows上必须拥有有效的Microsoft签名。开发阶段最简单的方式是启用测试签名模式。打开管理员命令行执行bcdedit /set testsigning on重启后桌面上出现“测试模式”的水印表示已生效。如果你的机器启用了Secure Boot这个命令会失败需要在BIOS/固件里临时关闭Secure Boot。第二种方式是使用启动菜单的“禁用驱动程序强制签名”选项但那只对本次启动有效开发时一重启又没了。要关闭测试签名执行bcdedit /set testsigning off即可。企业环境的机器建议不要长期开启否则会被安全策略拦掉。4.2 驱动文件部署sc与INF两条路径IDE编译完成后你会得到一个moufilter.sys。把它拷贝到系统驱动目录例如C:\Windows\System32\drivers\。可以用INF安装也可以直接创建内核服务。先看INF安装方式右键moufilter.inf选择“安装”或命令行pnputil /add-driver moufilter.inf /install但pnputil更适合有HardwareID的PnP驱动对非PnP驱动不一定可靠。这里推荐直接用sc建服务sc.exe create MouFilter type kernel binPath C:\Windows\System32\drivers\moufilter.sys sc.exe start MouFilter如果注册成功但启动失败先检查sc.exe query MouFilter里的状态。最可能的两个错误ERROR 577签名无效或ERROR 2文件找不到。ERROR 577需要重新检查驱动签名或测试签名ERROR 2则要确认binPath是否正确且文件确实存在。要卸载驱动先停止sc.exe stop MouFilter sc.exe delete MouFilter注意停止前退出所有正在使用鼠标输入的程序否则显式句柄未释放可能导致卸载失败。4.3 验证绑定是否成功WinDbg双机调试加载后从内核调试器观察。WinDbg连接目标机后执行lm m moufilter能看到模块名、基址和标志说明驱动已加载。再执行!devobj \Device\MouFilter0输出的DeviceExtension显示Driver指向MouFilterAttachedDevice链上可以看到指向PointerClass0或下一层设备对象。如果要看IRP是否经过MouFilterRead可以对IRP_MJ_READ设置断点bu moufilter!MouFilterRead g当系统再次处理鼠标输入时断点会命中输入kd k可以看调用栈确认路径。没有双机调试环境时可以用DbgPrint输出到调试器或使用DebugView配合Global Flags开启内核打印。但新版Windows上内核DbgPrint需要调试器捕获DebugView并不总是能看到。我的建议是直接开一个空内核调试会话能完整看到IRP_MJ_READ和IRP_MJ_PNP的时序。4.4 常见坑代码31、设备栈挂错位置和卸载蓝屏过滤驱动最常见的一种异常是设备管理器显示“代码31”几乎都是资源或驱动绑定失败。如果你是PnP过滤驱动代码31也可能是UpperFilters里写的服务名不对如果按本示例的sc方式加载设备管理器不会显示鼠标设备有问题因为你的驱动不在设备栈里注册只挂在PointerClass0上层。另一种较隐蔽的问题是绑到了PointerClass1而不是PointerClass0。如果机器有多个鼠标类设备只绑其中一个过滤行为就会不稳定。检查方法是使用WinDbg的!object \Device查看所有PointerClass实例再决定绑哪一个。还有卸载蓝屏在存在引用计数或未完成IRP时直接IoDeleteDevice。一般来说过滤驱动要确保在DriverUnload时没有任何线程仍在等待这个设备对象也不要自己强制取消其他驱动的IRP。稳妥做法是在DriverEntry里设置一个全局事件让完成例程在卸载前等待所有在途IRP。5. 在WDM鼠标驱动里修改鼠标事件并验证结果5.1 在完成例程里改坐标与按键我们的过滤驱动没有业务逻辑真正的“产品功能”都在MouFilterReadCompletion里写。下面代码演示如何把所有鼠标移动清零、屏蔽右键只保留左键NTSTATUS MouFilterReadCompletion(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context) { if (Irp-IoStatus.Status STATUS_SUCCESS) { ULONG count (ULONG)(Irp-IoStatus.Information / sizeof(MOUSE_INPUT_DATA)); PMOUSE_INPUT_DATA data (PMOUSE_INPUT_DATA)Irp-AssociatedIrp.SystemBuffer; for (ULONG i 0; i count; i) { // 屏蔽右键0x02中键0x04等其他按键 data[i].Buttons 0x01; // 清掉位移鼠标指针将不会移动 data[i].LastX 0; data[i].LastY 0; } } return STATUS_SUCCESS; }参数说明MOUSE_INPUT_DATA.ButtonFlags和ButtonData在联合体里所以使用data[i].Buttons操作会同时影响两者。LastX/LastY在绝大多数鼠标事件里是相对位移只有Flags字段带MOUSE_MOVE_ABSOLUTE时它才是绝对坐标例如触屏或远程桌面场景这种场景下强行清零可能让指针跳到边缘。把上面的代码加入前面的骨架重新编译安装鼠标会表现为“无论怎么移动指针不动只有左键有效”。这正好用来验证过滤路径是否生效。5.2 验证过滤生效的3个检查点如果行为没有变化按下面顺序排查执行sc.exe query MouFilter看服务状态是否为RUNNING。如果显示STOPPED说明驱动加载失败回到4.1检查签名。在WinDbg里执行!devobj \Device\MouFilter0观察AttachedDevice是否指向PointerClass0链上的设备。如果AttachedDevice为0说明IoAttachDevice没有挂上。对moufilter!MouFilterReadCompletion下断点看是否命中。如果没有命中说明IRP_MJ_READ没有经过你的过滤驱动需要在设备栈里找真正的数据路径。如果第三步失败直接回头检查设备栈关系而不是在完成例程里堆代码。本文还有配套的精品资源点击获取