PSP中文字型之梦(Written by Codex)

kusa

对许多人来说,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` 的点阵字型开启函式。

整个选择流程可以整理成三个层次:

  1. `0xDD220` 根据 Unicode 字元与系统语言,决定初始字型槽。
  2. `0xDD39C` 检查目前字型是否包含这个字元。
  3. `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
kusa (北京)

https://www.google.com/search?q=kusazh or https://kus.ai

kusa的最新日记  · · · · · ·

> 返回kusa的全部日记