用 REA 拆了一个老游戏的图形引擎:真实感受、能与不能、以及我希望它改进的地方
背景:我在复刻一个 2016 年的老回合制游戏(《回合制Ⅱ》)的战斗系统。图形引擎是闭源 DLL,
没有文档、没有源码。我用 REA(Reverse Engineer Anything,基于 Ghidra 12.1.4 headless 的
编排层)把它拆开,拿到了动画模型、渲染管线、精灵方向约定这些硬结论。
这篇文章不讲怎么安装,只讲真实用下来的体验:它能干什么、干不了什么、为什么、怎么改进。
一、我的工作流(先交代清楚,后面才有讨论基础)
目标二进制
→ 判断壳(DIE/查壳工具 + REA analyze 的函数数对比)
→ 有壳先脱(UPX 用 upx -d,脱完 263KB → 708KB)
→ rea analyze(生成快照)
→ rea search / search-bytes(符号、字节模式定位)
→ rea decompile --procedure(逐个函数拉伪代码)
→ 伪代码结论 → 回到自己的项目里落地 → 用行为对照验证
REA 本体是个 Node 编排脚本(rea.mjs + Ghidra headless),分析结果落快照,
后续 search/decompile 都吃快照,不用每次重启 Ghidra------这个设计是对的。
二、它能做的(每条都有实战案例)
1. 带符号的模块,效率高得惊人
图形库 GfxLib_x.dll 导出表保留得很好:Spr_SetFlip、Ani_SetSpeed_e、GetPosition_e
......直接 search 符号 → decompile,一晚上把整个动画系统还原了:
- 动画是统一帧率,不是逐帧时长 。
Ani_SetSpeed的伪代码就三行:
interval = 1.0 / speed。SP 文件头里那张逐帧时长表引擎根本没读。
这直接推翻了我之前"看图说话"得出的"时长表基本匀速是巧合"的猜测------不是巧合,是引擎只支持匀速。 - 帧 = 图集 UV 窗口 (
Ani_SetFrame:frame_index × 格宽 → 纹理偏移),
所以"逐帧 PNG"和"图集 + 移动 UV"是等价实现,选哪个都对。 - 播放模式位 (bit0 倒放、结束行为 2/6/7 是 ping-pong)------原游戏那些"呼吸感"特效
的实现方式直接抄作业。
没有 REA,这些结论我要靠帧间差分猜测很久,而且永远不敢说"确定"。
2. 否定证明:search-bytes 是隐藏王牌
最值钱的一次使用不是"找到了什么",而是证明了"不存在"。
问题:精灵对象的 +0x6c 字段存了混合模式,绘制时怎么消费?
反编译 Spr_SetBlendMode 只看到写。我在脱壳后的整个 .text 段做字节级搜索
(8B??6C、FF??6C、83??6C、8B????6C000000 四种编码),返回 complete: true、零命中。
结论:这个字段在本模块内只写不读,消费方在模块外。
"零命中 + 扫描完整性确认"是一个可以写进研究笔记的硬结论------这比找到十个函数都有用。
后来我们的发光特效用"帧画颜色梯度 + lighter 混合"实现,实测观感等价,正是因为
引擎级混合模式本来就不是原游戏发光感的来源。
3. 反编译 + 自写解码器交叉验证
我把 SP 精灵文件自写解析器解码成图片,统计出"4 方向 4931 个、8 方向 1292 个",
再对照反编译出来的 Set_fDirection_e(对象 +0x14 方向字段)------
两条独立证据链指向同一结论:方向画在素材里,不是运行时镜像 。
原游戏"我方朝左、敌方朝右"是取对应方向的帧,不是 scaleX(-1)。
单靠任何一条证据我都只敢写"大概率";交叉之后我直接把项目里错误的统一镜像改掉了。
4. 无壳模块直接全量吃
XYRes.dll 没壳,2773 个函数直接 analyze,资源加载逻辑随便查。
对这类目标,REA 基本是"半自动源码阅读器"。
三、它不能做的(以及为什么)
1. 强壳主程序:直接放弃
mtxy2.exe 是强壳(VMP 类),analyze 出来只有 48 个函数、160 条字符串------
代码没有暴露,Ghidra 没有奇迹。REA 不会脱壳,壳是它的天花板 。
游戏逻辑(伤害公式、站位坐标)恰恰在这个 exe 引用的 Lua 字节码里,
我又扫了 GfxLib/XYRes/资源包,Lua 签名零命中------脚本载体被强壳包着,链条在这里断掉。
教训:开工前先看壳 。有壳先脱壳(UPX 秒脱;强壳要么放弃要么上动态分析),
别让 REA 硬啃,纯烧时间。
2. 跨模块数据流:断链就断死
+0x6c 案例的完整答案在宿主 exe 的设备层,而宿主强壳。
REA 的搜索和反编译都是单模块视角 ,它不会(也不能)帮你跨模块追一个字段的生命周期。
它能告诉你"本模块内只写不读"------这已经很有价值------但"读它的代码长什么样"要另外想办法。
3. 数据 vs 代码:二进制里没有的东西就是没有
战斗站位像素坐标存在 Lua 脚本里,不是编译进 DLL 的常量。
资源包(WDF/WD*)是自有格式数据文件,反编译器帮不上忙------那部分我靠自写解码器,
反而比 REA 快。先问"这个信息以什么形态存在",再选工具 :
代码形态找 REA,数据形态写解析器,形态不明先用十六进制编辑器看半小时。
4. 一些工程小坑
search-bytes不支持--snapshot参数,我带着参数白跑了好几轮才意识到。
报错不直观,命令行参数在子命令间不一致。- 反编译伪代码里大量
FUN_1003fc40式匿名函数,交叉引用要自己一层层点进去,
遇到虚表调用(设备抽象层)基本只能靠猜测注释。 - analyze 大 DLL 要等(7xxKB 脱壳后 2500+ 函数,几分钟级),快照复用是必须的,
但快照和目标文件的绑定关系没有提示,换了个脱壳版本差点对错快照。
四、如果让我给 REA 提改进建议
- 壳检测前置 :analyze 时先报"疑似加壳(函数数/字符串数异常低),建议先脱壳",
能省掉新手最多浪费的时间。 - 命令参数统一 :search 系列子命令的参数集应该一致,
--snapshot要么全支持要么全不用,
报错时给出"你是不是想用 X"的提示。 - 跨模块引用图 :哪怕只是"该字符串/符号在其他已分析快照中出现"的提示,
也能把断链的手工排查自动化一半。 - 伪代码可读性 :导出符号自动回填到调用点(现在只有部分)、虚表调用标注
"疑似设备接口",对逆向图形库这种虚表密集型目标帮助巨大。 - 结论导出 :跑完一轮研究,希望一键导出"分析过的符号 + 摘要 + 伪代码"成 markdown,
我现在是手工摘的。
五、总结:REA 的真实定位
一句话:REA 是"机制还原器",不是"万能破解机"。
- 它擅长:有壳外的代码、带符号或可定位的函数、以代码形态存在的信息。
在这些场景里,它把我从"猜"带进了"证"的世界------统一帧率、UV 图集、方向约定、
混合模式只写不读,每条都是伪代码级别的证据。 - 它不擅长(也不是它的错):强壳、跨模块断链、以数据/脚本形态存在的信息。
工具的边界就是二进制的边界:信息不在你分析的这段二进制里,再好的编排层也变不出来。
我的收获不是"逆向出了多少函数",而是学会了一套求证纪律 :
先看壳、先问信息形态、代码找 REA、数据写解码器、两条证据链交叉、
否定结论也要扫满 .text 才敢下。这套纪律比任何单次反编译结果都值钱。
复刻项目的后续:动画模型、特效三段式结构、精灵方向全部按实证结论重写,
混合模式问题用等效方案收口,布局像素坐标因脚本不可得改为实测截图对照。
工具有边界,但工程没有------边界之外的部分,用笨办法补齐就行。