ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

如何从14.3MB游戏代码中切出62808个函数:LostOdysseyRecomp函数边界分析实战指南

如何从14.3MB游戏代码中切出62808个函数:LostOdysseyRecomp函数边界分析实战指南

【免费下载链接】LostOdysseyRecomp

Experimental native PC port of Lost Odyssey using static recompilation. No game assets included.

项目地址:https://gitcode.com/gh_mirrors/lo/LostOdysseyRecomp
点击查看免费下载

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):

节起始大小说明
.rdata0x820006000x21EF2C只读数据、字符串
.pdata0x8221F6000x62798函数表,边界分析的基线
.text0x822900000xE4AA6C主代码,约 14.3 MB
.data0x831300000x248E98可写数据
.reloc0x833902000x144A24重定位表

基线从哪里来:.pdata + 分支目标

如果一切顺利,.pdata(Windows 异常处理用的函数表)就是现成的函数清单。但现实是它并不完整:

  1. .pdata覆盖不了全部函数——大量叶函数不在表里;
  2. 重编译器自己能发现一批起点(如bl调用目标、返回指令后的顺延代码),基线映射表config/baseline_func_mapping.txt里记录了63032 个这样的起点;
  3. 纯指针函数(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 符号止,生成一个函数跳转表导致的函数截短
3config/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.

项目地址:https://gitcode.com/gh_mirrors/lo/LostOdysseyRecomp
点击查看免费下载

相关推荐

上一篇:Kubernetes Python 客户端 DRA 模型实战:V1beta1NodeAllocatableResource 与节点可分配资源映射详解
下一篇:如何把你的环境接入AgentEvolver:EnvService环境服务API与自定义开发完全教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进