QA 工程师:日常做电信终端软件测试。
ITSHFBC 是 ITU 的高频(HF)传播预测软件套件,HF 通信、频率规划、链路预算都会用到。它是 90 年代的遗产:
我手上要汉化的是这几个:
C:\itshfbc\bin_win\Pointwin.exe 点对点链路预测
C:\itshfbc\bin_win\Areawin.exe 区域覆盖(VOAAREA)
C:\itshfbc\bin_win\Hfantwin.exe 天线分析
C:\itshfbc\bin_win\Si_win.exe 信号强度目标很明确:中文菜单 + 中文对话框 + 不崩。前面两个都还好,第三个是深渊。
翻译完第一次跑,看起来是好的。用着用着就弹窗:
Exception
Missing ']'
Unused arguments on winio call
0xC0000409 (STATUS_STACK_BUFFER_OVERRUN)关键特征是"间歇性" —— 同一个菜单项,走不同的数据路径崩、换个数据又不崩、连开两次可能结果不同。这种现象如果靠人工点点点验证,结论只能是"好像有点问题但说不上来",根本没法收敛。
我拿现成的 PE 分析工具扒了一下 ClearWin+ 的字符串处理逻辑,结论是这样:
ClearWin+ 把每个格式串的"隐藏长度"作为参数传给 `winio@` 的调用约定,跨串续行拼接完全依赖这个长度做边界判断。 汉化把英文串换成中文,字节数变了(UTF-8 更夸张,GBK 也会变),但传统汉化工具只改内容不改长度 —— 结果就是越界读,后面的内存被当成格式串继续解析,直到撞上某个非法字符才崩。
所以"越界读"是随机的,崩点也是随机的。这就是间歇性的来源。
我最初想的方案很朴素:把 4 个 EXE 的所有菜单、对话框、按钮手工点一遍,记录崩点。写了个 menu_test.py 自动化之后发现两个更麻烦的问题:
问题 1:全菜单遍历会互相干扰。 有的对话框会写状态文件到 C:\itshfbc\run\(performw.ini、icepacg.out 这类)。上一轮测试留下的状态会改变下一轮的程序行为路径,于是"同一个菜单项,上一轮崩、这一轮不崩"。这种情况下做单串二分定位,结论是不可复现的垃圾。
问题 2:肇事因素互相耦合。 崩的往往不是"某一个译文串有问题",而是"A 串和 B 串在特定交互序列下叠加才触发"。单独测 A 一切正常。
所以必须换策略。
核心思路:把定位单位从"单个串"换成"一组串"。
全菜单遍历 → 发现肇事菜单项 → 按对话框归组 → 排除整组 → 重建 → 复测该菜单项
→ 确认肇事 → 把偏移固化进 trans_exclusions.py → 全菜单遍历连跑两轮全 OK → 通过工具脚本一共九个,都放在 hanization\ 下:
| 脚本 | 作用 |
|---|---|
| menu_test.py | 菜单树枚举 + 逐项遍历(每项独立进程) |
| group_test.py | 分组排除定位 |
| menu_bisect.py | 菜单项触发测试 / 子集重建 |
| button_test.py | 按钮遍历 + rebuild()(从 .bak 重建) |
| diag_menu_crash.py | 抓 Exception 窗口里的具体运行时错误 |
| sweep_danger.py | 危险特征筛查回退 |
| build_argmap.py | 全量 LEA + push 引用图 |
| deep_patch.py | 深度长度补丁 |
| find_setup_culprit.py / find_printer_culprit.py | 对话框级 dump + 与 .bak 逐偏移 diff |遍历每项都走独立进程:启动 → 点 Accept → 发 WM_COMMAND(0x0111) + 菜单 ID → 监测 Exception 窗口 / 进程退出。按钮用 BM_CLICK(0x00F5)。
两个识别到的高危结构,供参考:
定位到肇事偏移只是止损。真正的解法是让翻译后的串"长度自洽"。
FTN95 编译出的 winio@ 调用,每个格式串参数是这么传进内核的:
; LEA 指向 .data 里的格式串
8D 05 xx xx xx xx lea eax, [offset .data]
68 20 00 00 00 push 20h ; ← 隐藏的逻辑长度那个 `push` 后的立即数,就是我一直在找的"隐藏长度"。 它就明明白白躺在 .text 段里。
所以传统做法(缩短内容 + \x00 填充到原长)是错的——填充不解决长度语义问题。正确做法:
build_argmap.py 做第一步,deep_patch.py 做第三步。以 Areawin 为例,基础重建之后一共 23 条深度补丁。
这几条都是踩出来的,每条都对应一次失败:
Defender 会给你制造假阳性。 EXE 重写后首次启动可能被杀毒软件扫描拖慢,NO-STARTDLG 这个判据会误判成"启动即崩"。首跑结果异常时先复测再下结论,这一条救了我至少三次误判。
状态文件会污染测试。 C:\itshfbc\run\ 下的 performw.ini、icepacg.out 会改变程序行为路径。对比测试前要么清理,要么明确保持两侧一致。
多实例并行会互相 taskkill。 同时跑两个测试实例,A 实例结束时 taskkill 把 B 实例的窗口干掉,B 被判为"崩溃"。一次只跑一个实例。
tasklist 输出是 GBK。 Python 里必须 decode('gbk'),否则 UnicodeDecodeError,而且报错位置离真正的原因十万八千里。
引擎不能裸跑。 调用方式是 <模型>w.exe <输入>.dat <输出>.out,且 cwd 必须是 run 目录。裸跑会挂起等回车。
Areawin 的菜单对消息注入免疫。 这一点很反直觉 —— 同一套 ClearWin+,Pointwin 接受 `WM_COMMAND`(运行→图形 = 4008,有效),Areawin 的 Run 菜单完全不接受:WM_COMMAND、坐标点击、前台激活全部无效。注入事件不满足它回调的内部条件,只能改用真实键盘菜单导航。我的教训是:不要把一个程序的自动化方案套用到另一个程序上。
区域覆盖验证不用等出图。 计算 + 显示要 10~20 分钟。判断标准是"无 Exception + voacapx.out 持续增长",够了。
汉化验证过程中顺手把点对点绘图也自动化了,现在是一个能发给同事用的 HTML 界面工具(零第三方依赖,18.3 MB 压缩包):
# 快速调用:JSON 注入 → 驱动 Pointwin → 出图
python -P pointwin_plot.py guangzhou_beijing.json plots/gz_bj_SNR.png --w 900
# 多图型一次跑完(引擎只算一次)
python -P pointwin_plot.py params.json --jobs jobs.json实测耗时:单图 12~13 秒(替代了原来 19 秒的抓屏方案);多图型首张 13 秒,后续每张约 5 秒(走绘图窗 Parameters 菜单重绘,不重跑 VOACAP)—— 6 张图从 78 秒降到约 40 秒。
下面是自动化工具出的两张成品图。第一张是广州→北京 24 小时 SNR 中位值(CCIR 系数、SSN=76),第二张是青岛→上海的最适用频率 MUFday。


MUF 那条蓝线(FOT)穿过整个彩色信噪比区的地方,就是最佳工作频率窗口——这条曲线是整个 VOACAP 的核心输出,也是我花时间最多的地方:让这玩意儿能被脚本驱动,且出图不带任何 DPI 变形和底部裁切。
# ❌ 单轮子串匹配:'REL' 会先命中 '...reliab...'(RPWRG 行的描述文字)
# ✅ 先按 ^\s([A-Za-z]+)\s= 精确比对,再退化到子串匹配这个 bug 的表现特别坑:"我要 REL 结果出了 RPWRG",日志里还显示操作成功。
另外,Pointwin 的参数列表框实测只有 20 项:
TANGLE DELAY VHITE MUFday LOSS DBU SDBW NDBW SNR RPWRG
REL MPROB SPRB SIGLW SIGUP SNRLW SNRUP TGAIN RGAIN SNRxx没有 `RANGLE`(辐射角只有 TANGLE)。我前端清单里多写了一个 RANGLE,结果全选时在第 11 张整批失败。"Time availability" 是 REL 的别名,会重复出图,也不要放。
系统 125% 缩放(1920×1080,DPI 120)时,用 PrintWindow 抓窗口得到的永远是放大 1.25 倍后被裁掉的局部 —— 右侧图例截断、底部 NTIA/ITS 落款全丢。更阴的是"底部墨迹自检"会误判通过(底部恰好是空白区)。
判定手法:量网格线间距。正常约 63 px,变形时约 77 px(= 63 × 1.25)。
正解是让程序自己导出:绘图窗口菜单 to Clipboard(命令 id 动态查找含 "clipboard" 的项),从剪贴板取回 DIB 存 PNG。这个导出与窗口大小、DPI 缩放完全无关 —— 实测窗口 850 / 1000 / 1400 宽,导出恒为 830×630,完整含底部轴和落款,无变形无裁切。
不要 `SendMessage`,只发 `PostMessage`。 SendMessage 同步等待,程序正忙时消息泵被堵住,命令根本不执行(实测 6 秒内无 DIB)。另外不要抢前台 —— 实测 SetForegroundWindow 成功(绘图窗变前台)时反而写不进剪贴板。每次触发前先 EmptyClipboard(),否则会取到上一张的残留。
这个是整个过程最耗时间的 bug,症状是"剪贴板明明有 DIB 却读不出来",而且静默返回 None,不报错:
user32 = ctypes.windll.user32
k32 = ctypes.windll.kernel32
# ❌ ctypes 默认 restype=c_int,会把 64 位句柄截断成 32 位
# 随后 GlobalLock 拿到空指针/垃圾指针
# ✅ 必须显式声明:
user32.GetClipboardData.restype = ctypes.c_void_p
user32.GetClipboardData.argtypes = [ctypes.c_uint]
k32.GlobalLock.restype = ctypes.c_void_p
k32.GlobalLock.argtypes = [ctypes.c_void_p]
k32.GlobalSize.restype = ctypes.c_size_t诊断手法(很有效,值得记住):枚举 EnumClipboardFormats 并逐个打印 GlobalSize 和头 32 字节,CF_DIB(8) / CF_DIBV5(17) 的真实尺寸立刻可见。本机实测 DIB 头 28000000 e3030000 f4020000 01002000 03000000 → 40 字节头 / 995×756 / 32bpp / BI_BITFIELDS。
顺带两个小坑:不要在已 `OpenClipboard` 的状态下再调 `OpenClipboard`(嵌套打开失败,封装成"自己开剪贴板"的函数后会得到空格式列表 → 又是静默 None);EnumClipboardFormats 本身不需要先 OpenClipboard。
Fortran 引擎把根目录读进约 47 字符的固定缓冲区,而且拼子路径不加分隔符。根目录 48 字符就会截掉尾字符,报错长这样:
In READLIST, could not open=...\ITSHFBDATABASE\METHOD.DAT
^^^^^^^^^^^^^^^^^^^^^^注意 itshfbc 和 database 之间没有分隔符,还少了末尾的 c —— 这就是"截断 + 不加分隔符"的双重特征。症状是启动阶段主窗口根本不出现,引擎卡在 Pause 等回车。
含空格也是死:D:\cp test_itshfbc 失败,D:\CPTEST~1 正常。症状是进程在,但"指定HF传播模型"对话框永不出现。
统一解法:无条件转 8.3 短路径,一次解决超长和空格两个问题:
import ctypes
buf = ctypes.create_unicode_buffer(260)
ctypes.windll.kernel32.GetShortPathNameW(
r'D:\code\openITS\dist\PointwinGuiPortable\itshfbc', buf, 260)
# → D:\CODE~1\DIST\POINTW~1\itshfbc中文路径本身没问题。
父进程 Popen(..., -u, env={PYTHONUNBUFFERED=1}),子进程里却写了这么一行:
# ❌ 顶层无参数重包 stdout
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
# ✅
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8',
errors='replace', line_buffering=True) # ← 关键新建 TextIOWrapper 会把 `-u` 设好的无缓冲状态丢掉,退回 8KB 块缓冲。
症状极具迷惑性:服务正常、SSE 正常、前端正常,但任务运行期间页面完全不动(进度条不走、日志空白),等子进程退出才一次性把 20 条日志全刷出来。实证:修复前 20 条 plot_done 事件的时间戳全挤在 10 ms 内,而 job_done.elapsed = 109 s。
验收判据(一定要做,别只看"日志出来了"):
python -P -u pointwin_plot.py params.json \
| python -c "import sys,time; t=time.time(); [print('%6.2f %s'%(time.time()-t, l.rstrip())) for l in sys.stdin]"正常应该 0.00 → 2.13 → 8.16 → 12.60 → … 逐步推进。全挤在同一个时刻就是缓冲没打开。
另一条更省事的:拉 /api/log,看 plot_done 的时间跨度应该 ≈ 任务总耗时;若 [1/20] 与 [20/20] 相差 < 0.1 s,直接确诊。
还有一个通用建议:调试 SSE 页面时别用 `--virtual-time-budget`,连接永不空闲会挂死 Chrome。用 --timeout=N 或 --dump-dom。
前面每一条坑,修完之后如果不固化,三个月后重踩一遍只是时间问题。所以我把整套流程和踩坑记录沉淀成了 Skill:
its-win32-ui-hanize-test 汉化质量验证全流程 + 长度补丁法
pointwin-plot 点对点自动绘图 + 14 条实测踩坑
voaarea-plot 区域覆盖绘图(自包含,可整目录拷到别的 PC)
pointwin-gui HTML 界面版 + 13 条开发调试踩坑之后的做法变成:踩坑 → 记进 Skill → 下次任务开始时 Skill 被自动加载 → 不再重复。
voaarea-plot 那一版是自包含的(脚本 + 建站模板都在里面),拷贝整个文件夹到目标机就能跑,目标机只要装好 ITSHFBC 套件 + Python 3.8 以上即可。已在异机路径、中文带空格路径下验证通过。
这七条不是理论,是踩出来的。前面八节里的每个坑,基本都能对应到其中一条。
汉化本身是体力活,难的是把"人肉点点点"变成"可复现的自动遍历"这件事。而这件事最省力的路径,就是让 AI Agent 来写这些笨脚本、并且把每一次失败的诊断过程都吃进去 —— 它不会嫌你的二进制反汇编文档枯燥,也不会在第三次遇到同样的 ctypes 句柄截断时重新困惑一遍。
AI Agent 在这类"有明确验证标准 + 可以放心大胆试错"的任务上,产出效率是真的高。但在需要"我不确定这个判断对不对"的地方,还是要自己拍板。
欢迎交流。ITSHFBC 系列其他 EXE(Si_win / Hfantwin / Worldwin)的汉化还在推进中,如果你手上也有类似"老 Fortran + 私有 GUI 框架"的活儿,评论区聊聊你们的处理思路。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。