CommModifyKit-Design.md 20 KB

CommModifyKit 设计文档

1. 项目概述

CommModifyKit 是一个串口监控与数据拦截套件,由用户态 DLL内核态驱动两部分组成。它以透明过滤驱动的方式挂载到串口设备栈上,实时捕获串口的读写数据,并通过回调函数将事件上报给上层应用(CommModifyService)。

核心能力

  • 透明拦截:作为串口 Upper Filter 驱动,无需修改应用程序即可捕获串口数据
  • 实时回调:捕获的读/写事件通过 TOnData 回调实时上抛
  • 双向读写:支持向串口写入数据、发起异步读请求
  • 多端口并行:支持同时监控多个 COM 端口,每端口独立环形缓冲

项目结构

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              # 卸载脚本(管理员)

2. 架构设计

2.1 整体架构

┌─────────────────────────────────────────────────────┐
│               CommModifyService (上层应用)            │
│            通过 GetProcAddress 调用 DLL 导出函数      │
└────────────────────────┬────────────────────────────┘
                         │ __stdcall 导出接口
┌────────────────────────▼────────────────────────────┐
│              CommModifyKit.dll (用户态)               │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │MonitorManager│──│ DriverClient │──│ EventLoop  │ │
│  │  (单例协调)   │  │ (IOCTL 封装) │  │(回调线程)  │ │
│  └──────────────┘  └──────┬───────┘  └─────┬──────┘ │
│         │                  │                 │        │
│  ┌──────▼──────┐    ┌─────▼─────┐   ┌──────▼──────┐ │
│  │ ErrorStore  │    │DeviceIoCtl│   │ TOnData回调 │ │
│  │(线程局部错误)│    │  (系统API) │   │  (上报上层)  │ │
│  └─────────────┘    └─────┬─────┘   └─────────────┘ │
└────────────────────────────┼─────────────────────────┘
                             │ IOCTL
┌────────────────────────────▼─────────────────────────┐
│           CommModifyKit.sys (内核态 KMDF 驱动)        │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │QueueCallback │──│ClientConnection│──│SerialFilter│ │
│  │ (IOCTL 分发) │  │ (端口/缓冲管理)│  │(IRP 拦截)  │ │
│  └──────────────┘  └──────┬───────┘  └─────┬──────┘ │
│                           │                  │        │
│                    ┌──────▼──────┐   ┌──────▼──────┐ │
│                    │EventRingBuf │   │下层串口设备栈│ │
│                    │(每端口环形缓冲)│   │(真实串口驱动)│ │
│                    └─────────────┘   └─────────────┘ │
└──────────────────────────────────────────────────────┘

2.2 数据流

监控捕获流程(读/写事件上抛)

应用读/写串口 → IRP 到达过滤驱动 → SerialFilter 拦截
    → IRP 透传到下层串口 → 下层完成 IRP → 完成例程回调
    → CaptureIrpData() 提取数据 → Push() 写入环形缓冲
    → DLL EventLoop 线程 ReadEvents IOCTL → 批量弹出事件
    → TOnData 回调 → 上层应用收到事件

写数据流程(应用主动写入)

上层调用 SetWrite_Data → MonitorManager.WriteData()
    → DriverClient.WritePort() → IOCTL_COMMKIT_WRITE_PORT
    → 驱动 HandleWritePort() → 构造 IRP 发给下层串口

3. 模块详细设计

3.1 用户态 DLL

3.1.1 导出接口(exports.cpp)

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        // 数据指针(只读,不应释放)
);

3.1.2 MonitorManager(核心管理器)

职责:协调 DriverClient 与 EventLoop,提供 7 个导出函数的底层实现。

设计要点

  • 全局单例GetMonitorManager() 返回静态实例,导出函数通过它统一入口
  • 互斥保护:所有公开方法使用 std::mutex 保护,保证线程安全
  • 延迟初始化:不在 DllMain 中初始化,避免 Loader Lock 死锁
  • 幂等操作:重复 InitMonitor / Monitor 已有端口均视为成功

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 绑定串口失败

3.1.3 DriverClient(驱动通信封装)

职责:封装 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 字节占位)。

3.1.4 EventLoop(事件循环线程)

职责:独立工作线程循环从驱动读取事件,调用 TOnData 回调上报上层。

工作流程

while (running_) {
    // 阻塞式 IOCTL:驱动无事件时 IRP pending
    DeviceIoControl(IOCTL_COMMKIT_READ_EVENTS, batch, ...);

    // 处理返回事件
    for (event in batch) {
        callback_(event.Sequence, event.TimeStamp, ...);
    }
}

设计要点

  • 阻塞式读取:IOCTL 在驱动端 pending,有事件时才返回,避免空转
  • 批量读取:单次最多读取 COMMKIT_EVENT_BATCH(16) 条事件
  • 优雅停止Stop() 设置 running_=false,关闭驱动句柄使阻塞 IOCTL 返回错误,线程自然退出
  • 错误重试:非致命错误 sleep 50ms 后重试,避免空转

3.1.5 ErrorStore(线程局部错误码)

使用 thread_local int 存储,与 Win32 GetLastError() 语义一致:每线程独立,记录最近一次操作结果。

3.1.6 Logger(日志系统)

  • 输出方式OutputDebugStringW(DebugView 可捕获)+ 可选文件输出(%TEMP%\CommModifyKit.log
  • 级别:Debug / Info / Warning / Error,默认 Info
  • 格式[2026-07-21 10:00:00][INF] message
  • 独立实现:不依赖 CommModifyService 的 Logger

3.1.7 DllMain(DLL 入口)

关键约束:DllMain 在 Loader Lock 内执行,严禁:

  • LoadLibrary / LoadLibraryEx(递归加载死锁)
  • 复杂 STL 操作(std::threadstd::mutex 动态初始化)

因此 DllMain 仅执行 DisableThreadLibraryCalls,所有初始化延迟到首次导出函数调用。


3.2 内核态驱动

3.2.1 DriverEntry(KMDF 入口)

  • 初始化 WDF 驱动对象
  • 调用 CreateControlDevice() 创建控制设备 \\.\CommModifyKit
  • 注册 EvtDeviceAdd(PnP 过滤设备创建)和 EvtDriverUnload

EvtDeviceAdd 实现(核心):

  1. 调用 WdfFdoInitSetFilter() 标记为过滤设备
  2. 注册 EvtDevicePrepareHardware 回调(解析 COM 编号)
  3. 调用 WdfDeviceInitAssignWdmIrpPreprocessCallback 注册 IRP_MJ_READ/WRITE/DEVICE_CONTROL 预处理回调
  4. 调用 WdfDeviceCreate() 创建 WDF 过滤设备
  5. 初始化 DEVICE_CONTEXT(ComNumber=0,由 PrepareHardware 解析)
  6. 调用 RegisterFilterDevice() 注册到全局端口表

EvtDevicePrepareHardware 实现

  • DevicePropertyFriendlyNameDevicePropertyDeviceDescription 解析 COM 编号
  • 支持多种格式:"COMx (port)"、"通信端口 (COMx)"、"Serial Port (COMx)"
  • 调用 UpdatePortComNumber() 更新端口表

3.2.2 ClientConnection(连接管理)

职责:维护 DLL 与驱动之间的连接状态和端口监控列表。

  • 端口表ports_table_[256],最多支持 256 个 COM 端口
  • 环形缓冲:每个端口首次 ATTACH 时分配 EventRingBuffer(非分页内存)
  • IOCTL 处理:HandleXxx 系列方法由 QueueCallback 调度
  • 同步KSPIN_LOCK 保护端口表(IRQL 安全)

HandleAttachPort 流程

  1. 查找端口上下文(通过 COM 编号在 ports_table_ 中查找)
  2. 若端口无环形缓冲,分配并初始化
  3. 推入 OP_OPEN 事件
  4. 设置 MonitoringEnabled = TRUE

端口上下文来源

  • 驱动作为 Upper Filters 自动加载到串口设备栈
  • EvtDeviceAdd 为每个串口创建过滤设备并注册到 ports_table_(ComNumber=0)
  • EvtDevicePrepareHardware 从设备属性解析 COM 编号并更新端口表

HandleReadEvents 流程

  1. 遍历所有已启用端口的环形缓冲
  2. 逐端口 PopBatch() 弹出事件到用户缓冲
  3. 返回总字节数

3.2.3 QueueCallback(IOCTL 分发)

基于 WDF I/O Queue 的 Sequential 派发模式,将 IOCTL 码路由到 ClientConnection 对应方法。

控制设备创建流程

WdfControlDeviceInitAllocate(SDDL_DEVOBJ_SYS_ONLY_ADM)  // 仅管理员可访问
→ WdfDeviceInitAssignName("\Device\CommModifyKit")
→ WdfDeviceCreate()
→ WdfDeviceCreateSymbolicLink("\DosDevices\CommModifyKit")
→ WdfIoQueueCreate(Sequential)
→ WdfControlFinishInitializing()

3.2.4 SerialFilter(串口 IRP 拦截)

职责:作为上层过滤驱动 attach 到串口设备栈,拦截读写 IRP。

KMDF WDM IRP 预处理模式: 通过 WdfDeviceInitAssignWdmIrpPreprocessCallbackEvtDeviceAdd 中注册。

预处理回调 行为
DispatchRead(WDFDEVICE, PIRP) 注册 OnReadComplete 完成例程 → WdfDeviceWdmDispatchPreprocessedIrp
DispatchWrite(WDFDEVICE, PIRP) 注册 OnWriteComplete 完成例程 → WdfDeviceWdmDispatchPreprocessedIrp
DispatchDeviceControl(WDFDEVICE, PIRP) 直接 WdfDeviceWdmDispatchPreprocessedIrp 透传

完成例程(OnReadComplete/OnWriteComplete):

  • 从 IRP 提取数据指针(MdlAddressSystemBuffer
  • IoStatus.Information 获取实际长度
  • 调用 EventRingBuffer.Push() 写入环形缓冲
  • 返回 STATUS_SUCCESS 继续完成 IRP

CaptureIrpData(IRP 完成后提取数据):

  • 从 IRP 的 MdlAddressSystemBuffer 获取数据指针
  • IoStatus.Information 获取实际数据长度
  • 调用 EventRingBuffer.Push() 写入环形缓冲

3.2.5 EventRingBuffer(环形缓冲区)

  • 容量COMMKIT_RING_CAPACITY = 256 条/端口
  • 同步KSPIN_LOCK(IRQL <= DISPATCH_LEVEL 安全)
  • 溢出策略:满了覆盖最旧事件(tail 跟随 head),不阻塞 IRP 完成例程
  • 内存:非分页池分配(ExAllocatePool2 + POOL_FLAG_NON_PAGED
  • 批量弹出PopBatch() 一次弹出多条,减少锁竞争

3.2.6 DeviceContext(设备上下文)

每个串口过滤设备关联的上下文结构:

字段 类型 说明
ComNumber ULONG COM 编号(0=未知)
MonitoringEnabled BOOLEAN 是否已通过 ATTACH_PORT 启用
RingBuffer PVOID EventRingBuffer 实例指针
LowerDevice PDEVICE_OBJECT 下层真实串口设备
WdfDevice WDFDEVICE WDF 设备句柄

3.3 共享定义(common/)

CommKitEvents.h — 事件类型常量

常量 含义
COMMKIT_OP_NONE 0 无事件
COMMKIT_OP_OPEN 1 串口打开/开始监控
COMMKIT_OP_READ 2 读取到数据
COMMKIT_OP_WRITE 3 写入数据
COMMKIT_OP_CLOSE 4 串口关闭/停止监控

CommKitIoctl.h — IOCTL 码与数据结构

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 每端口环形缓冲容量

4. 关键设计决策

4.1 Loader Lock 安全

DllMain 仅执行 DisableThreadLibraryCalls,所有初始化延迟到 InitMonitor 首次调用。PROCESS_DETACH 也不调用 FreeAll,由上层 FreeMonitor() 显式释放。

4.2 SEH 异常保护

WorkerThread(EventLoop::Run)使用 __try/__except 包裹,防止 SEH 异常导致线程静默崩溃和初始化超时。

4.3 超时处理使用 detach()

当 WorkerThread 超时未完成时,使用 detach() 而非 join(),避免主线程无限阻塞。

4.4 环形缓冲溢出策略

满了覆盖最旧事件而非阻塞 IRP 完成例程,保证串口 I/O 性能不受监控影响。

4.5 驱动服务常驻

FreeMonitor 不卸载驱动服务(不调用 DeleteService),驱动在系统生命周期内常驻,避免反复安装/卸载的开销和权限问题。

4.6 1 字节 pack 对齐

所有跨边界(内核态↔用户态,WOW64)传递的结构使用 #pragma pack(1),避免对齐差异导致数据损坏。


5. 编译与构建

5.1 用户态 DLL

  • 工具链:MSVC v142(VS2019)
  • 平台:Win32(x86)为主,x64 可选
  • 配置:Release / Debug
  • 输出build\Release\Win32\CommModifyKit.dll

5.2 内核态驱动

  • 工具链:WDK + MSVC(KMDF)
  • 平台:x64(内核驱动仅 64 位)
  • 输出build\Release\x64\CommModifyKit.sys

5.3 解决方案配置

项目 Win32 配置 x64 配置
CommModifyKit (DLL) Win32 x64
CommModifyKitDriver 映射到 x64 x64

6. 与 CommModifyService 的集成

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()                 // 全部释放