仓库源文站点原文

01 前言

作为一个80后的游戏老玩家,PS2游戏机在我心中一直有着特殊的地位。时至今日,已经过去了20多年,然而,最近我因为模拟器的缘故重新接触到了它。在重温了一段时间游戏后,我突发奇想,能否通过现在的知识来回忆年少时的自己?于是,我开始了这一系列文章的创作,从分析PS2存储卡的文件系统开始,逐步深入的解析其文件存储机制及每个游戏的存档文件。我的目标是,最终通过Python和OpenGL,模拟出游戏存档中经典的3D人物旋转效果,以此来纪念这个曾经陪伴我度过青春时光的经典游戏机。

这是系列的第一篇作品,解析PS2存储卡的文件系统。

02 词汇表

03 文件系统结构

注:这里用标准的8M存储卡举例。

3.1 数据结构

从"超级块"中可得知"页"的大小是512字节,"簇"的大小是2个"页"。spare area可以根据公式(page_len / 128) * 4得到,是16字节,则文件系统基本数据结构如图:

3.2 逻辑结构

了解了最基本的数据结构,接下来我们划分一下存储卡的逻辑结构。如下图,一块存储卡大致能分为以下几个逻辑区块。(黑白部分本文不涉及,可以忽略。)注意:组成逻辑区块的最小单位是簇。

超级块

位于整个文件开头(也就是第一个簇)的前340个字节,这是文件系统中唯一具有固定位置的部分。下图示意了一个存储卡文件的超级块。

注:PS2存储卡的字节序是小端序Little-endian。

Offset Name Length Default Description
0 magic byte[28] - 固定字符串"Sony PS2 Memory Card Format", 表明该卡已成功初始化
28 version byte[12] 1.X.0.0 版本号
40 page_len uint16 512 page的大小(以字节为单位)
42 pages_per_cluster uint16 2 簇中的页数
44 pages_per_block uint16 16 块中的页数
46 - uint16 0xFF00 未知
48 clusters_per_card uint32 8192 卡的总大小(以簇为单位)
52 alloc_offset uint32 41 第一个可分配簇
56 alloc_end uint32 8135 最后一个可分配簇
60 rootdir_cluster uint32 0 根目录的第一个簇,相对于alloc_offset
64 backup_block1 uint32 1023 本文无用
68 backup_block2 uint32 1022 本文无用
80 ifc_list uint32[32] 8 间接 FAT 簇列表,在标准 8M 卡上只有一个间接 FAT 簇
208 bad_block_list uint32[32] -1 本文无用
336 card_type byte 2 必须是2,说明这是一张PS2存储卡
337 card_flags byte 0x52 存储卡的物理特性

字段page_lenpages_per_clusterpages_per_blockcluster_per_card定义文件系统的基本几何结构。可以使用ifc_list访问FATrootdir_cluster给出根目录的第一个簇。FAT和目录项中的簇偏移量都与alloc_offset相关。

FAT

文件分配表是一个链表,当你找到一个文件的起始簇时,你想象有两个线程,线程x用来读取这个簇里的内容(即数据),线程y去FAT里寻找下一个簇,交由x读取,然后不断循环,当然两个线程不是必须的。这里引用一张图说明一下这种工作方式:

图片来源:https://www.slideserve.com/yahto/file-system-implementation

直接FAT

由前文可以得知,直接FAT和间接FAT都是保存在簇里的。簇里的数据必须有一个良好的结构,才能使我们简单的解析成FAT链表。FAT在簇里的结构可以想象成长这样:

这是一个矩阵M,行定义为FAT所在的簇,列定义为每个FAT簇里的数据。每个FAT簇,保存的都是4字节32位的整形数组,数量为1024 / 4 = 256个,因此矩阵有256列。FAT一共有多少个簇呢?这点可以在间接FAT的簇中解析出来,我们之后再讲。在这里FAT一共占据了32个簇,因此矩阵有32行。

M矩阵的大小为32 * 256 = 8192,意味着这个FAT可以管理8192个簇。假设现在要找簇n在矩阵中的位置rowcolumn,可以根据简单的计算得出:

row = (n // 256) % 256
column = n % 256

既然已经计算出了位置,那就可以取到对应的值了,没错,这个值?就是下一个簇。通过不断循环,直到取到的值为0xFFFFFFFF,表示簇链到结尾了,不需要再查找了。

注:FAT表里储存的值为32位,最高位为8代表正常使用的簇,其它值代表簇未分配,最高位为8时,取低31位的整形值。值为0xFFFFFFFF代表已是簇链末尾。

间接FAT

前文留了一个问题,为什么FAT占有了32个簇?

在超级块中有一个字段ifc_list,是一个4字节32位的整形数组,再想象一下上面出现的矩阵。ifc_list是一个只有一行的矩阵,虽然它有32个元素,但只有第一个有值,其值8即间接FAT簇ifc。将簇8的内容按照上文的方法解析出来,再形成一个矩阵,行是ifc_list的个数,理论上是32,但由于只有1个元素,因此这个矩阵的行也为1。矩阵的列依然是256。解析其中的值,可以得到FAT所在的簇为9到40,即32个。

可分配簇

是一个范围,从alloc_offset开始到alloc_end结束。除去超级块、FAT、保留簇等的位置,所有的游戏存档都位于可分配簇内。

04 文件和目录

接着我们要研究下可分配簇里,每个簇都保存了些什么东西?简单来说,可分配簇里只有两种簇:“条目簇”和“数据簇”。保存条目的簇称为“条目簇”,保存数据的簇称为“数据簇”。

4.1 条目

每个目录或文件都有一个“条目”,可以看作是元数据,保存有文件名、大小、创建和修改时间等属性。每个“条目”的长度为 512 字节,因此每个 1024 簇中只能容纳两个“条目”。“条目簇”不会保存文件数据,即使“条目簇”里只有一个“条目”。

除了根目录没有root这个“条目”外,每个目录都有以自己的目录名命名的“条目”,每个文件也有以自己的文件名命名的“条目”,“条目”的结构如下表:

Offset Name Length Description
0 mode uint16 标识该文件的属性
4 length uint32 如果是文件,以字节为单位;如果是目录,以项为单位。
8 created byte[8] 创建时间
16 cluster uint32 条目对应的第一个簇,是相对于alloc_offset的相对值。
20 dir_entry uint32 无用
24 modified byte[8] 修改时间
32 attr uint32 用户属性
36 name byte[32] 文件名,x00以后的需截断

05 结尾

至此,相信大家对一个ps2存储文件有了大致认识了吧。有兴趣的可以自己写一个程序解析下了。稍后我也会创建一个项目,附上本篇文章涉及的源代码。

下一篇文章我们将开始把游戏存档从存储卡里导出来,看看每个游戏存档都有哪些文件。

06 参考文献

本文主要参考了如下文章,在此表示感谢🙏: