【免费下载链接】LostOdysseyRecomp
Experimental native PC port of Lost Odyssey using static recompilation. No game assets included.
LostOdysseyRecomp 是一个基于静态重编译(static recompilation)技术的《失落的奥德赛》(Lost Odyssey)原生 PC 移植项目。它的核心工作之一,就是从那约14.3 MB 的 PowerPC 代码段(.text 节)中,精准切分出62808 个函数——每个函数的起始地址和长度必须分毫不差,重编译器才能生成可运行的宿主代码。这篇文章带你完整走一遍这套"函数边界分析"流水线:从 XEX 镜像 dump,到 .pdata 基线、跳转表修复,再到自动脚本与手工兜底。
为什么静态重编译必须先切函数?
静态重编译的思路是:在构建时把整份游戏代码逐函数翻译成宿主 C++,而不是像动态模拟器那样运行时逐条解释。这对重编译器的要求非常苛刻:
| 问题 | 切错函数边界后的后果 |
|---|---|
| 两个函数被并成一个 | 前一个函数的尾部数据被当成代码翻译,行为错乱 |
| 一个函数被截短 | 函数内部的b(跳转)变成"跳出函数"的未解析目标,直接生成失败 |
| 虚函数/函数指针目标漏切 | 运行时宿主跳到空指针(地址 0),当场崩溃 |
而原始镜像是一整块扁平二进制,函数之间没有任何显式标记,全凭静态分析推断边界。这就是本项目函数边界工作的全部难点所在。
📌 目标二进制:四张光盘的
default.xex解密解压后逐字节相同(md5f07cb5afd8ab6fa8fbdc14f4fceb5858),所以只需处理 Disc 1 一个镜像,base0x82000000。
第一步:用 xexdump 提取扁平镜像
工具链的第一步是把 XEX 文件解密、解压成一张扁平内存镜像,并导出符号表:
cmake -S tools/xexdump -B out/build/tools -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ cmake --build out/build/tools --target XenonRecomp XenonAnalyse xexdump out/build/tools/xexdump LostOdysseyRecompLib/private/disc1/default.xex \ LostOdysseyRecompLib/private/image_disc1.bin- 工具源码:tools/xexdump/(
xexdump <in.xex> <out.bin>) - 构建流程:docs/BUILDING.md
dump 出来的节区布局(摘自 docs/notes/recomp.zh-CN.md):
| 节 | 起始 | 大小 | 说明 |
|---|---|---|---|
| .rdata | 0x82000600 | 0x21EF2C | 只读数据、字符串 |
| .pdata | 0x8221F600 | 0x62798 | 函数表,边界分析的基线 |
| .text | 0x82290000 | 0xE4AA6C | 主代码,约 14.3 MB |
| .data | 0x83130000 | 0x248E98 | 可写数据 |
| .reloc | 0x83390200 | 0x144A24 | 重定位表 |
基线从哪里来:.pdata + 分支目标
如果一切顺利,.pdata(Windows 异常处理用的函数表)就是现成的函数清单。但现实是它并不完整:
.pdata覆盖不了全部函数——大量叶函数不在表里;- 重编译器自己能发现一批起点(如
bl调用目标、返回指令后的顺延代码),基线映射表config/baseline_func_mapping.txt里记录了63032 个这样的起点; - 纯指针函数(pointer-only functions)——只被虚表 / 函数指针表引用、不在
.pdata、也不是任何bl目标的函数,边界分析完全看不到它们,会被并进前一个函数的尾部。
项目里真实的事故是这样的:0x823067C0被并进了0x82306660,当虚表槽 67 调用它时,函数表里是空指针,宿主直接跳到地址 0 崩溃。
最隐蔽的坑:跳转表让函数"自我截短"
这是本项目函数边界工作中最核心的问题。XenonRecomp 的边界分析把bctr(经 CTR 寄存器跳转)当作尾调用处理,于是:
一个不在 .pdata 里、内部带
switch跳转表的叶函数会被截短;截短后,函数内部的b指令变成了"跳出函数"的未解析目标。
首轮统计:91 个 switch 站点、96 个未解析目标全部因此而来。另外,重编译输出里的// ERROR <addr>注释就是这些未解析目标的直接标记,是边界修正的重要信号源。
XenonAnalyse 检测到的841 张跳转表(全部为绝对地址表)写入了 LostOdysseyRecompLib/config/switch_tables.toml;漏检的表手工补在 LostOdysseyRecompLib/config/switch_tables_manual.toml(目前手工补了一张82FB21AC的 u16 偏移表,16 项)。
核心武器:gen_function_bounds.py 的五条规则
tools/gen_function_bounds.py 是整个流水线的中枢脚本。输入是扁平镜像、switch_tables.toml、上一轮重编译日志和输出,输出是写入 LostOdysseyRecompLib/config/LostOdysseyRecomp.toml 的functions = [...]显式边界列表。规则如下:
| # | 规则 | 解决什么问题 |
|---|---|---|
| 1 | 每个// ERROR <addr>目标成为一个函数(延伸到下一个符号,且不超出所在 .pdata 函数末尾),目标跨轮累积在config/branch_targets.txt | 分支分析漏掉的函数起点 |
| 2 | 每张 label 未被所在 .pdata 函数覆盖的跳转表,从最近符号起、到最远 label 之后的下一个 pdata/bl 符号止,生成一个函数 | 跳转表导致的函数截短 |
| 3 | config/function_bounds_manual.txt提供手工条目 | 自动规则覆盖不到的边角 |
| 4 | 指针专属函数:扫描 .text 以外整个镜像里指向 .text 的 4 字节对齐指针,要求不在 .pdata 内、前一个字是blr/bctr/b/0 填充、不是 switch 标签、不在基线映射里。命中43 个,同时作为"硬符号"截断前面的函数 | 虚表/函数指针独占的函数 |
| 5 | 代码中物化的地址:lis rX,hi+addi/ori(64 字节窗口内)拼出的 .text 地址,条件同上,且前一个字必须是blr或 0 填充(若允许b,会把计算跳转的 case 标签误判成函数,产生 15 个错误文件) | 运行时才填进对象、无任何静态引用的函数指针 |
⚠️ 两个关键细节:
- 基线过滤不可省:规则 4 不加
baseline_func_mapping.txt过滤时,会误报5000+ 个"函数"(那些其实重编译器自己就能发现),并产生大量// ERROR。 - 错误目标默认不再累积:由错误切分产生的假目标会永久污染
branch_targets.txt,所以脚本默认不累积重编译输出的// ERROR,需要时显式设置环境变量LO_ACCUMULATE_ERRORS=1。
手工兜底的真实用例只有一条,却十分典型——LostOdysseyRecompLib/config/function_bounds_manual.txt:
827C39F8 18 # small stub that otherwise flows into pdata function # 827C3A10 and steals its switch table一个 24 字节的小函数,如果切错就会"流进"相邻的 .pdata 函数并抢走它的跳转表。最终functions = [...]列表共116 项,其中47 个是指针专属函数。
收敛验证:如何确认"切对了"
这套流程不是一次跑通的,而是迭代到第 8 轮收敛:0 未解析目标、0 跳转表错误。每轮改动后的验收动作:
# 1. 重新生成 python3 -B tools/ppc_codegen.py generate # 2. 检查未解析目标,必须无输出 grep -l '// ERROR' LostOdysseyRecompLib/ppc/*.cpp运行期还有第二道保险:kernel/memory.cpp会把函数表里所有空槽填成MissingFunction记录桩,日志一旦打印call to unrecompiled guest function(例如... ctr=0x82CF21C0),就说明还有漏网的函数——那正是规则 5 要处理的类型。
最终生成结果(见 docs/notes/recomp.zh-CN.md):
| 指标 | 数值 |
|---|---|
| 生成目录 | LostOdysseyRecompLib/ppc/ |
| 文件数 | 250 个 |
| 总大小 | 252 MB |
| 函数数 | 62808 个 |
| 单轮 XenonRecomp 耗时 | 约 90 秒(MSVC 构建) |
进阶案例:bdzlr 条件位修正让 memset 少切了两刀
边界分析还有一个上游 Bug 值得记录:XenonAnalyse 用!(BO & 0x10)判断bclr/bcctr是否有条件,只看了"忽略 CR"位,漏了"忽略 CTR"位(0x04),导致bdzlr/bdnzlr被当成无条件返回。命中处是 CRT 的memset(0x82B7BC40,尾部为stb; bdzlr; stb; bdzlr; stb; blr):长度 %4 为 2 或 3 时少写 1~2 字节,最终表现为USkeletalMeshComponent::SkelControlIndex63 项只填了 61 项、进战斗场景越界崩溃。修正为(BO & 0x14) != 0x14后,重跑函数数减少 2 个(0x82B7BCD0/0x82B7BCD8并回 memset),其余无变化。补丁位于 tools/patches/XenonRecomp-lostodyssey.patch,tools/build_tools.bat编译前会自动应用。
相关文件速查
| 资料 | 路径 |
|---|---|
| 边界分析完整笔记(中/英) | docs/notes/recomp.zh-CN.md / docs/notes/recomp.md |
| 边界生成脚本 | tools/gen_function_bounds.py |
| 显式函数边界表 | LostOdysseyRecompLib/config/LostOdysseyRecomp.toml |
| 手工边界条目 | LostOdysseyRecompLib/config/function_bounds_manual.txt |
| 跳转表配置 | LostOdysseyRecompLib/config/switch_tables.toml |
| 镜像 dump 工具 | tools/xexdump/ |
| 构建文档 | docs/BUILDING.md |
| 生成入口脚本 | tools/ppc_codegen.py |
| 工具目录总览 | tools/README.md |
一句话总结:函数边界分析 =.pdata基线 + 分支目标累积 + 跳转表修正 + 指针专属函数扫描 + 手工兜底,配合"grep// ERROR必须为 0"的收敛标准,迭代 8 轮,最终从 14.3 MB 的 .text 中稳定切出 62808 个函数——这正是整个静态重编译项目能跑起来的基石。
如果你想动手复现,仓库地址:
git clone https://gitcode.com/gh_mirrors/lo/LostOdysseyRecomp【免费下载链接】LostOdysseyRecomp
Experimental native PC port of Lost Odyssey using static recompilation. No game assets included.
相关推荐
如何选择最适合你的开源字体:M+ FONTS 实用指南
如何选择最适合你的开源字体:M+ FONTS 实用指南 还在为寻找一款既美观又实用的开源字体而烦恼吗?M+ FONTS 开源字体项目或许就是你一直在寻找的答案。
UI组件StarRocks array_slice 数组切片函数完全指南:语法、参数、边界行为与源码实现剖析
StarRocks array_slice 数组切片函数完全指南:语法、参数、边界行为与源码实现剖析 数组切片(Array Slicing)是 SQL 分析中最
大数据OLAP数据仓库湖仓一体数据库数据分析react-md-editor深度揭秘:CodeMirror的getTokenAt如何实现光标格式智能检测
react md editor深度揭秘:CodeMirror的getTokenAt如何实现光标格式智能检测 react md editor 是一个基于 Reac
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考