CommModifyKit 是一个串口监控与数据拦截套件,由用户态 DLL 和内核态驱动两部分组成。它以透明过滤驱动的方式挂载到串口设备栈上,实时捕获串口的读写数据,并通过回调函数将事件上报给上层应用(CommModifyService)。
TOnData 回调实时上抛CommModifyKit/
├── CommModifyKit.sln # VS 解决方案
├── common/ # 用户态/内核态共享定义
│ ├── CommKitEvents.h # 事件类型常量
│ └── CommKitIoctl.h # IOCTL 码 + 共享数据结构
├── dll/ # 用户态 DLL 项目
│ ├── CommModifyKit.vcxproj # VS 项目文件
│ ├── CommModifyKit.def # 导出定义(无修饰名)
│ ├── dllmain.cpp # DLL 入口(最小化)
│ ├── exports.cpp # 7 个 __stdcall 导出函数
│ ├── MonitorManager.h/cpp # 核心管理器(单例)
│ ├── DriverClient.h/cpp # 驱动 IOCTL 通信封装
│ ├── EventLoop.h/cpp # 事件循环工作线程
│ ├── ErrorStore.h/cpp # 线程局部错误码
│ ├── Logger.h/cpp # 日志系统
│ └── pch.h # 预编译头
├── driver/ # 内核态驱动项目
│ ├── CommModifyKitDriver.vcxproj # VS 项目文件(WDK)
│ ├── CommModifyKit.inf # 驱动安装 INF
│ ├── DriverEntry.cpp # KMDF 驱动入口
│ ├── ClientConnection.h/cpp # 用户态连接管理
│ ├── QueueCallback.h/cpp # I/O Queue IOCTL 处理
│ ├── SerialFilter.h/cpp # 串口 IRP 拦截
│ ├── EventRingBuffer.h/cpp # 内核环形缓冲区
│ └── DeviceContext.h # WDF 设备上下文
└── installer/ # 安装/卸载脚本
├── install.ps1 # 安装脚本(管理员)
└── uninstall.ps1 # 卸载脚本(管理员)
┌─────────────────────────────────────────────────────┐
│ CommModifyService (上层应用) │
│ 通过 GetProcAddress 调用 DLL 导出函数 │
└────────────────────────┬────────────────────────────┘
│ __stdcall 导出接口
┌────────────────────────▼────────────────────────────┐
│ CommModifyKit.dll (用户态) │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │MonitorManager│──│ DriverClient │──│ EventLoop │ │
│ │ (单例协调) │ │ (IOCTL 封装) │ │(回调线程) │ │
│ └──────────────┘ └──────┬───────┘ └─────┬──────┘ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌─────▼─────┐ ┌──────▼──────┐ │
│ │ ErrorStore │ │DeviceIoCtl│ │ TOnData回调 │ │
│ │(线程局部错误)│ │ (系统API) │ │ (上报上层) │ │
│ └─────────────┘ └─────┬─────┘ └─────────────┘ │
└────────────────────────────┼─────────────────────────┘
│ IOCTL
┌────────────────────────────▼─────────────────────────┐
│ CommModifyKit.sys (内核态 KMDF 驱动) │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │QueueCallback │──│ClientConnection│──│SerialFilter│ │
│ │ (IOCTL 分发) │ │ (端口/缓冲管理)│ │(IRP 拦截) │ │
│ └──────────────┘ └──────┬───────┘ └─────┬──────┘ │
│ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │EventRingBuf │ │下层串口设备栈│ │
│ │(每端口环形缓冲)│ │(真实串口驱动)│ │
│ └─────────────┘ └─────────────┘ │
└──────────────────────────────────────────────────────┘
应用读/写串口 → IRP 到达过滤驱动 → SerialFilter 拦截
→ IRP 透传到下层串口 → 下层完成 IRP → 完成例程回调
→ CaptureIrpData() 提取数据 → Push() 写入环形缓冲
→ DLL EventLoop 线程 ReadEvents IOCTL → 批量弹出事件
→ TOnData 回调 → 上层应用收到事件
上层调用 SetWrite_Data → MonitorManager.WriteData()
→ DriverClient.WritePort() → IOCTL_COMMKIT_WRITE_PORT
→ 驱动 HandleWritePort() → 构造 IRP 发给下层串口
DLL 导出 7 个 __stdcall 函数,通过 .def 文件确保导出名无修饰:
| 序号 | 函数名 | 参数 | 返回值 | 说明 |
|---|---|---|---|---|
| 1 | InitMonitor |
wchar_t* cKey, TOnData aOnData |
BOOL | 初始化监控:校验 Key、启动驱动、注册回调 |
| 2 | Monitor |
DWORD dwNumber |
BOOL | 开始监控指定串口(1=COM1) |
| 3 | Stop |
DWORD dwNumber |
BOOL | 停止监控指定串口 |
| 4 | SetWrite_Data |
DWORD dwNumber, char* lpBuffer, int Len |
BOOL | 向串口写数据 |
| 5 | SetRead_Data |
DWORD dwNumber, char* lpBuffer, int Len |
BOOL | 发起异步读请求(结果通过回调返回) |
| 6 | FreeMonitor |
无 | void | 释放所有资源 |
| 7 | get_LastErrror |
无 | int | 获取最近错误码(注意 3 个 r) |
回调函数类型定义:
typedef LONG(CALLBACK* TOnData)(
int Sequence, // 递增序列号
double dTime, // OLE Automation date 时间戳
DWORD ComNumber, // 串口编号(COM n 的 n)
DWORD Irp, // 事件类型(1=OPEN, 2=READ, 3=WRITE, 4=CLOSE)
DWORD dwSize, // 数据字节数
char* lpData // 数据指针(只读,不应释放)
);
职责:协调 DriverClient 与 EventLoop,提供 7 个导出函数的底层实现。
设计要点:
GetMonitorManager() 返回静态实例,导出函数通过它统一入口std::mutex 保护,保证线程安全InitMonitor 流程:
1. 校验回调非空、Key 非空
2. EnsureDriverReady()
├── 检查管理员权限(否则返回 -254)
├── 打开 SCM,检查/创建驱动服务
├── 检查驱动 .sys 文件存在性
└── 启动驱动服务(若未运行)
3. DriverClient.Open() → CreateFile("\\\.\\CommModifyKit")
4. DriverClient.RegisterCallback() → IOCTL_COMMKIT_REGISTER_CALLBACK
5. EventLoop.Start() → 启动事件循环工作线程
错误码表:
| 错误码 | 含义 |
|---|---|
| 0 | 成功 |
| -1 | 未初始化 / 回调为空 |
| 3 | 串口读取失败 |
| 4 | 串口写入失败 |
| -241 | 安装驱动失败(SCM 操作失败) |
| -243 | 其他错误(注册回调/事件循环失败) |
| -244 | 驱动文件不存在或设备打开失败 |
| -252 | Key 无效 |
| -253 | 启动驱动服务失败 |
| -254 | 需要管理员权限 |
| -255 | 绑定串口失败 |
职责:封装 CreateFile + DeviceIoControl,提供同步 IOCTL 调用接口。
| 方法 | 对应 IOCTL | 说明 |
|---|---|---|
Open() |
CreateFile | 打开 \\.\CommModifyKit 设备 |
RegisterCallback() |
IOCTL_COMMKIT_REGISTER_CALLBACK | 注册回调管道 |
AttachPort(com) |
IOCTL_COMMKIT_ATTACH_PORT | 绑定串口 |
DetachPort(com) |
IOCTL_COMMKIT_DETACH_PORT | 解绑串口 |
WritePort(com,data,len) |
IOCTL_COMMKIT_WRITE_PORT | 写数据(METHOD_IN_DIRECT) |
ReadPort(com,data,len) |
IOCTL_COMMKIT_READ_PORT | 读请求(METHOD_IN_DIRECT) |
ReadEvents(buf,max) |
IOCTL_COMMKIT_READ_EVENTS | 批量读取事件 |
FreeAll() |
IOCTL_COMMKIT_FREE_ALL | 释放所有 |
WritePort/ReadPort 数据组装:使用 COMMKIT_DATA_REQUEST 变长结构,实际大小 = sizeof(COMMKIT_DATA_REQUEST) - 1 + len(Data 字段为 1 字节占位)。
职责:独立工作线程循环从驱动读取事件,调用 TOnData 回调上报上层。
工作流程:
while (running_) {
// 阻塞式 IOCTL:驱动无事件时 IRP pending
DeviceIoControl(IOCTL_COMMKIT_READ_EVENTS, batch, ...);
// 处理返回事件
for (event in batch) {
callback_(event.Sequence, event.TimeStamp, ...);
}
}
设计要点:
COMMKIT_EVENT_BATCH(16) 条事件Stop() 设置 running_=false,关闭驱动句柄使阻塞 IOCTL 返回错误,线程自然退出使用 thread_local int 存储,与 Win32 GetLastError() 语义一致:每线程独立,记录最近一次操作结果。
OutputDebugStringW(DebugView 可捕获)+ 可选文件输出(%TEMP%\CommModifyKit.log)[2026-07-21 10:00:00][INF] message关键约束:DllMain 在 Loader Lock 内执行,严禁:
LoadLibrary / LoadLibraryEx(递归加载死锁)std::thread、std::mutex 动态初始化)因此 DllMain 仅执行 DisableThreadLibraryCalls,所有初始化延迟到首次导出函数调用。
CreateControlDevice() 创建控制设备 \\.\CommModifyKitEvtDeviceAdd(PnP 过滤设备创建)和 EvtDriverUnloadEvtDeviceAdd 实现(核心):
WdfFdoInitSetFilter() 标记为过滤设备EvtDevicePrepareHardware 回调(解析 COM 编号)WdfDeviceInitAssignWdmIrpPreprocessCallback 注册 IRP_MJ_READ/WRITE/DEVICE_CONTROL 预处理回调WdfDeviceCreate() 创建 WDF 过滤设备DEVICE_CONTEXT(ComNumber=0,由 PrepareHardware 解析)RegisterFilterDevice() 注册到全局端口表EvtDevicePrepareHardware 实现:
DevicePropertyFriendlyName 或 DevicePropertyDeviceDescription 解析 COM 编号UpdatePortComNumber() 更新端口表职责:维护 DLL 与驱动之间的连接状态和端口监控列表。
ports_table_[256],最多支持 256 个 COM 端口EventRingBuffer(非分页内存)KSPIN_LOCK 保护端口表(IRQL 安全)HandleAttachPort 流程:
ports_table_ 中查找)OP_OPEN 事件MonitoringEnabled = TRUE端口上下文来源:
EvtDeviceAdd 为每个串口创建过滤设备并注册到 ports_table_(ComNumber=0)EvtDevicePrepareHardware 从设备属性解析 COM 编号并更新端口表HandleReadEvents 流程:
PopBatch() 弹出事件到用户缓冲基于 WDF I/O Queue 的 Sequential 派发模式,将 IOCTL 码路由到 ClientConnection 对应方法。
控制设备创建流程:
WdfControlDeviceInitAllocate(SDDL_DEVOBJ_SYS_ONLY_ADM) // 仅管理员可访问
→ WdfDeviceInitAssignName("\Device\CommModifyKit")
→ WdfDeviceCreate()
→ WdfDeviceCreateSymbolicLink("\DosDevices\CommModifyKit")
→ WdfIoQueueCreate(Sequential)
→ WdfControlFinishInitializing()
职责:作为上层过滤驱动 attach 到串口设备栈,拦截读写 IRP。
KMDF WDM IRP 预处理模式:
通过 WdfDeviceInitAssignWdmIrpPreprocessCallback 在 EvtDeviceAdd 中注册。
| 预处理回调 | 行为 |
|---|---|
DispatchRead(WDFDEVICE, PIRP) |
注册 OnReadComplete 完成例程 → WdfDeviceWdmDispatchPreprocessedIrp |
DispatchWrite(WDFDEVICE, PIRP) |
注册 OnWriteComplete 完成例程 → WdfDeviceWdmDispatchPreprocessedIrp |
DispatchDeviceControl(WDFDEVICE, PIRP) |
直接 WdfDeviceWdmDispatchPreprocessedIrp 透传 |
完成例程(OnReadComplete/OnWriteComplete):
MdlAddress 或 SystemBuffer)IoStatus.Information 获取实际长度EventRingBuffer.Push() 写入环形缓冲STATUS_SUCCESS 继续完成 IRPCaptureIrpData(IRP 完成后提取数据):
MdlAddress 或 SystemBuffer 获取数据指针IoStatus.Information 获取实际数据长度EventRingBuffer.Push() 写入环形缓冲COMMKIT_RING_CAPACITY = 256 条/端口KSPIN_LOCK(IRQL <= DISPATCH_LEVEL 安全)ExAllocatePool2 + POOL_FLAG_NON_PAGED)PopBatch() 一次弹出多条,减少锁竞争每个串口过滤设备关联的上下文结构:
| 字段 | 类型 | 说明 |
|---|---|---|
ComNumber |
ULONG | COM 编号(0=未知) |
MonitoringEnabled |
BOOLEAN | 是否已通过 ATTACH_PORT 启用 |
RingBuffer |
PVOID | EventRingBuffer 实例指针 |
LowerDevice |
PDEVICE_OBJECT | 下层真实串口设备 |
WdfDevice |
WDFDEVICE | WDF 设备句柄 |
| 常量 | 值 | 含义 |
|---|---|---|
COMMKIT_OP_NONE |
0 | 无事件 |
COMMKIT_OP_OPEN |
1 | 串口打开/开始监控 |
COMMKIT_OP_READ |
2 | 读取到数据 |
COMMKIT_OP_WRITE |
3 | 写入数据 |
COMMKIT_OP_CLOSE |
4 | 串口关闭/停止监控 |
| IOCTL 码 | 功能号 | 方法 | 说明 |
|---|---|---|---|
IOCTL_COMMKIT_REGISTER_CALLBACK |
0x800 | METHOD_BUFFERED | 注册回调 |
IOCTL_COMMKIT_ATTACH_PORT |
0x801 | METHOD_BUFFERED | 绑定串口 |
IOCTL_COMMKIT_DETACH_PORT |
0x802 | METHOD_BUFFERED | 解绑串口 |
IOCTL_COMMKIT_WRITE_PORT |
0x803 | METHOD_IN_DIRECT | 写数据 |
IOCTL_COMMKIT_READ_PORT |
0x804 | METHOD_IN_DIRECT | 读请求 |
IOCTL_COMMKIT_READ_EVENTS |
0x805 | METHOD_OUT_DIRECT | 批量读事件 |
IOCTL_COMMKIT_FREE_ALL |
0x806 | METHOD_BUFFERED | 释放全部 |
关键数据结构(均 #pragma pack(1) 对齐):
// 事件结构(内核 → 用户态)
struct COMMKIT_EVENT {
INT32 Sequence; // 递增序列号
double TimeStamp; // OLE Automation date
UINT32 ComNumber; // 串口编号
UINT32 EventType; // 事件类型
UINT32 DataSize; // 数据字节数(≤ 4096)
CHAR Data[4096]; // 内联数据缓冲
};
// 端口请求(ATTACH/DETACH 输入)
struct COMMKIT_PORT_REQUEST {
UINT32 ComNumber;
};
// 数据请求(WRITE/READ 输入,变长)
struct COMMKIT_DATA_REQUEST {
UINT32 ComNumber;
UINT32 DataLen;
CHAR Data[1]; // 变长数据起始
};
常量:
| 常量 | 值 | 说明 |
|---|---|---|
COMMKIT_MAX_DATA |
4096 | 单条事件最大数据字节 |
COMMKIT_EVENT_BATCH |
16 | 单次 IOCTL 最多返回事件数 |
COMMKIT_RING_CAPACITY |
256 | 每端口环形缓冲容量 |
DllMain 仅执行 DisableThreadLibraryCalls,所有初始化延迟到 InitMonitor 首次调用。PROCESS_DETACH 也不调用 FreeAll,由上层 FreeMonitor() 显式释放。
WorkerThread(EventLoop::Run)使用 __try/__except 包裹,防止 SEH 异常导致线程静默崩溃和初始化超时。
当 WorkerThread 超时未完成时,使用 detach() 而非 join(),避免主线程无限阻塞。
满了覆盖最旧事件而非阻塞 IRP 完成例程,保证串口 I/O 性能不受监控影响。
FreeMonitor 不卸载驱动服务(不调用 DeleteService),驱动在系统生命周期内常驻,避免反复安装/卸载的开销和权限问题。
所有跨边界(内核态↔用户态,WOW64)传递的结构使用 #pragma pack(1),避免对齐差异导致数据损坏。
build\Release\Win32\CommModifyKit.dllbuild\Release\x64\CommModifyKit.sys| 项目 | Win32 配置 | x64 配置 |
|---|---|---|
| CommModifyKit (DLL) | Win32 | x64 |
| CommModifyKitDriver | 映射到 x64 | x64 |
CommModifyService 通过 GetProcAddress 动态加载 DLL 的 7 个导出函数:
// CommModifyService/DllWrapper.cpp 伪代码
HMODULE hDll = LoadLibrary("CommModifyKit.dll");
auto pInitMonitor = (BOOL(__stdcall*)(wchar_t*, TOnData))
GetProcAddress(hDll, "InitMonitor");
auto pMonitor = (BOOL(__stdcall*)(DWORD))
GetProcAddress(hDll, "Monitor");
// ... 其余 5 个函数同理
调用时序:
1. InitMonitor(key, callback) // 初始化
2. Monitor(com_number) // 监控各端口
3. [循环] SetWrite_Data / SetRead_Data // 读写操作
4. [回调] TOnData // 事件到达
5. Stop(com_number) // 停止某端口
6. FreeMonitor() // 全部释放