我的头像小萧的开源知识站
首页标签系列子网站

编写汇编代码以加载字库

撰写日期 2024-07-01 阅读大约用时 4 分钟

本人不是很有自信能写好这个教程,加上本人时间紧张没法保证能稳定更新,如果有疏漏或者疑问请在评论区指出,非常感谢! 首先非常感谢指导我学习 NDS 游戏破解的 Enler 大佬,如果没有他我的童年回忆的汉化和这个破解教程就不能实现了,非常感谢!

本教程的破解源代码(流星洛克人一代汉化的汉化破解源代码)将会在汉化完成后整理(为了尊重汉化组劳动成果,仅包含我之前乱翻的测试文本)并传到 Github 上,届时大家可以自行查看学习。

如果你是初次接触老游戏机游戏的破解,我个人先建议你去尝试一下简单的游戏破解教程: https://www.bilibili.com/read/cv18346849/

前言

本文其实应该需要提前准备好字库以使用的(或者其他传统说法叫做字模),考虑到大家更加匮乏的是编写汇编代码的过程,而字库生成的方式可以自行设计,所以先讲解汇编代码的内容。

同时,这篇文章需要你对 ARM/THUMB 指令集有一定基础了解,且对计算机工作原理也要一定了解,否则阅读起来会非常吃力!

字库?

上面前言说了,我为了尽可能节省文章篇幅,跳过了制作字库的部分,那么我们先假设我们有这么一种字库:

所有字体尺寸都是 16x16 像素(虽然以实际情况来说,流星洛克人的主字体实际的显示尺寸是 12x12),且按照 GBA 4BPP 方式存储,即一个字体占用 16×16÷2=128 字节。 然后每个字形数据紧密相连,没有空隙。

00000000100200300400500600700800900A00B00C00D00E00F
000000000001000011011011011001000000000000011011001
100000000010003000000031000011011011011000000001000
200000000000000011000000000010000000000000000000000
300000000000000000000000000011000000000000000000000
400000000001000000000001000000010001000000000000000
500000000000000000000000000000000000000000000000000
600000000000000000000000000000000000000000000000000
700000000000000000000000000000000000000000000000000
流星洛克人 天马 中的“字”字的数据内容和可视化
将光标悬浮到每个位或像素上可以查看对应映射到的数据/像素位置

如果按照这种方式存储的话,大家可以想象一下之后我们读取每个字形数据的时候,应该只需要将 字形索引编号 乘以 32,然后加上字库的起始地址(对于如果会把字库载入到整个内存的情况来说的话需要),就可以得到这个字形的数据了。

介于如何生成或者制作字库的方法,可以自行编写 Python 脚本或其他程序实现,这里不再赘述。

确认需要插入汇编代码的位置

以流星洛克人举例,既然我们打算编写有关读取文件系统里的字库的代码,那么原先被写死的字库数据就可以被我们随意使用。毕竟那块区域之后就不会再被游戏使用了。

流星洛克人 天马 的主字体所在区域(0xED798)

以此,我们可以借助 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 函数

然后点击汇编面板上对应的 FS_Init 的第一个指令(通常是 push r14),该行字体会变为红色,表示已经添加了断点:

添加断点

接下来按下 Ctrl + R 重启游戏,游戏应该会立即暂停在 FS_Init 函数的开头:

暂停在 FS_Init 函数开头

有提前了解过 ARM 指令集的朋友应该可以知道,每当处理器发生跳转的时候(执行 blblx 指令)都会将 R14 寄存器(又被称作 LR 寄存器)的值设置为跳转前的指令地址(也就是返回地址)。 也就是说这个寄存器在刚进入 FS_Init 函数的时候,存储的就是调用 FS_Init 函数的地址。

为了避免判断出错,我们通过调试器的“跳转到函数返回点”(Run to Sub-return)来直接跳转到调用 FS_Init 的位置,选择 Run 菜单,然后点击 Run to Sub-return:

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

做一些初始化

如果你的游戏它使用了以下函数:

那就可以实现在 NDS 里轻松读写文件了(虽然如果没有,也可以自己手写,不过难度很高,这里不再赘述)

如何被游戏读取我们的字形?

这里提供一个我自己使用的思路:

因为游戏会直接在固定的位置读取字体数据,那么只要我们可以将字库数据放到这个位置上,再把游戏读取的字体位置设置成这个地址,那么游戏就可以直接读取我们的字库数据了。