ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

遗留VB工程维护实战:反编译、SendInput与MSComm串口解析

遗留VB工程维护实战:反编译、SendInput与MSComm串口解析 如果把“VB标准”当成一场单纯的能力比拼很容易陷入版本争论。真正接手过 Visual Basic 老项目的程序员会知道VB 生态里最磨人的不是语法而是一堆长期存在的“工程边界”进程边界、窗口边界、输入边界、COM 控件边界。我去年维护一台老设备上位机时最耗时的不是把代码改通而是让一个在 Windows 7 时代写好的 VB6 程序在 Windows 10 上稳定运行要和串口设备按帧收发数据要定位另一个老进程的窗口标题要模拟键盘输入后不丢按键还要在一个没有源码、只有 exe 的界面上猜测内部逻辑。这几个问题恰好对应了经常被搜到的几组词VB 模拟键盘输入 SendInput、VB 应用 MSComm 控件实例、VB Decompiler、VB UI 控件、VB 调用 EnumWindows。这篇文章要说的“VB标准”不是某个神秘评级体系而是老 VB 工程维护里反复出现的一套实用判断标准先看清你能动什么再决定怎么动。尤其在没有源码、控件缺失、Windows 版本迁移这几类场景里这套标准比很多新框架的技巧更值钱。1. 拿到遗留 VB 程序先从工程还原和授权边界开始1.1 为什么“反编译”是维护链条的第一环很多 VB6 项目在交付后并不总是保留完整源码。常见情况是当年开发的人走了安装包还在能跑但frm、bas、vbp文件不知去向或者从中途某个版本开始只有exe被人反复复制源码早已和旧电脑一起消失。这时候直接看反编译结果几乎是必经之路。VB6 程序在编译时有两种常见输出形式Native Code 和 P-Code。前者更接近机器指令后者是一种中间字节码。不同编译方式对反编译工具有直接差异。有一类工具专门针对 VB 程序做逆向还原典型名字里会带 Decompiler网上也常看到 VB Decompiler、VB Decompiler Pro 这类名称。它们能做的事情大致包括列出窗体、模块、类模块定位事件过程还原字符串引用甚至在 P-Code 模式下恢复出和源码非常接近的伪代码。但这里必须先强调边界反编译只能用于自己拥有源码权限的旧项目或者用于获得授权的维护场景。把它当成“破解别人软件”的手段既不合适也超出了本文要讨论的技术范围。1.2 反编译能还原什么不能还原什么在实际使用中反编译的结果更像是一张“施工图纸”而不是原始源代码。它能告诉你事件流程、按钮名称、控件属性、字符串常量去了哪里但不能完整还原你已经丢掉的所有变量命名和注释。在开始反编译前可以先建立一个预期表格问题常见情况窗体结构能还原出多数控件和布局信息但图形资源不一定能完美导出事件过程按钮点击、窗体加载等事件过程通常能定位字符串引用可以顺着字符串反查代码位置是快速理解业务的关键变量名和注释通常不可恢复还原后可能要按业务语义重新命名Native Code 程序接近汇编级阅读成本比 P-Code 高不少所以正确姿态不是指望“一键恢复源码”而是把反编译当成业务盘点工具。先找出有哪些窗体、哪些按钮、哪些字符串再慢慢拼出程序的大致脉络。1.3 先用“字符串索引 事件定位”搭业务清单拿到一个没有源码的 VB exe我一般不会马上去读反编译细节而是先做一轮“黑盒白盒”混合操作先运行程序记录界面上出现的所有按钮、菜单和提示文案。用反编译工具搜索这些提示文案。找到每个字符串在哪个事件过程中被引用。对照触发按钮和输入框形成一张“界面动作 → 事件过程 → 底层 API/控件”的映射表。在这个映射表基础上再判断能不能做兼容性修改还是必须重写。这个过程有一个额外好处它会逼着你先理解业务闭环。比如一个串口下位机程序往往不是发一条命令就完事而是发送后要等应答、解析帧、超时重试。如果只盯着控件事件很容易错过真正的核心逻辑。建议把反编译出来的工程还原结果放在虚拟机或隔离目录里做二次分析避免误改重要生产环境。2. 用 SendInput 模拟键盘输入胜在“真实按键”而不是“发消息”2.1 模拟键盘输入不是只有 SendKeys 一种选择老 VB 项目经常需要模拟键盘操作。典型场景是一个旧程序不提供命令行参数只能靠人工点击按钮、输入内容另一个新系统要自动调用它但又不方便改它的源码于是程序只能通过模拟键盘来驱动。很多新手第一反应是用SendKeys。这不是不行但问题很多它容易受窗口焦点影响运行速度不稳定在输入法状态不同时结果也不一样。对稍复杂的界面尤其是目标窗口标题经常变化的情况下SendKeys往往会莫名其妙失败。相比之下Windows API 层的能力更可靠一些。常见几个方案是方案特点适合场景SendKeys写法简单但容易受焦点和时序影响极简单的界面自动化keybd_event老 API能模拟按下和释放低版本兼容场景中的单键操作SendInput较新的输入模拟 API能模拟更真实、支持 Unicode需要稳定输入的自动化流程SendMessage / PostMessage直接向窗口发消息不是真正键盘队列已知目标控件句柄时的文本写入这里要特别说明一点如果只是想给某个输入框设置一个值用 SendMessage 发送WM_SETTEXT成本最低。但目标程序如果是靠键盘事件来触发业务逻辑的比如扫码枪输入、按键热键、组合键菜单那SendMessage就不一定有用因为它的行为不完全等同于用户真实按键。这就是 SendInput 的核心价值它把输入事件投递到系统输入队列目标程序看起来就像用户在真实键盘上按了一次键。2.2 最小调用流程与常见写法在 VB6 里声明 SendInput 会比在 C# 里麻烦一些因为它涉及结构体而 VB6 处理联合类型并不方便。经常看到的方法是用 Type 定义INPUT和KEYBDINPUT再借助CallWindowProc或内存拷贝绕开联合限制。这里不直接堆一个超长声明先看最小流程 示意代码仅体现虚拟键和扫描码思路不用于直接编译 1. 先找到目标窗口并把它放到前台 2. 获取窗口句柄后调用 SetForegroundWindow 3. 按下某个键 4. 松开某个键如果你只是临时验证用老接口 keybd_event 也能跑Private Declare Sub keybd_event Lib user32 (ByVal bVk As Byte, ByVal bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long) Private Sub SendKey(vKey As Integer) keybd_event vKey, 0, 0, 0 keybd_event vKey, 0, 2, 0 End Sub这段代码的核心是“按下 释放”。虚拟键码代表逻辑键扫描码代表物理键位。大多数简单按键场景只需要传入虚拟键码但如果你要模拟组合键或者处理某些特殊程序扫描码也很关键。需要说明的是keybd_event 属于旧接口遇到带管理员权限、UAC 弹窗、UIPI 隔离这类场景时会受限。生产环境如果追求稳定仍然应该优先使用 SendInput只是 VB6 里的声明要花更多功夫。2.3 最容易翻车的 4 个坑模拟键盘输入看起来简单真正稳定跑起来却很难主要坑集中在四层第一前台焦点。目标窗口如果没有激活输入会跑到当前前台窗口里。不能只靠AppActivate字符串匹配窗口标题因为标题可能重复也可能在启动过程中临时变化。更稳的流程是先FindWindow或EnumWindows找到句柄再SetForegroundWindow。第二权限隔离。普通权限程序不能直接向高权限窗口模拟输入这是 Windows 的用户界面隔离机制。自动化程序要以管理员身份运行时目标程序通常也要同一权限级别否则按键会无效。第三输入法。如果模拟的是普通 ASCII 字符很多时候会被输入法拦截。更稳的做法是模拟 Unicode 输入也就是 SendInput 里的KEYEVENTF_UNICODE标志。第四节奏。输入过快目标程序处理不过来会丢字符。不能只关注“发出去了”还要关注“目标程序收到了”。常见的补偿方式是每次按键之间加一点延时或者定时检查界面状态确认触发成功。仿真输入最有价值的不是快而是可复现。每次输入前先确认窗口存在输入后再验证结果比盲目加快速度靠谱得多。3. MSComm 控件实例串口不是发字符串要按照通信协议处理3.1 控件配置有一套“实用套路”VB 老项目里MSComm 控件几乎是串口上位机的标配。它的全称是 Microsoft Communications Control常见文件是 mscomm32.ocx。虽然现在很多人转向 .NET 里的System.IO.Ports.SerialPort但存量设备里还有大量基于 MSComm 的上位机程序。一个典型的 MSComm 配置流程类似这样MSComm1.CommPort 3 MSComm1.Settings 9600,N,8,1 MSComm1.InputMode 1 1 表示二进制方式 MSComm1.RThreshold 1 缓冲区收到 1 个字节就触发 OnComm MSComm1.InputLen 0 读取时取走整个接收缓冲 MSComm1.PortOpen True接收事件里通常会这样写Private Sub MSComm1_OnComm() Select Case MSComm1.CommEvent Case 2 comEvReceive Dim buf() As Byte buf MSComm1.Input 将数据交给缓冲区处理函数 Call ProcessReceivedBytes(buf) End Select End Sub这里有一点经常被忽视InputLen默认不是 0。如果以前有人设过InputLen 1那么每次调用MSComm1.Input只能读一个字节容易造成接收逻辑混乱。3.2 为什么不能只看到字符要看到“帧”不少初学者把一个串口程序调通后会极度兴奋因为 OnComm 事件确实触发了Input也确实读到了数据。但很快会发现数据长度经常不完整或者下次重发后内容错位。原因很简单串口是流式传输没有“消息边界”。设备端发送的是一段字节流但电脑端在哪一次触发 OnComm、每次收到多少字节完全取决于串口缓冲和系统调度。正确做法不是把接收功能写成一个点而是写成一个“攒数据 找帧头 按长度提取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进