本人不是很有自信能写好这个教程,加上本人时间紧张没法保证能稳定更新,如果有疏漏或者疑问请在评论区指出,非常感谢! 首先非常感谢指导我学习 NDS 游戏破解的 Enler 大佬,如果没有他我的童年回忆的汉化和这个破解教程就不能实现了,非常感谢!
本教程的破解源代码(流星洛克人一代汉化的汉化破解源代码)将会在汉化完成后整理(为了尊重汉化组劳动成果,仅包含我之前乱翻的测试文本)并传到 Github 上,届时大家可以自行查看学习。
如果你是初次接触老游戏机游戏的破解,我个人先建议你去尝试一下简单的游戏破解教程: https://www.bilibili.com/read/cv18346849/
本文其实应该需要提前准备好字库以使用的(或者其他传统说法叫做字模),考虑到大家更加匮乏的是编写汇编代码的过程,而字库生成的方式可以自行设计,所以先讲解汇编代码的内容。
同时,这篇文章需要你对 ARM/THUMB 指令集有一定基础了解,且对计算机工作原理也要一定了解,否则阅读起来会非常吃力!
上面前言说了,我为了尽可能节省文章篇幅,跳过了制作字库的部分,那么我们先假设我们有这么一种字库:
所有字体尺寸都是 16x16 像素(虽然以实际情况来说,流星洛克人的主字体实际的显示尺寸是 12x12),且按照 GBA 4BPP 方式存储,即一个字体占用 16×16÷2=128 字节。 然后每个字形数据紧密相连,没有空隙。
如果按照这种方式存储的话,大家可以想象一下之后我们读取每个字形数据的时候,应该只需要将 字形索引编号 乘以 32,然后加上字库的起始地址(对于如果会把字库载入到整个内存的情况来说的话需要),就可以得到这个字形的数据了。
介于如何生成或者制作字库的方法,可以自行编写 Python 脚本或其他程序实现,这里不再赘述。
以流星洛克人举例,既然我们打算编写有关读取文件系统里的字库的代码,那么原先被写死的字库数据就可以被我们随意使用。毕竟那块区域之后就不会再被游戏使用了。
以此,我们可以借助 ARMIPS 的 .region
和 .autoregion
指令来让汇编器自动将我们的代码插入到指定的区域:
; 移动到原本的字库位置
.org (0xED898 + 0x20000000 - 0x4000)
.region 0x80 * 0x1E1
.endregion
; 编写某些 Hook 代码(地址是虚构的)
.org 0x02012346
bl LoadFont
; 从刚刚声明的区域开始插入代码
.autoregion
.align 4 ; 以防万一建议对齐到 4 字节
LoadFont:
; 省略代码
b lr
.endautoregion
在上一章,我们获取了游戏中任何使用了 NitroSDK 的函数的符号表。我们可以通过这些符号表来找到游戏中使用的 NitroSDK 函数。
那么首先我们应该还是需要将字体文件先在某个地方先行打开,留以后续用途。
通常来说,我们可以考虑通过 Hook FS_Init
函数来实现初始化我们的字库文件,因为当这个函数被调用之后,大部分破解常用的游戏系统组件都已经初始化完成。
接下来我们就是要寻找什么函数调用了 FS_Init
函数了,将其覆写成我们的自定义函数,以实现初始化资源的目的。
打开 No$GBA ,按下 Ctrl + G
打开跳转窗口,输入 FS_Init
,然后回车:
然后点击汇编面板上对应的 FS_Init
的第一个指令(通常是 push r14
),该行字体会变为红色,表示已经添加了断点:
接下来按下 Ctrl + R
重启游戏,游戏应该会立即暂停在 FS_Init
函数的开头:
有提前了解过 ARM 指令集的朋友应该可以知道,每当处理器发生跳转的时候(执行 bl
或 blx
指令)都会将 R14
寄存器(又被称作 LR
寄存器)的值设置为跳转前的指令地址(也就是返回地址)。
也就是说这个寄存器在刚进入 FS_Init
函数的时候,存储的就是调用 FS_Init
函数的地址。
为了避免判断出错,我们通过调试器的“跳转到函数返回点”(Run to Sub-return)来直接跳转到调用 FS_Init
的位置,选择 Run 菜单,然后点击 Run to Sub-return:
此时当前指令的上一行应该是 bl FS_Init
或者 blx FS_Init
这时候我们可以看到当前指令的上一行应该是 bl FS_Init
或者 blx FS_Init
,这时候我们就可以将这个地址(此处是 0x02012B14
)记录下来,作为我们之后 Hook 的目标地址。
接下来就可以编写 汇编代码来 Hook 这个函数了,我们将这一个地址的指令替换成我们的初始化函数,然后在里面调用 FS_Init
函数来完成原本的初始化之后再运行我们自己的代码:
; Hook FS_Init 函数
.org 0x02012B14
bl Hook_FS_Init
.autoregion
.align 4
Hook_FS_Init:
push {r4, r5, r6, r7, lr} ; 保存寄存器状态
; 调用原本的 FS_Init 函数
blx FS_Init
; 在这里编写你的初始化代码
; 比如打开字库文件,读取字库数据等等
bl LoadFont
; 返回到原本的 FS_Init 函数调用处
pop {r4, r5, r6, r7, pc} ; 恢复寄存器状态并返回
.endautoregion
如果你的游戏它使用了以下函数:
FS_ReadFile
FS_SeekFile
FS_InitFile
FS_OpenFile
那就可以实现在 NDS 里轻松读写文件了(虽然如果没有,也可以自己手写,不过难度很高,这里不再赘述)
这里提供一个我自己使用的思路:
因为游戏会直接在固定的位置读取字体数据,那么只要我们可以将字库数据放到这个位置上,再把游戏读取的字体位置设置成这个地址,那么游戏就可以直接读取我们的字库数据了。