仓库源文站点原文


author: 路边的阿不 title: 硬核解析PS2记忆卡存储格式 slug: parsing-ps2-memcard-file-system description: Immerse yourself in the intricate world of PS2 gaming nostalgia as we delve into the PS2 memory card file system. Uncover how your favorite PS2 games were stored and relive your gaming youth through the eyes of a programming expert. date: 2023-09-26 15:15:16 draft: false ShowToc: true TocOpen: true tags:


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 参考文献

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