Squashfs文件系统
一、简介
Squashfs的设计是专门为一般的只读文件系统的使用而设计,它可应用于数据备份,或是系统资源紧张的电脑上使用。最初版本的Squashfs采用 gzip 的数据压缩。版本 2.6.34 之后的Linux内核增加了对 LZMA和 LZO压缩算法的支持,版本 2.6.38 的内核增加了对LZMA2的支持,该算法同时也是xz使用的压缩算法。
Squashfs 是一个高度压缩的只读文件系统,它可以将高达 2-3GB 的数据压缩到一个只有 700M 的文件中。
Squashfs 系统支持以回环(loopback)的方式挂载,然后便可以访问其上的文件了,在访问这些文件时,它们就会被解压缩并装载在 RAM 中,而不需要将整个文件解压缩后才去访问其中的文件,这样一来访问速度就快多了。
Squashfs文件系统概述:
- 数据、索引节点和目录被压缩。
- Squashfs存储完整的uid/gid(32位)和文件创建时间。
- 理论上最多支持2^64字节的文件。理论上,文件系统可以最大为2^64字节。
- 索引节点和目录数据高度压缩,并按字节打包边界。每个压缩索引节点的平均长度为8字节(确切长度因文件类型而异,符号链接和块/字符设备索引节点具有不同的大小)。
- Squashfs可以使用高达1MB的块大小(默认大小为128K)。使用128K块实现了比正常更大的压缩比4K块大小。
- 检测并删除文件副本。
二、环境
# 操作系统 Ubuntu 20.04 gcc # firmware-mod-kit https://github.com/rampageX/firmware-mod-kit |
下载firmware-mod-kit源码之后,编译里面自带的文件系统工具,源码中包含很多版本,这里选用比较常见的squashfs-4.2版本为例。
git clone https://github.com/rampageX/firmware-mod-kit
cd firmware-mod-kit/src/others/squashfs-4.2
make
编译好后,在目录出现两个可执行程序mksquashfs(打包)、unsquashfs(解包)。

三、文件系统的打包
这里我们主要是为了了解squashfs文件系统的结构,首先我们来创建一个目录rootfs,里面包含001目录,002.txt文件内容为test002,002.link为001/002.txt的链接,还包含一个003目录,004.txt文件内容为test004,005.txt文件内容为test005,文件树结构如下所示:

使用命令mksquash打包为一个UBIFS文件系统映像:
./mksquashfs rootfs/ rootfs.squashfs -b 131072 -comp xz
常用参数说明:
序号 | 参数 | 说明 |
1 | -b 131072 | 指定块大小131072字节 |
2 | -comp xz | 指定压缩方式xz,通常支持gzip、lzma、xz压缩 |
3 | -be | 大端序打包,这个版本没有这个参数,默认为小端序 |
四、文件系统的结构分析
使用16进制工具查看打包后的文件rootfs.squashfs
00000000 68 73 71 73 07 00 00 00 16 2e 5f 63 00 00 02 00 |hsqs......_c....|
00000010 01 00 00 00 04 00 11 00 c0 04 01 00 04 00 00 00 |................|
00000020 c3 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 |................|
00000030 f8 01 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................|
00000040 86 00 00 00 00 00 00 00 20 01 00 00 00 00 00 00 |........ .......|
00000050 a8 01 00 00 00 00 00 00 ea 01 00 00 00 00 00 00 |................|
00000060 0c 80 00 00 06 00 83 00 40 00 00 00 02 00 74 65 |........@.....te|
00000070 73 74 30 30 32 0a 74 65 73 74 30 30 34 0a 74 65 |st002.test004.te|
00000080 73 74 30 30 35 0a 98 00 fd 37 7a 58 5a 00 00 01 |st005....7zXZ...|
00000090 69 22 de 36 03 c0 5d e3 01 21 01 02 00 00 00 00 |i".6..]..!......|
000000a0 4a 3a 4f f6 e0 00 e2 00 55 5d 00 01 00 16 e5 c9 |J:O.....U]......|
000000b0 26 a4 9a b7 e9 86 66 3d 05 49 8d 45 c8 c6 fc 6a |&.....f=.I.E...j|
000000c0 0a 22 8a a1 78 88 2a 64 70 49 66 c7 2d a8 e0 34 |."..x.*dpIf.-..4|
000000d0 e7 ba 48 70 8b cd ce c2 fe e1 75 d3 63 62 c1 10 |..Hp......u.cb..|
000000e0 ef 6b 39 6e f3 f4 66 d1 50 32 3a 6e e9 66 de 26 |.k9n..f.P2:n.f.&|
000000f0 ac fa 96 42 92 5c 34 4c c3 ff 06 a8 8a 93 bf 80 |...B.\4L........|
00000100 00 00 00 00 a4 75 43 27 00 01 71 e3 01 00 00 00 |.....uC'..q.....|
00000110 d3 d0 e3 d4 3e 30 0d 8b 02 00 00 00 00 01 59 5a |....>0........YZ|
00000120 74 00 fd 37 7a 58 5a 00 00 01 69 22 de 36 02 c0 |t..7zXZ...i".6..|
00000130 43 77 21 01 02 00 6c c3 5e 02 e0 00 76 00 3b 5d |Cw!...l.^...v.;]|
00000140 00 00 6a 7f 7b 11 6e e9 01 34 1e 1a a8 e9 c1 fb |..j.{.n..4......|
00000150 7a 33 6d 6e 0d ca fe 10 3a fb c4 38 7a ec b8 65 |z3mn....:..8z..e|
00000160 10 c2 ba b2 ad e7 42 38 5b 62 15 7b 80 7f 44 63 |......B8[b.{..Dc|
00000170 82 15 14 87 96 7e 76 ea 43 25 9d 4c 00 00 49 ac |.....~v.C%.L..I.|
00000180 6f 50 00 01 53 77 23 5f 71 7f 90 42 99 0d 01 00 |oP..Sw#_q..B....|
00000190 00 00 00 01 59 5a 10 80 6e 00 00 00 00 00 00 00 |....YZ..n.......|
000001a0 18 00 00 01 00 00 00 00 96 01 00 00 00 00 00 00 |................|
000001b0 38 80 20 00 00 00 00 00 00 00 83 00 00 00 00 00 |8. .............|
000001c0 00 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000001d0 00 00 a3 00 00 00 00 00 00 00 63 00 00 00 00 00 |..........c.....|
000001e0 00 00 40 00 00 00 00 00 00 00 b0 01 00 00 00 00 |..@.............|
000001f0 00 00 04 80 e8 03 00 00 f2 01 00 00 00 00 00 00 |................|
00000200 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001000
1、squashfs_super_block
通过分析源码(struct squashfs_super_block),并结合文件,分析结构如下:
序号 | 偏移 | 说明 |
1 | 0x00-0x03 | 标志,字符串hsqs,表示小端序,如果是sqsh则为大端序 |
2 | 0x04-0x07 | inodes数量,值为7 |
3 | 0x08-0x0B | 时间戳,值为0x635f2e16 |
4 | 0x0C-0x0F | block_size,块大小,值为0x20000 |
5 | 0x10-0x13 | fragments,值为1 |
6 | 0x14-0x15 | 压缩方式,值为4表示xz,通常包含gzip:1,lzma:2,gzip(非标):3,xz:4 |
7 | 0x16-0x17 | block_log,值为0x11 |
8 | 0x18-0x19 | flags,值为0x4C0 |
9 | 0x1A-0x1B | no_ids,值为1 |
10 | 0x1C-0x1D | s_major,主版本4 |
11 | 0x1E-0x1F | s_minor,次版本0,即版本号4.0 |
12 | 0x20-0x27 | root_inode,值为0xC3 |
13 | 0x28-0x2F | bytes_used,值为0x200 |
14 | 0x30-0x37 | id_table_start,值为0x1F8 |
15 | 0x38-0x3F | xattr_id_table_start,值为0xFFFFFFFFFFFFFFFF |
16 | 0x40-0x47 | inode_table_start,值为0x86 |
17 | 0x48-0x4F | directory_table_start,值为0x120 |
18 | 0x50-0x57 | fragment_table_start,值为0x1A8 |
19 | 0x58-0x5F | lookup_table_start,值为0x1EA |
2、file data
常规文件由一系列连续的压缩块组成,和/或压缩片段块(尾端压缩块)。压缩的大小每个数据块的文件索引节点。在读取“大”文件(256 MB或更大),代码实现了一个索引缓存,用于缓存来自磁盘上数据块位置的块索引。索引缓存允许Squashfs处理大型文件(高达1.75TiB)在磁盘上保留简单且节省空间的块列表。缓存它被分成多个插槽,缓存多达8个224 GiB文件(128 KiB块)。较大的文件使用多个插槽,1.75 TiB文件使用所有8个插槽。索引缓存设计为内存高效,默认情况下使用16 KiB。
序号 | 偏移 | 说明 |
1 | 0x60-0x85 | 数据内容 |
3、inode_table
元数据(索引节点和目录)被压缩为8K字节的块。每个压缩块的前缀是两个字节的长度,如果块未压缩。如果设置了-noI选项,则块将被解压缩,或者如果压缩块大于未压缩块。
索引节点被打包到元数据块中,并且不与块对齐边界,因此索引节点与压缩块重叠。已识别索引节点通过48位数字编码压缩元数据块的位置包含索引节点,以及索引节点所在块的字节偏移量放置(<block,offset>)。
为了最大化压缩,每个文件类型都有不同的索引节点(常规文件、目录、设备等)、索引节点内容和长度随类型而变化。
为了进一步最大化压缩,两种类型的常规文件索引节点和定义了目录索引节点:针对频繁出现的索引节点进行了优化常规文件和目录,以及额外的扩展类型必须存储信息。
通过分析源码(函数 uncompress_inode_table),并结合文件,分析结构如下:
序号 | 偏移 | 说明 |
1 | 0x86-0x87 | c_byte,说明区段是否压缩和压缩大小,区段为压缩,值0x0098 |
2 | 0x88-0x11F | xz压缩的数据 |
将压缩的数据解压后,如下所示:
00000000 02 00 b4 01 00 00 00 00 ef 2c 5f 63 04 00 00 00 |.........,_c....|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 |................|
00000020 01 00 fd 01 00 00 00 00 ef 2c 5f 63 01 00 00 00 |.........,_c....|
00000030 00 00 00 00 02 00 00 00 1e 00 00 00 03 00 00 00 |................|
00000040 03 00 ff 01 00 00 00 00 ff 2c 5f 63 07 00 00 00 |.........,_c....|
00000050 01 00 00 00 0b 00 00 00 30 30 31 2f 30 30 32 2e |........001/002.|
00000060 74 78 74 02 00 b4 01 00 00 00 00 90 2c 5f 63 06 |txt.........,_c.|
00000070 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 08 |................|
00000080 00 00 00 01 00 fd 01 00 00 00 00 90 2c 5f 63 02 |............,_c.|
00000090 00 00 00 00 00 00 00 02 00 00 00 1e 00 1b 00 03 |................|
000000a0 00 00 00 02 00 b4 01 00 00 00 00 a1 2c 5f 63 05 |............,_c.|
000000b0 00 00 00 00 00 00 00 00 00 00 00 10 00 00 00 08 |................|
000000c0 00 00 00 01 00 fd 01 00 00 00 00 ff 2c 5f 63 03 |............,_c.|
000000d0 00 00 00 00 00 00 00 04 00 00 00 44 00 36 00 08 |...........D.6..|
*
000000e3
这里面记录着文件索引等信息。
4、directory_table
与索引节点(inode)一样,目录被打包成压缩的元数据块,存储在目录表中。使用的起始地址访问目录包含目录和偏移量的元块解压缩块(<block,offset>)。
目录以稍微复杂的方式组织,而不是简单的文件名列表。该组织利用事实上(在大多数情况下)文件的索引节点将位于同一位置压缩元数据块,因此可以共享起始块。因此,目录被组织为两级列表,即目录包含共享起始块值和目录序列的标头每个条目共享共享的起始块。新目录标题写入一次/如果索引节点开始块发生更改。目录头/目录条目列表被重复必要的次数。
目录已排序,可以包含目录索引以加快速度文件查找。目录索引为每个元块存储一个条目,每个条目将索引/文件名映射存储到第一目录头在每个元数据块中。目录按字母顺序排序,并且在查找时,线性扫描索引以查找第一个文件名按字母顺序大于所查找的文件名。此时已找到文件名所在的元数据块的位置。索引的总体思想是确保只需要一个元数据块解压缩以执行查找,而不考虑目录的长度。这种方案的优点是不需要额外的内存开销并且在磁盘上不需要太多额外的存储。
通过分析源码(函数 uncompress_inode_table),并结合文件,分析结构如下:
序号 | 偏移 | 说明 |
1 | 0x120-0x121 | c_byte,说明区段是否压缩和压缩大小,区段为压缩,值0x0074 |
2 | 0x122-0x195 | xz压缩的数据 |
将压缩的数据解压后,如下所示:
00000000 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 |................|
00000010 02 00 06 00 30 30 32 2e 74 78 74 00 00 00 00 00 |....002.txt.....|
00000020 00 00 00 06 00 00 00 63 00 00 00 02 00 06 00 30 |.......c.......0|
00000030 30 34 2e 74 78 74 03 00 00 00 00 00 00 00 01 00 |04.txt..........|
00000040 00 00 20 00 00 00 01 00 02 00 30 30 31 40 00 06 |.. .......001@..|
00000050 00 03 00 07 00 30 30 32 2e 6c 69 6e 6b 83 00 01 |.....002.link...|
00000060 00 01 00 02 00 30 30 33 a3 00 04 00 02 00 06 00 |.....003........|
00000070 30 30 35 2e 74 78 74 |005.txt|
00000077
这里面记录着文件名以及文件所属、权限等信息。
5、fragment_table
常规文件可以包含映射到片段的片段索引磁盘上的位置和压缩大小。这片段查找表本身被压缩存储为元数据块。第二个索引表用于定位这些。第二个索引表访问速度(因为它很小)在装载时读取并缓存在内存中。
通过分析源码(函数 read_fragment_table_4),并结合文件,分析结构如下:
序号 | 偏移 | 说明 |
1 | 0x196-0x197 | c_byte,说明区段是否压缩和压缩大小,值0x8010,这里表示无压缩,总共0x10个字节 |
2 | 0x198-0x19F | fragment[0].start_block,值0x6E |
3 | 0x1A0-0x1A3 | fragment[0].size,值0x1000018 |
4 | 0x1A4-0x1A7 | fragment[0].unused,值0 |
5 | 0x1A8-0x1AF | fragment_table_index,指向fragment_table,值0x196 |
6、export table
使Squashfs文件系统能够导出(通过NFS等)文件系统可以选择(使用-no exports Mksquashfs选项禁用)包含索引节点号到索引节点磁盘位置查找表。这是必需的启用Squashfs将在文件句柄中传递的索引节点号映射到索引节点磁盘上的位置,这在重新验证导出代码时是必需的过期/刷新的索引节点。此表被压缩存储为元数据块。第二个索引表是用于定位这些。第二个索引表用于访问速度(并且因为它很小)在装载时读取并缓存在内存中。
序号 | 偏移 | 说明 |
1 | 0x196-0x1A7 |
7、uid/gid index table
为了节省空间,常规文件存储uid和gid索引使用id查找表转换为32位uid/gid。这张桌子是压缩存储为元数据块。第二个索引表用于找到这些。第二个索引表用于访问速度(因为它小)在装载时读取并缓存在内存中。
通过分析源码(函数 read_uids_guids_4),并结合文件,分析结构如下:
序号 | 偏移 | 说明 |
1 | 0x1F2-0x1F3 | c_byte,说明区段是否压缩和压缩大小,值0x8004,这里表示无压缩,总共4个字节 |
2 | 0x1F4-0x1F7 | id_table,值0x3E8 |
3 | 0x1F8-0x1FF | id_index_table,指向id_table,值0x1F2 |
8、padding
序号 | 偏移 | 说明 |
1 | 0x200-0xFFF | 填充0,4k对齐 |
五、总结
一个squashfs文件系统最多由九个部分组成,按字节对齐方式打包在一起,通常export table、xattr table可能根据需要进行选择,并不是最重要的部分:
---------------
| superblock |
|---------------|
| compression |
| options |
|---------------|
| datablocks |
| & fragments |
|---------------|
| inode table |
|---------------|
| directory |
| table |
|---------------|
| fragment |
| table |
|---------------|
| export |
| table |
|---------------|
| uid/gid |
| lookup table |
|---------------|
| xattr |
| table |
---------------
当从源目录读取文件时,压缩数据块被写入文件系统,并检查是否重复。写入所有文件数据后,将写入完整的索引节点、目录、片段、导出和uid/gid查找表。
六、参考链接
squashfs文件系统 - 多弗朗强哥 - 博客园 (cnblogs.com)
Squashfs 4.0 Filesystem — The Linux Kernel documentation
https://github.com/rampageX/firmware-mod-kit/blob/master/src/others/squashfs-4.2/README