软件如何“伪造”一个串口?深入解析 Windows 虚拟串口驱动的工作机制
你有没有遇到过这种情况:写好了一个串口通信程序,却因为没有真实的 GPS 模块或 PLC 设备而无法测试?又或者你的工控机只有两个物理 COM 口,但项目需要连接十几个串行设备?
别急——虚拟串口驱动(Virtual Serial Port Driver)就是为此而生的。它不是魔法,也不是黑盒,而是一套精巧的 Windows 内核级软件工程实践。今天我们就来揭开它的面纱,看看它是如何在系统中“无中生有”地创建出一个看起来和用起来都跟真的一模一样的 COM 端口。
为什么我们需要“假”串口?
尽管 USB、以太网甚至无线通信早已普及,但在工业控制、嵌入式调试、协议仿真等领域,RS-232 风格的串行通信仍然坚挺。原因很简单:简单、稳定、兼容性极强。
但问题也随之而来:
- 物理串口数量有限(主板通常只提供 1~2 个)
- 增加硬件成本高且不灵活
- 开发阶段频繁插拔设备效率低下
- 很难监控中间数据流
这时候,virtual serial port driver的价值就凸显出来了。它通过纯软件方式模拟完整的串口行为,让应用程序根本“意识不到”自己连的是个“影子端口”。
更重要的是:不需要修改任何上位机代码。只要你的程序调用了CreateFile("COM3", ...),就能无缝接入虚拟世界。
它到底做了什么?三个核心步骤讲明白
我们可以把虚拟串口驱动的工作流程拆解为三个关键动作:造设备、建通道、转数据。下面一步步来看它是怎么骗过操作系统的。
第一步:我宣布,这个设备存在!
要让 Windows 认可一个新的 COM 端口,光有个名字是不够的。必须走正规流程——注册为一个合法的即插即用(PnP)设备。
当驱动加载时(比如vspd.sys),它会做这几件事:
在内核中创建一个设备对象:
\Device\VSPD_COM3绑定一个用户态可见的符号链接:
\DosDevices\COM3 → \Device\VSPD_COM3向 PnP 管理器报告:“嘿,我发现了一个新设备!”
即使这个设备根本没有对应的芯片、引脚或电平信号。
这一步的关键在于使用WDM 或 WDF 框架 API来构造标准设备栈。例如,在DriverEntry()中调用:
IoCreateDevice( DriverObject, sizeof(DEVICE_EXTENSION), &deviceName, // \Device\VSPD_COM3 FILE_DEVICE_SERIAL_PORT, 0, FALSE, &deviceObject );随后还要注册 IRP 分发函数,处理后续所有来自应用程序的读写请求。
一旦完成,你在设备管理器里就能看到 “COM3” 出现了——虽然它背后啥也没有。
✅ 提示:这就是为什么有些虚拟串口工具安装后需要重启。因为它要向系统声明新设备的存在,而某些资源分配只能在启动时完成。
第二步:让两个“空气端口”互相通信
最经典的用法是什么?创建一对互连的虚拟串口,比如 COM3 ↔ COM4。
想象一下,你有两个程序:
- A 程序打开 COM3 发送数据
- B 程序从 COM4 接收数据
理想情况下,A 写进去的东西,B 应该立刻能读出来。就像用一根虚拟的串口线把它们连了起来。
那这个“连线”是怎么实现的?
核心机制:IRP 重定向 + 缓冲队列
每个读写操作在内核中都被封装成一个I/O Request Packet(IRP)。驱动的任务就是拦截这些 IRP,并决定如何处理。
举个例子:
- 程序 A 调用
WriteFile(hCom3, "HELLO", 5) - 系统生成
IRP_MJ_WRITE,交给 VSPD 驱动处理 - 驱动一看:“哦,这是发往 COM3 的”
- 查表发现 COM3 配对的是 COM4
- 把
"HELLO"存入 COM4 的接收缓冲区 - 如果此时有程序正在
ReadFile(COM4)等待数据,立即唤醒它并返回
反向也一样成立。这种双向透传的设计,本质上是一个内存中的数据管道,只不过披上了串口的外衣。
数据结构示意(简化版)
typedef struct _VSPD_PORT { LIST_ENTRY ReadQueue; // 待读取的数据包链表 KSPIN_LOCK QueueLock; // 多线程访问保护 BOOLEAN IsOpen; // 是否已被打开 struct _VSPD_PORT *PairedPort; // 配对端口指针 } VSPD_PORT, *PVSPD_PORT;每次写入都加锁操作队列,确保线程安全;每次读取尝试取头节点,若为空则挂起等待。
第三步:假装支持波特率、校验位……其实全都不管用
这是最容易被误解的一点:虚拟串口根本不传输电信号,所以像 9600 波特率、奇偶校验、RTS/CTS 流控这些,在物理层毫无意义。
但!应用程序还是会去查:
DCB dcb; GetCommState(hCom, &dcb); // 查询当前串口设置 printf("Baud: %d\n", dcb.BaudRate); // 输出可能是 115200如果你的驱动不响应这些请求,程序可能会报错或拒绝运行。
所以优秀的虚拟串口驱动必须做到一件事:演得足够像。
如何“演”?
- 实现所有标准串口 IOCTL 控制码:
IOCTL_SERIAL_SET_BAUD_RATEIOCTL_SERIAL_GET_COMMSTATUSIOCTL_SERIAL_SET_DTR/CLR_DTR……共数十种
在内部维护一份 DCB(Device Control Block)状态副本
- 收到
SetCommState就更新内存变量 - 收到
GetCommState就原样返回
至于这些设置会不会影响数据传输?当然不会。数据还是照样飞快地从一个缓冲区拷贝到另一个缓冲区,一秒几兆都不带卡的。
但程序满意了:“嗯,这确实是个正经串口。”
高手才知道的几个细节
你以为这就完了?真正的工程挑战才刚刚开始。
🛠️ IRP 必须妥善完成,否则系统会卡死
每一个进入驱动的 IRP,最终都必须被IoCompleteRequest()完结。哪怕你只是忽略它,也要给个状态码(如STATUS_SUCCESS或STATUS_INVALID_PARAMETER)。
漏掉这一步会发生什么?
→ 应用层的ReadFile或WriteFile永远不会返回!
→ 程序卡住,任务管理器杀都杀不死
→ 严重时可能导致系统无响应
这是很多初学者自己写驱动时踩的最大坑之一。
🔐 多进程并发访问怎么办?
设想多个程序同时读写同一个虚拟 COM 口。如果不加保护,缓冲区可能被撕裂、覆盖或读到乱码。
解决方案:使用自旋锁(Spin Lock)或快速互斥量(Fast Mutex)
KLOCK_QUEUE_HANDLE lock; KeAcquireInStackQueuedSpinLock(&port->QueueLock, &lock); // 安全操作接收队列 InsertTailList(&port->ReadQueue, &newPacket->ListEntry); KeReleaseInStackQueuedSpinLock(&lock);注意:不能用分页内存中的锁,也不能在 DPC 级别调用可能导致睡眠的操作。
💾 内存池选择也很讲究
驱动中分配的数据结构(如缓冲区、IRP 上下文)应优先使用非分页池(Non-paged Pool)
为什么?
因为串口驱动常在中断上下文或 DPC 中运行,而这些环境不允许发生页面调度。如果访问了会被换出的内存页,直接蓝屏(BSOD)。
buffer = ExAllocatePool(NonPagedPool, BUFFER_SIZE); if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;实际应用场景:不只是“用来测试”
很多人以为虚拟串口只是开发辅助工具。其实它的用途远比你想象的广泛。
场景一:串口数据抓包分析
你想看某台设备和上位机之间的通信内容,但又不想破坏原有连接。
方案:使用虚拟串口做“中间人代理”
[真实设备] ←→ [虚拟COM3] ⇄ [监控软件] ↖ 监听复制 [虚拟COM4] ←→ [原上位机软件]驱动可以在转发数据的同时,将每帧内容记录到日志文件,用于后期协议逆向或故障排查。
场景二:跨进程通信桥接
两个独立开发的应用程序,原本无法直接通信。但它们都能操作串口。
于是你可以:
- 创建一对虚拟串口
- A 程序向 COM5 写命令
- B 程序从 COM6 读取并执行
相当于用“伪硬件接口”实现了进程间通信(IPC),而且天然支持跨权限、跨会话。
场景三:云化串口设备
更进一步,可以把虚拟串口桥接到 TCP socket,实现“网络串口服务器”的效果。
本地程序 → 虚拟COM → 驱动 → TCP → 远程设备这样即使设备在千里之外,也能像本地串口一样访问。
自己能做一个吗?可以,但门槛不低
如果你想动手实现一个基础版本,技术路径很清晰:
- 使用WDK(Windows Driver Kit)搭建开发环境
- 编写基于KMDF(Kernel-Mode Driver Framework)的驱动
- 实现
EvtDeviceCreate,EvtIoWrite,EvtIoRead回调 - 维护配对逻辑与环形缓冲区
- 签名并通过测试模式加载
但要注意:
- 内核编程容错率极低,一个小错误就会导致蓝屏
- 必须通过WHQL 认证才能在未启用测试签名的机器上运行
- Windows 10/11 对驱动强制签名要求严格
因此大多数开发者会选择成熟的商业方案,如:
-Eltima Virtual Serial Port Driver
-HHD Software Serial Port Monitor
-com0com(开源免费)
尤其是 com0com,虽然是开源项目,但结构清晰,非常适合学习参考。
最后的小结:它不是“替代”,而是“延伸”
虚拟串口驱动的本质,是利用操作系统提供的抽象能力,扩展物理世界的边界。
它告诉我们:
即使没有硬件,也可以有接口;
即使没有电线,也可以有通信;
只要协议一致,真假又有何区别?
对于工程师而言,掌握这项技术的意义不仅在于提高调试效率,更在于理解Windows I/O 架构的核心思想——设备即文件,驱动即桥梁。
下次当你打开设备管理器看到一堆“凭空出现”的 COM 口时,你会知道:那不是幻觉,那是代码的艺术。
💬互动时间:你在项目中用过虚拟串口吗?是用来仿真设备、抓包分析,还是做进程通信?欢迎在评论区分享你的实战经验!