KMDF串口过滤驱动实战:拦截IRP抓取数据与避坑指南

发布时间:2026/10/1 13:26:49
KMDF串口过滤驱动实战:拦截IRP抓取数据与避坑指南 简介这份资源聚焦Windows平台下的串口驱动过滤技术面向具备一定驱动开发基础、希望深入理解串口通信拦截与定制的开发者。内容围绕串口过滤驱动展开讲解如何在系统串口驱动堆栈中插入自定义驱动实现数据监控、流修改与安全增强并涉及上滤驱动与下滤驱动的分工以及WDM、UWD框架下的开发思路与Windbg调试方法。压缩包共5个文件约121KB包含sln解决方案、vcxproj工程文件、cpp源码、dll动态库与exe可执行程序覆盖从工程搭建到编译测试的完整链路便于直接研究过滤驱动的实现结构。目前已有383人学习下载。对于想动手实践简单串口过滤驱动、理解内核态I/O请求处理流程的读者可借助源码与配套工具快速验证通信行为积累驱动调试与排错经验。1. 串口过滤驱动到底拦的是什么从 CH340 被占用说起你插上一块 CH340 或 FTDI 的 USB 转串口板上位机程序打开 COM3 正常收发关掉程序想重连突然报「端口拒绝访问」或者你写了个协议解析工具想在不改动原厂上位机的前提下把每一帧进出串口的数据顺手记下来做审计。这两种场景最后都会把你引到同一个技术点上串口过滤驱动。它不接管物理串口也不重写 USB 转串口芯片的厂商驱动而是把自己插在「应用层 CreateFile 打开 COM 口」和「底层串口功能驱动」之间对 IRP 做拦截、记录、改写甚至拒绝。标题里的「简单串口过滤驱动」重点在「简单」——不搞协议栈、不做虚拟串口对只做一层可加载、可卸载、能看清数据流向的过滤。适合谁适合要抓串口通信黑匣子、要做端口访问控制、要复现「端口被占用」这类玄学问题的嵌入式与上位机工程师。下面按 KMDF 的路子把选型、骨架、参数和踩坑一次讲透。2. 为什么选 KMDF 过滤驱动而不是虚拟串口对先立住选型2.1 过滤驱动、虚拟串口、API Hook 三条路的边界做串口数据拦截从业者手里通常有三条路。第一条是虚拟串口对用 com0com 这类工具造出一对互联的虚拟口让上位机连一端、你的程序连另一端做中转。它上手快但缺点很硬必须改上位机的端口号配置原程序如果写死了 COM3 就废了而且它拦的是「你愿意转发的数据」原程序直连物理口时你什么都看不到。第二条是 API Hook在用户态拦 CreateFile、ReadFile、WriteFile。它不用装驱动但只对走 Win32 API 的进程有效遇到直接走内核或换 API 集的程序就漏稳定性也随目标进程架构变化。第三条就是内核态过滤驱动挂在串口功能设备对象FDO之上成为过滤设备对象FiDO。它不动端口号、不改应用、对所有进程透明代价是要写内核代码、要处理即插即用和电源 IRP。「简单串口过滤驱动」这个标题落点就在第三条。选它的核心理由是透明性应用层看到的还是原来的 COM 口过滤逻辑对上层不可见。KMDF 相比老式 WDM把 IRP 分发、设备对象生命周期、即插即用状态机封装成了框架回调写一个能加载的过滤驱动代码量能压到几百行这正是「简单」二字的来源。2.2 过滤驱动在设备栈里的位置理解位置比背 API 重要。一个 USB 转串口设备插上后设备栈大致是总线驱动枚举出物理设备对象PDO厂商的串口功能驱动创建 FDO上层还有一层或多层过滤驱动。你要做的是把自己注册成「上层过滤驱动」Upper Filter附加在 FDO 之上。这样应用打开 COM 口时IRP 先到你的 FiDO你决定放行、记录还是拦截再往下传。注册成上层过滤靠的是 INF 里的UpperFilters值或者用FilterClass配合服务安装。这里有个关键认知过滤驱动不创建新的设备接口它复用底层 FDO 的符号链接所以应用看到的 COM 号不变。这也是它比虚拟串口对更适合「无感抓包」的根本原因。2.3 最小可加载骨架INF 与 DriverEntry先给一个能编译、能装、能卸的最小骨架。下面这段是 KMDF 过滤驱动的入口和 EvtDeviceAdd 回调重点看它如何声明自己是被动级过滤。// simple_serial_filter.c #include wdf.h DRIVER_INITIALIZE DriverEntry; EVT_WDF_DRIVER_DEVICE_ADD EvtDeviceAdd; NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { WDF_DRIVER_CONFIG config; // 关键声明为过滤驱动框架不会去创建 FDO WDF_DRIVER_CONFIG_INIT(config, EvtDeviceAdd); config.DriverInitFlags | WdfDriverInitNonPnpDriver; // 视安装方式取舍 return WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, config, WDF_NO_HANDLE); } NTSTATUS EvtDeviceAdd(_In_ WDFDRIVER Driver, _Inout_ PWDFDEVICE_INIT DeviceInit) { WDFDEVICE device; // 标记为过滤设备附加到已有设备栈 WdfFdoInitSetFilter(DeviceInit); return WdfDeviceCreate(DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, device); }逻辑说明WdfFdoInitSetFilter是整段代码的灵魂它告诉 KMDF「我不是功能驱动别给我创建 FDO我要附加到别人的栈上」。少了这一句驱动会尝试自己枚举设备装上去也拦不到任何 IRP。参数上WDF_DRIVER_CONFIG_INIT的第二个参数是设备添加回调过滤驱动里它只负责附加不负责硬件资源。DriverInitFlags是否加WdfDriverInitNonPnpDriver取决于你用 INF 安装还是用服务方式加载用 INF 的UpperFilters注册时通常不需要。对应的 INF 关键片段如下UpperFilters的值必须和你的服务名一致[Version] Signature$WINDOWS NT$ ClassPorts ClassGuid{4d36e978-e325-11ce-bfc1-08002be10318} [DefaultInstall.Services] AddServiceSimpleSerialFilter,,FilterService [FilterService] DisplayName %FilterServiceDesc% ServiceType 1 StartType 3 ErrorControl 1 ServiceBinary %12%\simple_serial_filter.sys [DefaultInstall] AddRegFilterAddReg [FilterAddReg] HKR,,UpperFilters,0x00010000,SimpleSerialFilter参数说明ServiceType1表示内核驱动StartType3是手动启动调试阶段建议保持手动避免蓝屏后系统反复加载。UpperFilters是 REG_MULTI_SZ多个过滤驱动用逗号分隔顺序就是 IRP 经过的顺序。这里最容易翻车的是 ClassGuid 写错——串口属于 Ports 类写成别的类INF 装上去不报错但过滤不生效。3. 拦截读写 IRP把每一帧串口数据落到日志3.1 注册 EvtIoRead / EvtIoWrite 与队列配置骨架能加载只是第一步真正干活的是拦截读写。KMDF 里拦截 IRP 靠 I/O 队列你要在 EvtDeviceAdd 里创建队列并注册读、写、设备控制三类回调。NTSTATUS EvtDeviceAdd(_In_ WDFDRIVER Driver, _Inout_ PWDFDEVICE_INIT DeviceInit) { WDFDEVICE device; WDF_IO_QUEUE_CONFIG queueConfig; WDFQUEUE queue; WdfFdoInitSetFilter(DeviceInit); NTSTATUS status WdfDeviceCreate(DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, device); if (!NT_SUCCESS(status)) return status; // 并行队列读写可以并发避免串口半双工被我们拖死 WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE( queueConfig, WdfIoQueueDispatchParallel); queueConfig.EvtIoRead EvtIoRead; queueConfig.EvtIoWrite EvtIoWrite; queueConfig.EvtIoDeviceControl EvtIoDeviceControl; return WdfIoQueueCreate(device, queueConfig, WDF_NO_OBJECT_ATTRIBUTES, queue); }逻辑说明队列分发方式选WdfIoQueueDispatchParallel而不是 Sequential是因为串口读写本身可能并发用顺序队列会把请求排队轻则延迟变大重则上位机超时。参数上WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE创建的是默认队列所有没被其他队列认领的 IRP 都会进来。EvtIoDeviceControl必须注册因为串口配置波特率、设置超时都走 IOCTL漏了它端口能打开但配置不了。3.2 在 EvtIoWrite 里抓发送数据并转发写回调是抓「上位机发出去的数据」的地方。核心动作是拿到请求的内存缓冲、记录、然后把请求原样转发给下层。VOID EvtIoWrite(_In_ WDFQUEUE Queue, _In_ WDFREQUEST Request, _In_ size_t Length) { WDFMEMORY memory; PVOID buffer; NTSTATUS status WdfRequestRetrieveInputMemory(Request, memory); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } buffer WdfMemoryGetBuffer(memory, NULL); // 只记录前 64 字节避免大包刷爆日志 size_t logLen min(Length, (size_t)64); DbgPrint(SERFILTER TX len%zu data%!HEX!\n, Length, buffer, logLen); // 原样转发不修改请求 WdfRequestFormatRequestUsingCurrentType(Request); WdfRequestSend(Request, WdfDeviceGetDefaultQueue( WdfIoQueueGetDevice(Queue)), WDF_NO_SEND_OPTIONS); }逻辑说明WdfRequestRetrieveInputMemory拿到的是请求的输入缓冲对写请求就是待发送数据。记录后必须转发否则数据发不出去。这里用WdfRequestFormatRequestUsingCurrentType保持原 IRP 类型再WdfRequestSend往下发。参数上min(Length, 64)是日志节流串口波特率低时无所谓但高速串口下每帧都全量打印会拖垮系统。DbgPrint只适合调试正式版应换成 ETW 或写环形缓冲DbgPrint在 release 下开销不可控。3.3 读方向的数据抓取与缓冲对齐读方向比写方向麻烦因为数据是下层驱动填进来的你要在请求完成之后才能看到内容。常见做法是给读请求设置完成例程在例程里读缓冲。VOID EvtIoRead(_In_ WDFQUEUE Queue, _In_ WDFREQUEST Request, _In_ size_t Length) { WDFMEMORY memory; NTSTATUS status WdfRequestRetrieveOutputMemory(Request, memory); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } // 保存 memory 到请求上下文完成例程里再取 WDF_OBJECT_ATTRIBUTES attr; WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(attr, READ_CTX); PREAD_CTX ctx; WdfObjectAllocateContext(Request, attr, (PVOID*)ctx); ctx-Memory memory; WdfRequestFormatRequestUsingCurrentType(Request); WdfRequestSetCompletionRoutine(Request, ReadComplete, ctx); WdfRequestSend(Request, WdfDeviceGetDefaultQueue( WdfIoQueueGetDevice(Queue)), WDF_NO_SEND_OPTIONS); } VOID ReadComplete(_In_ WDFREQUEST Request, _In_ WDFIOTARGET Target, _In_ PWDF_REQUEST_COMPLETION_PARAMS Params, _In_ WDFCONTEXT Context) { PREAD_CTX ctx (PREAd_CTX)Context; if (NT_SUCCESS(Params-IoStatus.Status)) { PVOID buf WdfMemoryGetBuffer(ctx-Memory, NULL); size_t len Params-IoStatus.Information; DbgPrint(SERFILTER RX len%zu\n, len); } WdfRequestComplete(Request, Params-IoStatus.Status); }逻辑说明读请求的缓冲在请求完成前是空的所以必须挂完成例程等下层填完数据再读。WdfObjectAllocateContext把 memory 句柄存进请求上下文完成例程里通过 Context 取回。参数上Params-IoStatus.Information是实际读到的字节数不能直接用请求的 Length因为串口读可能只返回部分数据。这里有个血泪经验完成例程里必须调用WdfRequestComplete把请求还给框架漏了它请求永远挂起上位机读操作直接卡死。4. 避坑与排查串口过滤驱动最容易翻车的五件事4.1 装完驱动端口打不开报「拒绝访问」现象INF 安装成功设备管理器里串口正常但应用一打开就报拒绝访问。原因通常是过滤驱动的队列没有正确转发 Create 请求或者EvtIoDeviceControl里对某个 IOCTL 直接完成了失败状态。排查方法先在EvtIoDeviceControl里对所有请求无条件转发确认端口能打开再逐个加拦截逻辑。解决Create 请求走的是EvtIoCreate或默认队列的创建路径确保没有被你误判为非法请求。4.2 蓝屏 IRQL_NOT_LESS_OR_EQUAL现象一收发数据就蓝屏错误码常见 IRQL_NOT_LESS_OR_EQUAL。原因是在 DISPATCH_LEVEL 上调用了只能在 PASSIVE_LEVEL 用的函数比如WdfRequestRetrieveInputMemory在某些路径下要求低 IRQL或者你在完成例程里访问了分页内存。解决把数据拷贝和日志写入放到WdfWorkItem里异步做完成例程里只做状态判断和请求完成。参数上完成例程的 IRQL 可能是 DISPATCH_LEVEL任何可能触发页错误的操作都不能碰。4.3 日志丢帧高速串口下数据对不上现象低速时日志完整波特率上到 921600 后日志缺帧。原因是DbgPrint是同步且慢的在 IRP 路径里直接打印会阻塞后续请求。解决改成写环形缓冲用户态程序定期读取或者用 ETW 的TraceLogging接口内核态开销可控。注意缓冲要预分配不能在 IRP 路径里动态申请内存否则高负载下分配失败会丢数据。4.4 卸载驱动后端口消失或系统不稳定现象sc stop或设备管理器卸载过滤驱动后串口设备异常。原因是过滤驱动卸载时没有正确处理即插即用 IRP或者队列里还有未完成的请求。解决在EvtDeviceReleaseHardware或EvtDeviceSurpriseRemoval里取消所有挂起请求用WdfIoQueuePurge清空队列。参数上WdfIoQueuePurge是同步的会等待所有已分发请求完成别在持有自旋锁时调用。4.5 多个过滤驱动叠加顺序错乱现象系统里已有别的串口过滤驱动你的驱动装上后行为异常。原因是UpperFilters是顺序敏感的IRP 按列表顺序经过。解决用reg query HKLM\SYSTEM\CurrentControlSet\Enum\...\Device Parameters /v UpperFilters看清现有顺序决定自己插在前面还是后面。注意修改UpperFilters后要重新枚举设备才生效直接重启最稳。5. 用 ETW 替代 DbgPrint让过滤驱动在生产环境可观测调试阶段DbgPrint够用但一旦要长期跑在生产机上它的开销和日志管理都是灾难。我一般会把日志通道换成 ETW配合一个用户态收集程序。下面是一个最小 ETW 提供程序注册和写入的写法。// 注册 ETW 提供程序句柄 REGHANDLE g_EtwHandle 0; EventRegister(SERFILTER_PROVIDER_GUID, NULL, NULL, g_EtwHandle); // 在 IRP 路径里写事件注意用预分配的 EVENT_DATA_DESCRIPTOR void LogSerialEvent(PVOID data, size_t len, UCHAR dir) { EVENT_DATA_DESCRIPTOR desc[2]; EventDataDescCreate(desc[0], dir, sizeof(dir)); EventDataDescCreate(desc[1], data, (ULONG)len); EventWrite(g_EtwHandle, SERIAL_EVENT, 2, desc); }逻辑说明EventRegister在 DriverEntry 里调用一次拿到句柄。EventWrite是异步的内核态开销远低于DbgPrint而且用户态可以用logman或自己写的消费者按需开启不开时几乎零成本。参数上EventDataDescCreate的缓冲区必须在事件写入完成前保持有效所以别传栈上的临时变量给异步路径用请求上下文里的内存。验证方法上我习惯分三步先用logman start开一个会话确认事件能收到再用波特率 115200 和 921600 各跑一轮对比丢帧率最后做一次热插拔和驱动卸载确认没有残留句柄。这套流程跑通一个「简单串口过滤驱动」才算真正能交付。我自己踩得最狠的一次是在完成例程里直接DbgPrint大缓冲低速测试全过现场高速设备一上就蓝屏后来全部改成 ETW 加环形缓冲才稳住。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询