ARTICLE DETAIL

资讯详情

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

WDM鼠标驱动开发实战:从源码到IRP拦截

WDM鼠标驱动开发实战:从源码到IRP拦截 简介Windows WDM鼠标驱动程序完整源代码面向有一定C/C基础、希望深入驱动开发领域的开发者也适合学习Windows设备驱动模型的学生与工程师。压缩包共13个文件以头文件、实现文件为主体并包含工程文件、INF安装配置、makefile及def导出定义等整体仅12KB结构紧凑、便于对照分析。已有921人学习下载。通过这份源码可以直观学习设备初始化、IRP请求处理、即插即用与电源管理、HID设备数据转换等核心环节INF文件展示了驱动安装与硬件关联方式工程文件则帮助快速搭建WDM驱动编译环境。对理解WDM驱动架构、掌握鼠标类设备驱动开发流程以及后续定制和优化驱动均有实用参考价值。1. 一个“鼠标驱动程序源代码, windows下WDM开发”压缩包先要分清是哪一层拿到写着“鼠标驱动程序源代码, windows下WDM开发”的压缩包别急着去翻 DriverEntry。鼠标输入这条链路在 Windows 里长期是“端口驱动 类驱动 过滤驱动”三层结构应用层看到的鼠标移动和点击最终都来自底层驱动对IRP_MJ_READ的回应。这个压缩包里的代码可能是其中任意一层替换 i8042prt 的端口驱动、实现 mouclass 角色的功能驱动、或者最常见的过滤驱动。跑通它之前必须先确认自己要挂到设备栈的哪个位置。这篇文章按“定位设备栈 → 搭出可编译的 WDM 过滤驱动 → 双机调试验证 → 暴露调试接口”的顺序把一份鼠标 WDM 源码从静态文件变成能拦截 IRP 的工具。适合想从零理解 WDM 设备对象模型的开发者也适合在维护 Windows XP 时代遗留鼠标驱动的工程师。2. WDM鼠标驱动源代码的设备栈定位与 IRP 分发骨架2.1 先判断源码包属于设备栈哪一层解压后先不看注释直接在源码目录里搜三个 APIIoCreateDevice、DriverExtension-AddDevice、IoAttachDeviceToDeviceStack。我通常会执行Get-ChildItem -Recurse -Include *.c,*.h | Select-String -Pattern IoAttachDeviceToDeviceStack|DriverExtension-AddDevice|IoCreateDevice | Select-Object -First 20搜索结果能快速区分代码的角色只有IoCreateDevice、没有AddDevice的是 Windows NT 4 时代的传统驱动PnP 管理器不会调用它的AddDevice在现代 Windows 上基本只能作教学参考有AddDevice且调用IoAttachDeviceToDeviceStack的是 WDM 过滤驱动这也是鼠标 WDM 项目里最常见的形态有AddDevice但不做 Attach 的是功能驱动或类驱动要自己处理IRP_MJ_PNP和IRP_MJ_POWER维护成本明显更高。三类角色的差异如下表源码角色设备对象AddDevice 特征在鼠标设备栈中的典型位置功能驱动FDO创建设备对象后直接挂在 PDO 上mouclass 类驱动所在层或替换 i8042prt过滤驱动FiDO创建设备对象后 Attach 到现有设备栈夹在类驱动与端口驱动之间可观察两侧 IRP端口驱动PDO不设 AddDevice由 PnP 枚举器创建i8042prt / hidusb直接接触 PS/2 或 USB 控制器第二个判断点是 INF 文件。源码包自带 INF 时打开看AddService和CopyFiles如果ClassMouse或ClassMouseClass说明它面向鼠标设备类如果里面写了LowerFilters或UpperFilters基本可以确定这是过滤驱动。只通过sc create加载内核服务、没有 INF 关联硬件 ID 的项目多半只完成了驱动骨架离真正附加到鼠标设备栈还差一步。2.2 DriverEntry 与 AddDevice 的分工边界WDM 驱动里有两个不同时机的入口。DriverEntry在驱动服务被加载时执行运行在PASSIVE_LEVEL可以分配非分页内存、注册DriverUnload、填充MajorFunction数组。而AddDevice由 PnP 管理器在枚举到匹配设备时调用每个物理设备实例都会触发一次传入的参数是物理设备对象PhysicalDeviceObject。过滤驱动的AddDevice要做的事比功能驱动少但顺序不能错先用IoCreateDevice创建一个过滤设备对象并分配设备扩展再从设备扩展中取出LowerDeviceObject指针在调用IoAttachDeviceToDeviceStack之前必须把DO_DEVICE_INITIALIZING从新设备对象的 Flags 中清掉。最后把下层设备的DeviceType、Characteristics、AlignmentRequirement和缓存方式抄到自己的设备对象上。抄缓存方式这一步常被省略结果过滤驱动与下层驱动对METHOD_BUFFERED还是METHOD_DIRECT的理解不一致蓝屏时很难定位。设备扩展本身是IoCreateDevice分配的一块非分页内存过滤驱动的所有状态都要放这里包括下层设备对象指针、统计计数、以及可选的取消例程上下文。注意不要在这个结构里放分页池指针因为读完成例程可能在DISPATCH_LEVEL运行此时访问分页内存会触发 bugcheck。2.3 哪些 IRP 要做完成例程哪些直接透传鼠标过滤驱动的大多数 IRP 不需要处理。IRP_MJ_PNP和IRP_MJ_POWER几乎都是原样透传过滤驱动只需要在下层完成后感知事件不应该截留电源请求。真正值得接管的是IRP_MJ_READ类驱动往下发读请求端口驱动在完成时把鼠标返回的数据包填入 IRP 缓冲区过滤驱动可以在完成例程里观察甚至改写数据。透传有两种写法区别非常关键。不需要观察结果的 IRP 用IoSkipCurrentIrpStackLocation跳过当前 I/O 栈单元然后直接IoCallDriver需要在下层完成后运行的必须用IoCopyCurrentIrpStackLocationToNext拷贝栈单元再调用IoSetCompletionRoutine注册完成例程。如果对IRP_MJ_READ用了IoSkipCurrentIrpStackLocation完成例程不会被执行因为 IRP 栈里已经没有当前驱动的栈位置信息。反过来说如果对不需要处理的 PnP IRP 也做Copy SetCompletionRoutine会增加无谓的栈拷贝和调用开销还容易在取消路径上写错状态。这个选择是过滤驱动蓝屏的高发点。判断标准只有一个你需不需要在下层驱动完成这个 IRP 之后做点事。需要就 Copy不需要就 Skip。3. 在 WDK 里编译一个最小 WDM 鼠标过滤驱动3.1 可直接用的 DriverEntry 和 AddDevice 骨架下面这个代码是一个能在鼠标设备栈里挂载的最小过滤驱动。它拦截IRP_MJ_READ在完成例程里打印读取到的字节数其余 IRP 全部透传。把它保存为MouFilter.c。#include ntddk.h typedef struct _MOUFILTER_DEVICE_EXTENSION { PDEVICE_OBJECT LowerDeviceObject; // 挂在设备栈上后下一层设备对象 PDEVICE_OBJECT FilterDeviceObject; // 本驱动创建的过滤设备对象 LONG ReadCount; // 收到的读请求累计数 } MOUFILTER_DEVICE_EXTENSION, *PMOUFILTER_DEVICE_EXTENSION; NTSTATUS MouFilter_DispatchPassDown(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS MouFilter_AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject); NTSTATUS MouFilter_ReadCompletionRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context); VOID MouFilter_Unload(PDRIVER_OBJECT DriverObject); NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { ULONG index; DriverObject-DriverUnload MouFilter_Unload; DriverObject-DriverExtension-AddDevice MouFilter_AddDevice; for (index 0; index IRP_MJ_MAXIMUM_FUNCTION; index) { DriverObject-MajorFunction[index] MouFilter_DispatchPassDown; } return STATUS_SUCCESS; } NTSTATUS MouFilter_AddDevice( PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject) { NTSTATUS status; PDEVICE_OBJECT filterDeviceObject NULL; PMOUFILTER_DEVICE_EXTENSION extension; UNICODE_STRING deviceName; // 设备名只做演示生产代码应该为每个实例生成唯一名字 RtlInitUnicodeString(deviceName, L\\Device\\MouFilter); status IoCreateDevice( DriverObject, sizeof(MOUFILTER_DEVICE_EXTENSION), deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, filterDeviceObject); if (!NT_SUCCESS(status)) { return status; } extension (PMOUFILTER_DEVICE_EXTENSION)filterDeviceObject-DeviceExtension; RtlZeroMemory(extension, sizeof(MOUFILTER_DEVICE_EXTENSION)); extension-FilterDeviceObject filterDeviceObject; // 先抄下层设备的属性再清 DO_DEVICE_INITIALIZING最后 Attach filterDeviceObject-DeviceType FILE_DEVICE_UNKNOWN; filterDeviceObject-Characteristics 0; filterDeviceObject-AlignmentRequirement PhysicalDeviceObject-AlignmentRequirement; filterDeviceObject-Flags ~DO_DEVICE_INITIALIZING; extension-LowerDeviceObject IoAttachDeviceToDeviceStack(filterDeviceObject, PhysicalDeviceObject); if (extension-LowerDeviceObject NULL) { IoDeleteDevice(filterDeviceObject); return STATUS_DEVICE_REMOVED; } // 缓存方式与下层对齐避免缓冲区类型不一致 if ((extension-LowerDeviceObject-Flags DO_BUFFERED_IO) ! 0) { filterDeviceObject-Flags | DO_BUFFERED_IO; } if ((extension-LowerDeviceObject-Flags DO_DIRECT_IO) ! 0) { filterDeviceObject-Flags | DO_DIRECT_IO; } return STATUS_SUCCESS; } NTSTATUS MouFilter_DispatchPassDown(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PMOUFILTER_DEVICE_EXTENSION extension (PMOUFILTER_DEVICE_EXTENSION)DeviceObject-DeviceExtension; PIO_STACK_LOCATION currentStack IoGetCurrentIrpStackLocation(Irp); if (currentStack-MajorFunction IRP_MJ_READ) { IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine( Irp, MouFilter_ReadCompletionRoutine, extension, TRUE, // InvokeOnSuccess TRUE, // InvokeOnError TRUE); // InvokeOnCancel } else { IoSkipCurrentIrpStackLocation(Irp); } return IoCallDriver(extension-LowerDeviceObject, Irp); } NTSTATUS MouFilter_ReadCompletionRoutine( PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context) { PMOUFILTER_DEVICE_EXTENSION extension (PMOUFILTER_DEVICE_EXTENSION)Context; if (NT_SUCCESS(Irp-IoStatus.Status)) { InterlockedIncrement(extension-ReadCount); KdPrint((MouFilter: READ complete, bytes%lu, total%lu\n, (ULONG)Irp-IoStatus.Information, (ULONG)InterlockedCompareExchange( (LONG*)extension-ReadCount, 0, 0))); } return STATUS_SUCCESS; } VOID MouFilter_Unload(PDRIVER_OBJECT DriverObject) { KdPrint((MouFilter: unload\n)); }这段代码的要点在三个地方。第一IoCreateDevice的第六个参数Exclusive必须设为FALSE过滤驱动不能独占设备否则会影响上层类驱动打开设备。第二IoSetCompletionRoutine的三个布尔参数分别控制成功、出错、取消时是否调用完成例程鼠标读请求会有取消路径所以三个都传TRUE。第三完成例程里不能调用任何可能阻塞的函数Irp-IoStatus.Information在此时已经携带了下层实际传输的字节数打印它是安全的。3.2 编译、测试签名与 INF 装载参数老项目压缩包里的源码如果是用 DDK 的sources、dirs文件组织的直接拖到现代 WDK 里编译通常会因为头文件路径不同而失败。我一般会新建一个 WDM 空项目把.c文件加进去再在项目文件里把DriverType设置成WDM而不是 KMDF。WDK 10 配合 Visual Studio 的编译命令是msbuild.exe MouFilter.vcxproj /p:ConfigurationRelease /p:Platformx64编译后得到MouFilter.sys。测试机需要开启测试签名管理员命令行里执行bcdedit /set testsigning on重启后就能加载未签名的内核驱动。把 INF 和 sys 放到同一个目录INF 里最核心的是AddService和CopyFiles[MouFilter_DDI.NT] CopyFilesFilesToCopy [MouFilter_DDI.NT.Services] AddService MouFilter, 0x00000002, MouFilter_Service_Inst [MouFilter_Service_Inst] DisplayName MouFilter ServiceType 1 StartType 3 ErrorControl 1 ServiceBinary %12%\MouFilter.sys [FilesToCopy] MouFilter.sys %12%AddService后面的0x00000002是SPSVCINST_ASSOCSERVICE表示这个服务要关联到当前设备实例而不是一个独立启动的内核服务。ServiceType 1是内核驱动StartType 3是手动启动配合 PnP 管理器在枚举到鼠标设备时自动加载。如果只想快速验证驱动代码能进DriverEntry可以用sc create手动加载但它不会把 FiDO 挂到鼠标设备栈sc create MouFilter type kernel binPath C:\drivers\MouFilter.sys sc start MouFilter sc stop MouFilter sc delete MouFiltersc的等号后面必须有空格这是 Windows 服务控制命令的老规矩。sc start能成功只代表 Loader 路径通不代表过滤驱动真的附加到了设备栈要做功能验证必须走 INF 安装然后重启。4. WinDbg 双机调试下验证鼠标 IRP 是否穿过过滤驱动4.1 断点命中不了时先查设备栈而不是查语法过滤驱动装好后最常见的现象是 WinDbg 里下了断点动鼠标却毫无反应。先不要怀疑断点命令写错应该先确认过滤驱动是否真的挂在了目标设备的设备栈上。虚拟机里用串口管道做双机调试时宿主机 WinDbg 连接参数如下windbg -k com:port\\.\pipe\com_1,baud115200,pipe目标机需要开启内核调试bcdedit /debug on bcdedit /dbgsettings serial debugport:1 baudrate:115200连上后先看驱动对象有没有创建设备!drvobj MouFilter 1如果输出为空说明AddDevice没有被调用问题多半出在 INF 的硬件 ID 匹配不上鼠标设备。如果能看到设备对象再在这个驱动入口下断点bu moufilter!MouFilter_DispatchPassDown g断点没命中时优先用!irp找当前鼠标栈里的挂载点。把鼠标触发一次移动然后中断目标机执行!irp $irpIRP 输出里会列出每一层IO_STACK_LOCATION的DeviceObject和MajorFunction。如果整个链路上看不到MouFilter说明过滤设备对象没在这条链路上这是IoAttachDeviceToDeviceStack挂错了对象或挂载顺序不对。所谓“当前不会命中断点”一半是符号路径不对另一半就是这种设备栈挂载失败。4.2 调试鼠标过滤驱动的常用 WinDbg 命令命令用途容易踩的坑!drvobj MouFilter 1查看驱动对象是否创建了设备对象列出设备链表驱动服务加载失败时输出为空!irp $irp查看 IRP 的每一层栈单元确认过滤驱动在链路中只能在断点中执行且$irp依赖当前上下文dt moufilter!MOUFILTER_DEVICE_EXTENSION查看设备扩展里ReadCount和下层设备指针.pdb没加载时提示找不到类型lm m moufilter确认MouFilter.sys已加载及加载基址显示no symbols时检查_NT_SYMBOL_PATHdt命令看设备扩展是验证统计逻辑最快的手段。断点停在MouFilter_ReadCompletionRoutine时执行dt moufilter!MOUFILTER_DEVICE_EXTENSION 设备扩展地址能看到ReadCount是否在下层完成读请求后自增。如果ReadCount不涨但断点确实进入了完成例程说明InterlockedIncrement的执行路径和预期不同如果断点根本没进就先回头查IRP_MJ_READ是否走了IoCopyCurrentIrpStackLocationToNext因为只用IoSkipCurrentIrpStackLocation的过滤驱动是不会有完成例程的。4.3 设备管理器报代码 31 时的排查顺序代码 31 表示 Windows 无法加载这个设备所需的驱动程序。鼠标过滤驱动安装后如果设备管理器出现黄色感叹号先看 setupapi 日志Select-String -Path C:\Windows\inf\setupapi.dev.log -Pattern MouFilter -Context 2,2日志会明确写出失败阶段是文件复制失败、服务注册失败还是DriverEntry返回了错误。代码 31 的常见原因有三个INF 里ServiceBinary指向的路径不存在测试签名没开导致系统拒绝加载未签名驱动DriverEntry或AddDevice返回了非成功状态码。其中第三类最隐蔽因为退出DriverEntry时返回STATUS_SUCCESS但AddDevice里IoCreateDevice失败被忽略设备节点仍然会报错。排查时直接看 setupapi 日志里AddDevice对应的行通常能得到真正的 NTSTATUS 值。5. 把源码驱动的读取计数暴露给用户态一个调试 IOCTL5.1 在过滤驱动里增加一个查询接口WinDbg 能看内核态状态但每次都要断点很麻烦。更实用的做法是给源码驱动加一个IRP_MJ_DEVICE_CONTROL分发例程让用户态程序通过DeviceIoControl查询ReadCount。先在驱动头部定义 IOCTL#define IOCTL_MOUFILTER_GET_COUNT \ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x901, METHOD_BUFFERED, FILE_READ_DATA)然后在DriverEntry里把MajorFunction[IRP_MJ_DEVICE_CONTROL]指向自己的处理函数。为了让用户态能打开设备对象AddDevice里需要创建符号链接多个鼠标实例需要避免链接名冲突时可以用InterlockedCompareExchange保证只创建一次。处理函数本身很短NTSTATUS MouFilter_DeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION irpSp IoGetCurrentIrpStackLocation(Irp); PMOUFILTER_DEVICE_EXTENSION extension DeviceObject-DeviceExtension; ULONG info 0; if (irpSp-Parameters.DeviceIoControl.IoControlCode IOCTL_MOUFILTER_GET_COUNT) { if (irpSp-Parameters.DeviceIoControl.OutputBufferLength sizeof(ULONG)) { *(PULONG)Irp-AssociatedIrp.SystemBuffer (ULONG)InterlockedCompareExchange( (LONG*)extension-ReadCount, 0, 0); info sizeof(ULONG); } Irp-IoStatus.Status STATUS_SUCCESS; } else { IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(extension-LowerDeviceObject, Irp); } Irp-IoStatus.Information info; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }用户态用 CreateFile 打开\\.\MouFilter然后循环调用DeviceIoControl即可看到读取计数是否增加HANDLE h CreateFileW(L\\\\.\\MouFilter, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); DWORD count 0, bytes 0; DeviceIoControl(h, IOCTL_MOUFILTER_GET_COUNT, NULL, 0, count, sizeof(count), bytes, NULL); printf(read count%lu\n, count); CloseHandle(h);5.2 最终判定计数增长等于 IRP 确实穿过过滤层这个调试接口比 WinDbg 更适合做回归验证重启后先读一次计数再快速晃动鼠标再读一次。如果计数增长说明IRP_MJ_READ确实经过了过滤驱动的完成例程设备栈挂载和驱动加载两个问题同时被排除。如果计数不增长优先在MouFilter_DispatchPassDown下断点看IRP_MJ_READ是否进入了该函数如果进入了但计数不涨检查完成例程的Context是否指向了正确的设备扩展两个鼠标实例共用一个全局变量是这类统计丢失的最常见原因。验证通过后把KdPrint换成ETW事件再把这个查询 IOCTL 保留成诊断入口就能脱离调试器做长期运行监控。本文还有配套的精品资源点击获取
返回列表