PSP中文字型之梦(Written by Codex)
对许多人来说,PSP已经是一台属于过去的掌上主机。它的解析度只有 480 × 272,系统介面也早已停止更新;但对一个在乎字体排印、在乎每一个字形如何落在萤幕上的人来说,有些问题不会因为硬体老去而自动消失。
PSP中文系统的字形显示,就是这样一个困扰了我超过五年的问题。
这五年来,我不断解密、比较、修改 `paf.prx`,再把每一个新版本放回 PSP实机测试。我没有完整的 PSP开发环境,也无法在电脑上重现所有系统行为。我的研究方式很原始:提出一个假设,改几个位元组,保存成新的档案,安装到实机,然后观察XMB能不能启动、载入了哪一种字型、缺字时又发生了什么。
这是一种缓慢而孤独的研究。直到AI加入之后,那些分散在位址、机器码与实机现象之间的线索,才终于连成了一条完整的路径。
我真正想解决的,不只是「换字体」
PSP的系统字型行为并不一致。
当系统语言设为日文时,`paf.prx` 会优先使用日文字型 `jpn0.pgf`。PGF是PSP原生的比例字型格式,字形轮廓、笔画比例与萤幕上的排印品质,都明显优于中文系统使用的点阵字型。如果 `jpn0.pgf` 找不到某一个中文字,系统还会回到 `gb3s1518.bwfon`,以点阵字型补上缺少的字形。
换句话说,日文系统原本就具备一套合理的字型后备机制:
jpn0.pgf
↓ 缺字
gb3s1518.bwfon但是,当系统语言设为韩文、繁体中文或简体中文时,`paf.prx` 不会先载入 `jpn0.pgf`,而是直接使用 `gb3s1518.bwfon`。中文虽然能够显示,却失去了PGF的字形品质。对不在意字体排印的人来说,这或许只是一个很小的差异;对我来说,它却一直存在于每一个选单、每一段文字、每一次开机之后。
我的目标从来不是简单地把中文点阵字型「换成」日文字型。我真正想要的是:
中文系统
↓
优先使用 jpn0.pgf
↓ PGF没有这个字
回到 gb3s1518.bwfon也就是让中文系统拥有和原版日文系统相同的字型品质,同时保留完整的中文字覆盖范围。
第一次突破,以及它留下的缺口
长期实验之后,我找到了一组确实会改变结果的机器码。
在 Ghidra 标示的 `0xD277C` 附近,原本的两条MIPS指令是:
move $v0, $a3 addiu $sp, $sp, -0x10
将它们改成:
b 0xD259C move $a1, $zero
中文系统就会开始载入日文字型。从实机结果来看,这个修改第一次证明了一个重要事实:中文XMB并不是不能使用 `jpn0.pgf`,限制来自 `paf.prx` 内部的字型选择逻辑,而不是 PGF格式、中文字串或绘图引擎本身。
这已经是很大的进展,但它并不完整。
修改之后,中文点阵字型完全不再载入。`jpn0.pgf` 能显示的字变得漂亮了,可是 PGF没有收录的中文字失去了后备字型。结果只是从「所有中文都使用点阵」走到另一个极端:「所有文字都被迫使用日文字型」。
五年来最困难的地方,就卡在这里:我知道怎样让中文系统载入 `jpn0.pgf`,却不知道怎样在不破坏 `gb3s1518.bwfon` 的情况下,把它放到正确的优先顺序。
AI帮我看懂了旧 patch 真正做了什么
旧 patch 之所以会失去中文后备字型,并不是一个难以解释的副作用。完整反组译之后,它的原因其实非常直接。
`0xD277C` 不是一般的语言判断,而是中文 BWFont 的开启函式。它会准备一条写死的路径:
flash0:/font/gb3s1518.bwfon
接着呼叫 `0xD3B54`,建立点阵字型物件。
旧 patch 在这个函式一开始就无条件跳到 `0xD259C`。后者是普通PGF的依索引开启函式;把 `$a1` 清成零,实际上就是要求它开启第零号系统字型,也就是 `jpn0.pgf`。
因此,旧 patch 的实际意义不是「中文优先使用日文字型」,而是:
任何原本要开启 BWFont 的请求
↓ 全部改道
开启 jpn0.pgf`gb3s1518.bwfon` 的开启函式根本没有机会执行。后备字型消失,不是偶发错误,而是这个跳转必然造成的结果。
AI带来的关键改变,是把我多年累积的实机现象,与机器码的精确语义接在一起。我们先确认ELF载入区段存在 `0xC0` 的档案偏移差异,避免把虚拟位址和档案位址混在一起;接着处理MIPS的分支延迟槽,确认旧机器码其实同时完成了无条件跳转与参数清零;最后再向上追踪字型物件、函式表与字元选择流程。
这不是凭空猜出一个新位元组,而是第一次建立了 `paf.prx` 字型系统的结构模型。
`paf.prx` 其实保留了完整的字型后备机制
真正的突破来自 `paf.prx` 内部的一组字型槽位。
反组译显示,`paf.prx` 维护了五个字型槽。开启函式表中,大部分槽位使用普通PGF开启函式 `0xD259C` / `0xD2710`;只有槽2使用 `0xD277C` / `0xD27CC`,也就是 `gb3s1518.bwfon` 的点阵字型开启函式。
整个选择流程可以整理成三个层次:
- `0xDD220` 根据 Unicode 字元与系统语言,决定初始字型槽。
- `0xDD39C` 检查目前字型是否包含这个字元。
- `0xD2834` 实际查询某个字型物件有没有对应字形;如果主要字型缺字,就切换到另一个槽。
这表示 `paf.prx` 并不是只有「日文字型」和「中文字型」二选一。它原本就有能力同时保留多个字型物件,并在每一个字元绘制之前决定要使用哪一个。
日文系统的后备行为,就藏在 `0xDD39C` 里面:先检查槽1的PGF;如果找不到字形,再检查槽2的 BWFont。只要 BWFont 有这个字,系统就把当前字元切到槽2绘制。
我寻找了五年的机制,其实一直存在于原厂 `paf.prx` 里。真正的问题只是:中文语言被送进了另一条分支。
把韩文与简繁中文绑在一起的那一条指令
决定性的位置在虚拟位址 `0xDD3F4` 到 `0xDD3FC`。
`paf.prx` 先读取系统语言编号,并减去 9:
addiu $v0, $v0, -9
接着执行:
sltiu $v0, $v0, 3
这条指令会判断减去 9 之后的值是否小于 3。因此,连续的三个语言编号都会得到相同结果:
| PSP语言编号 | 系统语言 | 原本的分支 | |---:|---|---| | 9 | 韩文 | BWFont 优先 | | 10 | 繁体中文 | BWFont 优先 | | 11 | 简体中文 | BWFont 优先 |
对繁中与简中来说,`0xDD220` 原本已经把常见CJK字元指向PGF槽;但到了 `0xDD39C`,上述范围判断又把语言 9、10、11 归为同一组,并优先查询槽2。由于中文字通常都能在 BWFont 找到,系统马上切到点阵字型,PGF槽便没有机会成为主要字型。
相反地,日文语言不在 9 到 11 的范围内,因此会走另一条现成路径:
先查询槽1:jpn0.pgf
↓ 没有字形
再查询槽2:gb3s1518.bwfon这正是我想要的行为。
最后的解法,只有一个位元组
最终 patch 不再碰 `0xD277C`,也不取代任何字型开启函式。它只修改语言范围判断:
虚拟位址: 0xDD3F8 档案偏移: 0xDD4B8 原机器码: 03 00 42 2C sltiu $v0, $v0, 3 新机器码: 01 00 42 2C sltiu $v0, $v0, 1
实际差异只有第一个位元组:
03 → 01
修改之后,减去 9 的结果必须小于 1 才会进入原本的特殊路径。于是语言行为变成:
| 系统语言 | 修改后的行为 | |---|---| | 韩文(9) | 保持原厂 BWFont 优先逻辑 | | 繁体中文(10) | PGF优先,缺字时回到 BWFont | | 简体中文(11) | PGF优先,缺字时回到 BWFont | | 其他语言 | 完全不变 |
这个解法最重要的地方,不是改得少,而是它没有破坏原本的架构。
`jpn0.pgf` 仍由 PGF开启函式载入;`gb3s1518.bwfon` 仍由 BWFont 开启函式载入;`0xD2834` 仍负责逐字检查有没有字形;`paf.prx` 原厂写好的字型后备机制也完整保留。我们只是让繁中与简中不再被强制归入“点阵字型优先”的语言范围。
旧 patch 是把一条路封起来,再把所有请求导向另一条路;新 patch 则是让系统走回它本来就拥有、而且更合理的那条路。
可重现的 patch 资料
这次实机验证使用的是PSP 6.61 轫体解密后的 `paf.prx`。为了让后续研究能够核对,原始档案与修改档案的资料如下:
| 项目 | 数值 | |---|---| | 原始档案大小 | `1,744,980` 位元组 | | 原始档案 SHA-256 | `699413b667199732af0b3cc2379ea65df0c4e2daa7727c6d1bbb757df0160417` | | 修改档案 SHA-256 | `b7437cb6d5a1fcf8738e420b5acb0380ec6f4be98e55baa1c33b4c3f06a9a457` | | 差异数量 | 1 个位元组 | | 差异位置 | 档案偏移 `0xDD4B8`,`03` 改为 `01` |
Ghidra 中的虚拟位址与这个ELF档案的实际偏移相差 `0xC0`,因此虚拟位址 `0xDD3F8` 对应档案偏移 `0xDD4B8`。这个对应关系只适用于上述 SHA-256 完全相同的解密档案;其他韧体版本或不同来源的 `paf.prx`,必须重新确认载入区段与指令内容,不能直接套用位址。
此外,这个版本必须以未套用旧跳转 patch 的原始档案为基础。若保留 `0xD277C` → `0xD259C` 的旧修改,BWFont 开启函式仍会被绕过,也就无法得到实机验证成功的后备效果。
五年的研究,不是被一个位元组取代
从结果来看,这个问题最后只需要修改一个位元组。可是这个位元组并不简单。
如果没有过去五年的实机实验,我不会知道日文系统原本具备怎样的后备行为,不会知道旧 patch 虽然成功载入PGF,却同时让 BWFont 消失,也不会有足够明确的目标去判断哪一种修改才算真正正确。
AI并没有取代这五年的研究。它做的是把我已经找到、却无法独自串起来的证据,转化成可以验证的控制流程:从位址对齐、MIPS指令、延迟槽、函式参数,一路追到五个字型槽、Unicode 字元分类与语言范围判断。
对一个没有完整开发环境,只能不断制作新档案并在PSP实机上尝试的人来说,这种协作改变了研究的可能性。过去,一个错误假设可能要花好几天才能排除;现在,实机结果可以立刻回到反组译逻辑中,成为下一轮分析的依据。
随后,我把它安装到PSP flash0 进行实机验证。系统成功优先载入并使用 `jpn0.pgf`;遇到日文字型没有收录的中文字时,`gb3s1518.bwfon` 也会正常接手。XMB可以正常运作,主要字型与后备字型同时存在。
实机结果与反组译得到的控制流程完全一致。这一个位元组的修改,确实同时满足了五年来最重要的两个条件:日文字型必须载入,中文后备字型也不能消失。到这里,它不再只是一项静态分析中的候选解法,而是已经在真实PSP硬体上成立的修正。
我在意的,是每一个字终于回到正确的位置
追求完美有时会被当成一种不必要的执着。毕竟这只是一台二十年前的掌上主机,只是几百乘几百画素的萤幕,只是大多数人不会特别注意的中文字形。
但字体排印从来不是装饰。一个字的笔画如何展开、字面如何与旁边的文字相处、不同语言放在同一行时是否协调,都会改变我们阅读一个介面的感受。系统能够显示中文,不代表它已经把中文显示好。
五年来,我想修正的就是这段差距。
这一个位元组通过实机验证,代表的不只是PSP中文系统终于能够使用更漂亮的日文字型,也不只是一个旧韧体档案被成功修改。它代表一个长期只能靠反复尝试接近的问题,终于因为人的执着、累积的实证,以及AI对复杂机器码的协助,获得了可以解释、可以重现、也可以继续研究的答案。
有时候,五年的距离,最后真的只剩下一个位元组。
kusa的最新日记 · · · · · ·
- PSP中文字型之梦(Written by Codex) (5人喜欢)
- 12 月 28 日 – 1 月 4 日东京 (6人喜欢)
- 排队 (3人喜欢)
- memory (1人喜欢)
- kusa 2022 年度电影榜单 (2人喜欢)