AI逆向GBA实录:找到重力,未解跳跃
作者使用开源REA MCP服务器与Claude分析一份无源码、无符号的GBA ROM,尝试恢复角色移动和跳跃参数。实验提取了部分物理参数并用于原创浏览器游戏,同时披露未能还原的跳跃逻辑。
AI 深度解读
把一个没有源码、符号和文档的 GBA ROM 交给 AI,能找回角色的跳跃手感吗?作者用 Claude Opus 5.5 配合 REA 和 Ghidra,找到了重力、移动速度等参数,却没找到起跳速度和最大下落速度。
这次实验值得看的,是它把「工具证据」「代码解释」和「自行调参」分开了。对于用 AI 读陌生代码的程序员,这比生成一个能跑的游戏更有参考价值:找到常量只是开始,证明它代表什么才是难点。
AI 能读懂 ROM,靠的是工具和证据
实验对象是 Aladdin Game.gba,大小为 3,987,712 字节。GBA 将卡带 ROM 映射到固定地址执行,它没有常见可执行文件里的导入表和函数名。直接把十六进制数据贴给模型,很容易得到听起来合理的猜测。
作者使用的 REA,全称 Reverse Engineer Anything,是一个开源 MCP server。它让 AI 能调用真正的逆向工具,执行加载二进制、反编译函数、读取字节、追踪调用者和交叉引用等操作。底层使用 Ghidra,这是 NSA 在 2019 年开源的逆向工具。
模型负责提出搜索方向,反编译器负责返回可检查的证据。
REA 每次工具调用都会返回 evidence_id、文件的 SHA-256、Ghidra 版本和限制说明。于是,「某函数写入了重力常量」可以追溯到具体文件和反编译结果。它分析的是临时副本;注释存在会话数据库里,关闭时默认删除,需要保留则请求 snapshot。
环境搭建反而更费时间。作者先升级 Node,随后发现 Homebrew 安装的 Ghidra 太新,与 REA 6.8.0 不兼容,只好另装 Ghidra 12.1.4。官方压缩包缺少 Apple Silicon 原生反编译器,Claude 从随包 C++ 源码构建,第一次生成 x86_64,第二次才改成 arm64。期间还处理了 JDK 升级,最终通过 rea doctor,注册到 Claude Code 并重启。
这里还有一个影响结论的细节:REA 尚未配置成功时,Claude 已用 Ghidra headless scripts 做过首轮分析。之后才通过 REA 重跑关键步骤。因此,后来的少量工具调用不能当作从零定位问题的成本。
没有函数名,就从行为反向找
REA 没有 GBA loader,加载时需要指定 format: "raw-binary"、architecture: "arm",并把 entry_point 设为 0x08000000,也就是卡带映射地址。
文件头显示标题为 ALADDIN、游戏代码为 BADP,末尾的 P 表示欧洲版本。作者据此识别为 2004 年 3 月在欧洲发行的 Capcom SNES 移植版。Ghidra 自动分析找到 1,732 个过程、448 个字符串;首轮分析还发现,代码几乎都是 Thumb,即紧凑的 16 位指令集,并调用 GBA BIOS 完成 LZ77 解压、除法和平方根计算。
定位输入有一个明确入口:0x04000130,即 KEYINPUT 寄存器。它采用低电平有效,按钮按下时对应位为 0。Claude 顺着引用找到 0x08014d9c,发现函数会取反输入,再重新映射十个按钮。
作者推测这是适配 SNES 按键布局的移植层,但没有把推测写成已证实的事实。后续游戏逻辑读取的是映射结果,继续找跳跃判断就必须沿这条路径走。
重力则靠另一种行为特征定位:寻找每帧把对象某个字段加到另一个字段上的函数。最终找到 0x08010024:
// 0x08010024, called once per frame for objects affected by gravity
void apply_gravity(GameObject *object) {
object->velocity_y += object->gravity; // +0x30 += byte at +0x34
}
关键是 velocity_y += gravity:它揭示了更新关系,但还没解释谁决定重力值。
结合首轮 headless 分析,对象的 X、Y 位置分别位于 +0x10、+0x14,水平速度在 +0x28,垂直速度在 +0x30,单字节重力在 +0x34。位置使用定点数,每单位相当于 1/256 像素。
REA 的 procedure_callers 找到十个调用者。首轮分析中的 0x0800EFA0 会在垂直速度越过 0x80 时切换上升、下降标记,作者据此将其视为跳跃顶点判断。
继续追踪写入,0x08018EE0 默认把重力设为 0x38,三个标志条件满足时改为 0x80。另一个函数 0x080191E0 将水平速度限制为 0x180,另一分支直接设为 0x200。
数字找到了,含义仍有空白。作者把相关标志解释为腾空、松开跳跃键和奔跑输入,但这些解释尚未得到运行时验证。
找回重力,不等于还原跳跃
按 1/256 像素换算,0x180 和 0x200 分别是每帧 1.5、2 像素。按 60 帧计算,就是每秒 90、120 像素,放在宽 240 像素的屏幕上,移动尺度已经比较清楚。
跳跃还缺两个关键值:起跳速度和最大下落速度。作者怀疑起跳速度来自数据表,但静态分析没有把表与使用位置连接起来。他建议接入 mGBA 一类模拟器调试器继续追踪,本次实验则停在静态分析。
作者随后用恢复的参数制作了原创游戏 Arena Run。它采用纯 JavaScript 和 Canvas 2D,以 GBA 原生的 240×160 分辨率绘制,再用最近邻方式放大。
这段配置最能体现结果的边界:恢复值和自选值都在代码里标明了来源。
// Every value is in subpixels (1/256 px) per frame, at 60 frames per second.
export const PhysicsTuning = Object.freeze({
walkSpeed: 0x180, // ROM: speed cap in the routine at 0x080191E0
runSpeed: 0x200, // ROM: run branch of the same routine
risingGravity: 0x38, // ROM: gravity byte (+0x34) set at 0x08018EE0
fallingGravity: 0x80, // ROM: written at 0x08018EE0 when three flags line up
apexThreshold: 0x80, // ROM: rising/falling flag at 0x0800EFA0
jumpVelocity: -0x480, // not found in the ROM, my choice
maxFallSpeed: 0x500, // not found in the ROM, my choice
});
要留意最后两行,以及 risingGravity、fallingGravity 的命名:前者是作者调参,后者把已经找到的常量赋予了尚待验证的玩法含义。
游戏使用整数子像素和固定 60 Hz 时间步,按累计时间推进完整逻辑帧,避免 144 Hz 显示器改变跳跃结果。不过 GBA 实际约为 59.73 Hz,这里仍有时间尺度差异。
演示也有明确说明:操作来自搜索输入序列的 bot,22.8 秒通关,拿到 38 枚金币中的 36 枚,保住全部生命。它证明关卡可以完成,不能证明原版手感已经复现。
我的判断
我认可的是证据管理和搜索方法。输入寄存器、字段累加、速度限幅,都比要求 AI「找出跳跃代码」更具体,也更容易验证。REA 让每项发现带上文件身份和工具记录,适合需要复查的分析任务。
但文件哈希只能证明分析对象是谁,无法证明解释正确。「代码在条件满足时写入 0x80」和「松开跳跃键会加重重力」之间,还差一次行为验证。
作者没有提供与人工分析、直接使用 Ghidra scripts,或其他模型的对照数据。「比手工快很多」可以作为个人经验,不能据此估算普通团队的效率收益。REA 提高了可追溯性,文章自身也证明,没有它仍能完成部分分析。
整数子像素便于复刻原程序的数值规则,但原文将其描述为避免浏览器间浮点漂移,论证不够充分;是否使用固定时间步、运算顺序是否一致同样重要。加上缺失的跳跃参数和未确认的标志语义,Arena Run 更适合称为借用了恢复参数的原创作品。
作者选择不复刻原作角色、美术和关卡,是本次项目的边界。原文也承认逆向规则因国家而异,不能把其中关于参数和创作素材的区分直接当作通用法律结论。
能拿来做什么
- 用 AI 阅读陌生前端或 Node 代码时,先给出可观察行为,例如「哪里每帧更新垂直速度」或「哪里限制输入值」,再要求返回调用链和具体位置。
- 把分析结果分成代码直接证明、根据上下文推断、为实现自行选择三类,并在注释中保留来源,避免后续开发把假设当成既定规则。
- 做 Canvas 2D 动作游戏时,可以参考整数子像素与固定时间步的组织方式,同时明确目标逻辑频率;要复现手感,还得验证起跳速度、重力切换条件和下落限速。
- 当静态分析卡在数据表、运行时计算或函数指针时,改用调试器观察输入和状态变化,别让模型继续用解释填补证据缺口。