从零开始的反编译项目:Claude Code与2001年GBA游戏的51%

Lobsters Hottest 新闻

摘要

文章详细介绍了使用Claude Code启动一个2001年GBA游戏的反编译项目,实现了51%的反编译并发现了一个隐藏的彩蛋。

<p><a href="https://lobste.rs/s/ltm5qj/starting_decompilation_project_from">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/17 18:09

# 从零开始启动反编译项目:Claude Code 与 2001 年 GBA 游戏的 51% 源代码:https://gambiconf.substack.com/p/starting-a-decompilation-project [](https://substackcdn.com/image/fetch/$s_!djbw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748bf07b-13f0-49e4-94f4-9968f2943546_1620x702.png) 在上一章 (https://gambiconf.substack.com/p/can-llms-really-do-matching-decompilation) 中,我们获得了数据:一个由大语言模型驱动的流程匹配了 74% 的基准函数。现在,是时候看看它在一个完整游戏上的表现了。经过一年研究和构建用于**匹配反编译**的 AI 工具后,我终于不再拖延,开始反编译“*风之克罗诺亚2:梦中的帝国*”(KEoD)!这是一款我非常喜欢的 Game Boy Advance 游戏,正如我们将看到的,它也颇具挑战性:其函数异常庞大。 > ⚙️***什么是匹配反编译?*** 匹配反编译是一门将汇编代码转换回 C 源代码的艺术,转换后的 C 代码在编译时能产生字节完全一致的机器码。它在复古游戏社区中很流行,用于重现经典游戏的源代码。例如,《超级马里奥 64》(https://github.com/n64decomp/sm64) 和《塞尔达传说:时之笛》(https://github.com/zeldaret/oot) 已被完全匹配反编译。在我撰写本章时,该游戏 51% 的代码字节已被反编译。非常感谢 Felipe Sanches (https://github.com/felipesanches) 和 testyourmine (https://github.com/testyourmine) 帮助达成这一里程碑! ***剧透:** 我们将看到 Claude Code 如何自主发现了一个隐藏了 25 年的彩蛋!* 第一步当然是搭建项目脚手架。反编译项目的结构没有单一标准,不过社区通常遵循一些约定俗成的结构。对于 GBA 游戏,最流行的结构之一是 pret (https://pret.github.io/) 所使用的。我最初的想法是遵循与《索尼克 Advance 3》(https://github.com/SAT-R/sa3) 相同的代码组织方式,因为我对它更熟悉,但我很快偏离了,因为我设定了以下目标: - 项目不应包含来自 ROM 的任何资源或汇编代码。例如,[《Snowboard Kids 2》](https://github.com/cdlewis/snowboardkids2-decomp) 和 [《动物森林》](https://github.com/zeldaret/af) 就是这种情况。 - 我希望设置是可重现的,作为一条铺好的道路供其他 GBA 游戏 follow,因为我还想反编译其他克罗诺亚 GBA 游戏。为日后复制打下基础很重要。 - 更重要的是,项目组织结构应便于 AI 智能体工作。 这些目标有两个重要含义: - **基于脚本的设置:**我们需要有一个自动化脚本,能够从给定的 `.gba` ROM 生成 `.s` 文件,因为这些文件不受版本控制。这意味着对 `.s` 文件的任何更改都必须在生成它们的脚本中进行,而不是直接修改 `.s` 文件。例如,重命名函数或将其拆分为模块应在生成器脚本中完成。这与 pret 项目的运作方式不同,因为那些项目的汇编代码是提交到仓库的。 - **尽可能少的人工工作:**由于这个项目应该对 AI 友好,因此应该可以轻松生成 `git worktree` 以支持并行工作,并且应该易于添加新的已匹配函数。 设定了这些目标后,第一个产物就是汇编代码本身。为此,我使用了 [Luvdis](https://github.com/aarant/luvdis)。这是一个专为匹配反编译设计的 GBA 反汇编器。它读取 KEoD 的 ROM 并输出一个巨大的单个 `.s` 文件。由于我不希望在 Git 中保留任何原始游戏的汇编源代码,我将 Luvdis 作为一个 git 子模块包含,并编写了一个 shell 脚本 `./setup.sh` 来调用它。除了调用 Luvdis,这个脚本还会扩展以执行我们接下来要讨论的所有转换:编译器、改进反汇编代码、移动 `.s` 文件等。 我们需要找到开发者使用的编译器。由于许多 GBA 游戏使用 [agbcc](https://github.com/pret/agbcc)(GCC 2.95 的一个分支),KEoD 很可能也是如此。值得一提的是,KEoD 是在 GBA 发售后仅 4 个月,即 2001 年 7 月发行的。**它目前是正在被积极反编译的最古老的 GBA 游戏!** 1 (https://gambiconf.substack.com/p/starting-a-decompilation-project#footnote-1) 考虑到这个发行日期,我们可以排除两款流行的 GBA 游戏开发编译器:2001 年 11 月发布的 ADS 1.2 (https://silo.tips/download/developer-suite-arm-installation-and-license-management-guide-version-12-copyrig),以及 2002 年 4 月发布的 Metrowerks CodeWarrior (https://www.gamedeveloper.com/game-platforms/metrowerks-announces-codewarrior-tools-for-agb)。当然,开发者可能不走寻常路,使用了不同的编译器,比如 ADS 1.1。无论如何,我从 agbcc 开始,既然它能让简单函数用干净的 C 代码匹配成功,我就坚持使用它了。这是毫无疑问的。2 (https://gambiconf.substack.com/p/starting-a-decompilation-project#footnote-2) 为了编译这些最初简单的函数,我们需要让 Makefile 和链接器脚本工作起来。它们由 Claude Code 编写,灵感来自其他一些反编译项目。虽然 Luvdis 很适合入门,但我们仍需进行许多改进,使反汇编代码准备好被反编译。下面来讨论这些改进。 Luvdis 自动检测到 93 个函数,但游戏的函数肯定远不止这些。因此,我使用 Ghidra 来丰富函数列表,它又找到了约 300 个新函数。在使用 Claude Code 找到更多函数、合并结果并清理误报后,我们最终总共得到了 **663 个函数**。我对这个游戏的函数只有几百个感到惊讶。其他 GBA 游戏通常有更多函数。另一方面,KEoD 的函数通常更大,大得多。这将使游戏的反编译成为一项挑战,因为大函数通常比小函数更难处理。 [](https://substackcdn.com/image/fetch/$s_!UX6v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb374f14c-3348-4d29-a5f4-6a6ad9d570d8_2678x1676.png) 各个 GBA 匹配反编译项目的函数数量与规模 大部分函数拆分工作由 Claude Code 完成。它编写了一个可重现的 Python 脚本 `generate_asm.py`,该脚本接收 Luvdis 最初输出的代码,并将其拆分为更小的函数,每个函数位于自己的 `.s` 模块中。虽然大部分工作是由 AI 完成的,但我简要解释一下它使用的一个有趣技术和我们遇到的一个错误。经过多次反复迭代才得到合理的函数拆分,但这两个小节将让你一窥其工作原理。 Ghidra 找到了被代码直接引用的函数。例如,那些带有 `bl` 指令的函数。但许多函数的调用在汇编中并没有出现任何 `bl` 指令。这是因为代码使用了一个函数表。例如: 它编译为: 因此,我们需要在字面池中找到表的地址(此处为 `0x08000100`),并将存储在那里的字解包为函数地址。这种模式在这个游戏中频繁出现,因为它有一个大的回调机和许多分发表。以 gCallbackQueue 为例。 (https://github.com/Dream-Atelier/kl-eod-decomp/blob/6fe0e0b789bf76003fc5a262b9991919d8fdbc96/include/structs/variables.h#L301-L310) 大约 20% 的函数只能通过这样的指针到达。当 Claude Code 拆分函数时,它定义了每个函数起始的地址。但当地址放错位置时,就会产生一个函数片段。换句话说,一个不是自包含的函数。例如: `FragmentedFunction` 中没有返回语句。因此,它会落入下一个函数,执行 `FallthroughFunction` 的代码。如果我们要求 Claude Code 反编译这些函数之一,它会失败或作弊。这是因为上面的拆分是有缺陷的。通过合并这些错误拆分的函数,我们可以编写与合并版本匹配的 C 代码: 它编译为: 出现片段化函数的原因可能是: - 我们的函数拆分算法有错误(几乎总是这种情况)。 - 汇编是手写的(不太可能) - 原始 C 代码使用了 naked 属性 (https://support.arm.com/documentation/dui0774/l/Compiler-specific-Function--Variable--and-Type-Attributes/--attribute----naked---function-attribute)(不太可能) 拆分函数后,下一步是将单一的汇编文件拆分为逻辑模块。我使用 Ghidra 找到了好的边界(参见 Ghidra 脚本 (https://github.com/Dream-Atelier/kl-eod-decomp/blob/90be2ad100c0405e85bfbc0235b00f9bbbb40953/scripts/detect_data_regions.py)),然后更新 `generate_asm.py` 以自动将 `.s` 模块移动到各自的文件夹中。边界定义在 [TOML 文件](https://github.com/Dream-Atelier/kl-eod-decomp/blob/f210867369c921d42eb0a3e22af67c4ddfc9b3d4/klonoa-eod-decomp.toml#L1-L47) 中。重命名也在 `generate_asm.py` 中执行,基于 [TOML 中定义的名称](https://github.com/Dream-Atelier/kl-eod-decomp/blob/f210867369c921d42eb0a3e22af67c4ddfc9b3d4/klonoa-eod-decomp.toml#L49-L539)。 Sanches 使用 Claude Code 自动为函数赋予语义名称。即使它们尚未被反编译,Claude 也会根据汇编模式提供合理的猜测。 当一个函数被编译时,除了它的代码,可能还包括它引用的数据。例如: 编译为: 如果没有合适的反汇编器来识别给定地址是作为数据读取而不是作为代码执行,它将被反汇编为: 虽然内容相同,并且两者都编译成字节完全一致的机器码,但这种拼写在许多方面都不佳: 1. 像 m2c 和 asmlift 这样的程序化反编译器会产生错误的 C 代码或拒绝处理,因为它们会尝试将无意义的指令转换为 C 代码。出于同样的原因,大语言模型也感到困难。 2. objdiff (https://github.com/encounter/objdiff/),我们用来计算我们的 C 代码与目标汇编匹配程度的工具,将数据计为不匹配的指令。它夸大了差异,使得更难识别真正的代码差异。 3. 还记得上一节的“函数指针表”吗?嗯,如果不看到实际的函数地址,就无法在汇编中找到指针表。 Luvdis 已经做了相当多的工作来恢复每个能证明是数据的池,但它漏掉了一些在完全反编译游戏时相关的用例。因此,我运行了几轮,要求 Claude Code 识别那些被当作代码处理的数据部分,并将它们保存在 [TOML 文件](https://github.com/Dream-Atelier/kl-eod-decomp/blob/f210867369c921d42eb0a3e22af67c4ddfc9b3d4/klonoa-eod-decomp.toml#L541-L3184) 中。`generate_asm.py` 使用此信息来更新汇编代码。 我们需要一个顺畅的过程来用相应的反编译 C 代码替换汇编函数。一次一个函数。因此,我将其组织为两个文件夹:`asm/matchings` 和 `asm/nonmatchings`。当一个函数匹配成功后,它会被自动移动到 `asm/matchings`。通过始终保留原始的汇编函数(而不是依赖来自 `build/src` 的编译版本),我们拥有单一的汇编拼写方式。否则,我们会为未匹配的函数使用 `generate_asm.py` 的拼写,为已匹配的函数使用 agbcc 的拼写。确保我们有一个统一的拼写方式有助于减少比较函数时的思维负担,这在 Claude Code 阅读代码库时尤其重要。 同样考虑到 Claude Code,并受《Snowboard Kids 2》启发,我使用 `INCLUDE_ASM` 宏来组织 C 代码。每个尚未反编译的函数都以一个存根形式出现,像这样: 这样,智能体就可以轻松添加新的匹配函数:通常,它只需要将 `INCLUDE_ASM` 调用替换为等效的 C 代码。后来我了解到,这种使用 `INCLUDE_ASM` 存根的方法被非正式地命名为“splat 风格”,因为 [splat](https://github.com/ethteck/splat) 就是这样搭建项目脚手架的。这个工具可以将二进制文件拆分为更容易处理的源文件。它支持 N64、PSX、PS2、PSP……但不支持 GBA。因此,有趣的是,我在没有 splat 的情况下复制了 splat 风格。 在我匹配了一组简单函数后,testyourmine 创建了自己的仓库来反编译这个游戏 ([kleod](https://github.com/testyourmine/kleod/))。他愿意贡献,但不喜欢我项目的结构,而且我的代码太乱了(我同意 😅)。因此,我导入了他反编译的大部分函数和部分项目结构。尽管底层结构不同,但遵循一些社区标准是好事,因为它会更容易共享代码和获取灵感。例如,使用相同的 m4a 函数名称和每个 GBA 游戏通用的实用宏(例如 DMA 相关的宏)有助于减少代码库的差异,并在阅读其他 GBA 反编译项目代码寻找参考时降低认知负担。这对大语言模型很重要:Claude Code 通过阅读它们的代码来寻找参考,减少差异可以更好地利用这些参考。 此外,他的代码质量很棒。他的 C 代码比 AI 生成的干净得多。将他的函数移植到我的仓库,为 Claude Code 在反编译其他函数时树立了更好的遵循标准。 反编译的艰苦工作主要由 Claude Code 完成。但仅仅要求它去反编译,并期望它把一切都做好,这对 AI 来说要求太高了。所以,让我们来谈谈我们将接入 AI 循环以使其工作的工具:新工具和对 agbcc 的改进。 你可能已经注意到了,我非常喜欢构建工具,为了加速反编译过程,我壮大了工具家族!以下工具加入了这个行列:asmlift、Transmuter 和 gba-kit。每个工具都值得专门一章来介绍,但这里简要谈谈它们。 引发构建 **[asmlift](https://github.com/macabeus/asmlift)** 的驱动性问题是:**如果我们不专注于完成匹配,而是构建一台机器来为我们完成匹配呢?** asmlift 是一个程序化的反编译器。它很像 [m2c](https://github.com/matt-kempster/m2c)。事实上,构建 asmlift 时借鉴了许多从 m2c 中学到的东西。关键的区别在于它的开发方式。大多数专注于 AI 的匹配反编译项目都一次关注一个函数的匹配。它们要求 AI 匹配一个函数,并且作为额外输出,它可能会生成带有学习心得的 markdown 文件,希望这些能帮助大语言模型反编译接下来的函数。但我热衷于探索一种不同的方法:**使用 AI 从头开始设计一个模块化的匹配反编译器,加上使用 AI 循环来自动迭代改进反编译器本身。** asmlift 就是这次探索的成果。 在开发 asmlift 时,我更多地关注基准和评估方面,而不是反编译过程本身。有趣的是,当我在构建 asmlift 时,[mahaloz](https://github.com/mahaloz/) 正在构建 [Kuna](https://github.com/Noelo-Lab/kuna),一个智能体优先的反编译器,设计初衷就是由其他智能体来完善它。你可以在 Kuna 的发布公告 (https://noelo.org/blog/kuna-release/) 中了解更多。有意思的是,尽管反编译领域不大,但这个大致相同的想法由两个人独立开发。值得注意其中的区别:asmlift 专为匹配反编译设计,专注于复古游戏使用的编译器,而 Kuna 是一个更通用的反编译器,并且更

相似文章

From Zero to Claude Code in 19 Minutes (no code)

YouTube AI Channels

本文介绍了如何使用 Claude Code(无需编码知识)通过自然语言构建项目,包括计划模式、持久记忆、MCP 连接器等核心功能,并演示了打字游戏和看板应用的实际构建过程。