学习FAT系列文件系统

Last Updated: 2023-06-26 09:26:11 Monday

-- TOC --

理解了分区,接下来自然就到了格式化文件系统了。而学习文件系统的开始,一定是FAT系列。它年代最久,最早可追溯到比尔盖茨发明BASIC语言的时候(FAT8),也最简单,兼容性最好。

Windows系统进行文件操作时并非以扇区为单位,而是簇(cluster),簇是系统进行硬盘空间读写的最小单位,一个簇可以包含多个扇区。(现在硬盘扇区的实际size不再是512Bytes,参考:4K对齐

FAT文件系统整体结构如下:

BPB|Info|Backup|            
         Reserved            |  FAT  |  Data

BPB,info,backup这些扇区都包含在reserved区域中。

BPB

FAT分区的第一块扇区,512字节,叫做BPB,Bios Parameter Block,常常称BPB为FAT文件系统的引导扇区。

从Grub项目的fat.h中查到bpb的结构:

struct grub_fat_bpb
{
  grub_uint8_t jmp_boot[3];
  grub_uint8_t oem_name[8];
  grub_uint16_t bytes_per_sector;
  grub_uint8_t sectors_per_cluster;
  grub_uint16_t num_reserved_sectors;
  grub_uint8_t num_fats;        /* 0x10 */
  grub_uint16_t num_root_entries;
  grub_uint16_t num_total_sectors_16;
  grub_uint8_t media;           /* 0x15 */
  grub_uint16_t sectors_per_fat_16;
  grub_uint16_t sectors_per_track;  /* 0x18 */
  grub_uint16_t num_heads;      /* 0x1A */
  grub_uint32_t num_hidden_sectors; /* 0x1C */
  grub_uint32_t num_total_sectors_32;   /* 0x20 */
  union
  {
    struct
    {
      grub_uint8_t num_ph_drive;
      grub_uint8_t reserved;
      grub_uint8_t boot_sig;
      grub_uint32_t num_serial;
      grub_uint8_t label[11];
      grub_uint8_t fstype[8];
    } GRUB_PACKED fat12_or_fat16;
    struct
    {
      grub_uint32_t sectors_per_fat_32;
      grub_uint16_t extended_flags;
      grub_uint16_t fs_version;
      grub_uint32_t root_cluster;
      grub_uint16_t fs_info;
      grub_uint16_t backup_boot_sector;
      grub_uint8_t reserved[12];
      grub_uint8_t num_ph_drive;
      grub_uint8_t reserved1;
      grub_uint8_t boot_sig;
      grub_uint32_t num_serial;
      grub_uint8_t label[11];
      grub_uint8_t fstype[8];
    } GRUB_PACKED fat32;
  } GRUB_PACKED version_specific;
} GRUB_PACKED;

下面是读取U盘中一块FAT32分区的BPB扇区的内容:

$ sudo xxd -a -u -l512 /dev/sdc1
00000000: EB58 906D 6B66 732E 6661 7400 0220 2000  .X.mkfs.fat..  .
00000010: 0200 0000 00F8 0000 2000 4000 0008 0000  ........ .@.....
00000020: 00D0 9403 6039 0000 0000 0000 0200 0000  ....`9..........
00000030: 0100 0600 0000 0000 0000 0000 0000 0000  ................
00000040: 8000 29CD FA6B 824E 4F20 4E41 4D45 2020  ..)..k.NO NAME
00000050: 2020 4641 5433 3220 2020 0E1F BE77 7CAC    FAT32   ...w|.
00000060: 22C0 740B 56B4 0EBB 0700 CD10 5EEB F032  ".t.V.......^..2
00000070: E4CD 16CD 19EB FE54 6869 7320 6973 206E  .......This is n
00000080: 6F74 2061 2062 6F6F 7461 626C 6520 6469  ot a bootable di
00000090: 736B 2E20 2050 6C65 6173 6520 696E 7365  sk.  Please inse
000000a0: 7274 2061 2062 6F6F 7461 626C 6520 666C  rt a bootable fl
000000b0: 6F70 7079 2061 6E64 0D0A 7072 6573 7320  oppy and..press
000000c0: 616E 7920 6B65 7920 746F 2074 7279 2061  any key to try a
000000d0: 6761 696E 202E 2E2E 200D 0A00 0000 0000  gain ... .......
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55AA  ..............U.

最后这两个数字,看起来像似跟下面显示出来的数字对应的:

Device     Start      End  Sectors  Size Type
/dev/sdc1   2048 60086271 60084224 28.7G Linux filesystem

接着解析union.fat32:

Windows系统查看U盘此分区的总容量,刚好等于:

>>> # (总扇区数 - 保留扇区数 - 两张FAT表所用扇区数) * 512
>>> (60084224-32-14688*2)*512
30748065792

在Windows系统查看小文件的空间占用情况,大小是实际字节数,而占用空间,是cluster size的倍数。

fs_info

紧挨着BPB(0号),就是fs_info所在的1号扇区。fs_info扇区也有备份,在BPB备份扇区后面,默认是7。fs_info里面记录了FAT文件系统的两个重要信息:

$ sudo xxd -a -u -s512 -l512 /dev/sdc1
00000200: 5252 6141 0000 0000 0000 0000 0000 0000  RRaA............
00000210: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000003e0: 0000 0000 7272 4161 75A1 1C00 7601 0000  ....rrAau...v...
000003f0: 0000 0000 0000 0000 0000 0000 0000 55AA  ..............U.

做个算术题:

>>> (60084224-32-14688*2)/32  # total cluster number
1876713.0
>>> 1876341+374-2
1876713

0号和1号,只在FAT表中存在,没有真实的cluster对应。

FAT表(File Allocation Table)

保留扇区之后,就是两个一模一样的FAT表。

FAT表内存放的,是每个cluster的使用情况,用4个字节表示,即每4字节表示一个cluster,依然是little endian:

$ sudo xxd -a -u -s$((512*32)) -l32 /dev/sdc1
00004000: F8FF FF0F FFFF FFFF F8FF FF0F FFFF FF0F  ................
00004010: FFFF FF0F FFFF FF0F 2101 0000 FFFF FF0F  ........!.......

备份FAT拥有完全一样的值:

$ sudo xxd -a -u -s$((512*(32+14688))) -l32 /dev/sdc1
00730000: F8FF FF0F FFFF FFFF F8FF FF0F FFFF FF0F  ................
00730010: FFFF FF0F FFFF FF0F 2101 0000 FFFF FF0F  ........!.......

下面来做一道小学数学题:

>>> 14688*512//4
1880064
>>> 1880064-1876713
3351

FAT表可以表达的cluster数量,超过了总的cluster数量,超的还不少...?

数据区(短文件名Entry)

跳过保留区和两个FAT表的区域,就是数据区了。

$ sudo mount /dev/sdd1 /mnt/u
$ ll /mnt/u
total 224K
drwxr-xr-x. 5 root root  16K Jan  1  1970  .
drwxr-xr-x. 1 root root    8 Jun  1 09:44  ..
drwxr-xr-x. 3 root root  16K Jun  4 20:28  EFI
drwxr-xr-x. 4 root root  16K Jun  4 18:33  grub
-rwxr-xr-x. 1 root root 136K Jun  4 18:33  grubx64.efi
drwxr-xr-x. 2 root root  16K Jun  6 13:57 'System Volume Information'
-rwxr-xr-x. 1 root root    6 Jun  6 16:01  test.txt
$ sudo xxd -a -u -s$((512*(32+14688*2))) -l512 /dev/sdd1
00e5c000: 4167 0072 0075 0062 0000 000F 008F FFFF  Ag.r.u.b........
00e5c010: FFFF FFFF FFFF FFFF FFFF 0000 FFFF FFFF  ................
00e5c020: 4752 5542 2020 2020 2020 2010 0017 2C94  GRUB       ...,.
00e5c030: C456 C456 0000 2D94 C456 0300 0000 0000  .V.V..-..V......
00e5c040: 4546 4920 2020 2020 2020 2010 0026 2C94  EFI        ..&,.
00e5c050: C456 C456 0000 8EA3 C456 0400 0000 0000  .V.V.....V......
00e5c060: 4167 0072 0075 0062 0078 000F 0067 3600  Ag.r.u.b.x...g6.
00e5c070: 3400 2E00 6500 6600 6900 0000 0000 FFFF  4...e.f.i.......
00e5c080: 4752 5542 5836 3420 4546 4920 0072 2D94  GRUBX64 EFI .r-.
00e5c090: C456 C756 0000 2D94 C456 6B01 0020 0200  .V.V..-..Vk.. ..
00e5c0a0: 4220 0049 006E 0066 006F 000F 0072 7200  B .I.n.f.o...rr.
00e5c0b0: 6D00 6100 7400 6900 6F00 0000 6E00 0000  m.a.t.i.o...n...
00e5c0c0: 0153 0079 0073 0074 0065 000F 0072 6D00  .S.y.s.t.e...rm.
00e5c0d0: 2000 5600 6F00 6C00 7500 0000 6D00 6500   .V.o.l.u...m.e.
00e5c0e0: 5359 5354 454D 7E31 2020 2016 0031 3A6F  SYSTEM~1   ..1:o
00e5c0f0: C656 C656 0000 3B6F C656 7501 0000 0000  .V.V..;o.Vu.....
00e5c100: 4174 0065 0073 0074 002E 000F 008F 7400  At.e.s.t......t.
00e5c110: 7800 7400 0000 FFFF FFFF 0000 FFFF FFFF  x.t.............
00e5c120: 5445 5354 2020 2020 5458 5420 0079 3080  TEST    TXT .y0.
00e5c130: C656 C756 0000 3080 C656 7901 0600 0000  .V.V..0..Vy.....
00e5c140: E52E 0074 0065 0073 0074 000F 00A1 2E00  ...t.e.s.t......
00e5c150: 7400 7800 7400 2E00 7300 0000 7700 7000  t.x.t...s...w.p.
00e5c160: E545 5354 5458 7E31 5357 5020 0016 2F80  .ESTTX~1SWP ../.
00e5c170: C656 C656 0000 2F80 C656 7801 0010 0000  .V.V../..Vx.....
00e5c180: E574 0065 0073 0074 002E 000F 008D 7400  .t.e.s.t......t.
00e5c190: 7800 7400 7E00 0000 FFFF 0000 FFFF FFFF  x.t.~...........
00e5c1a0: E545 5354 7E31 2020 5458 5420 0079 3080  .EST~1  TXT .y0.
00e5c1b0: C656 C656 0000 3080 C656 0000 0000 0000  .V.V..0..V......
00e5c1c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00e5c1f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

2号root cluster紧挨在两张FAT表后面。

root cluster是分区根目录,目录中的数据,按32字节划分entry。这32自己的结构如下:

struct grub_fat_dir_entry
{
  grub_uint8_t name[11];
  grub_uint8_t attr;
  grub_uint8_t nt_reserved;
  grub_uint8_t c_time_tenth;
  grub_uint16_t c_time;
  grub_uint16_t c_date;
  grub_uint16_t a_date;
  grub_uint16_t first_cluster_high;
  grub_uint16_t w_time;
  grub_uint16_t w_date;
  grub_uint16_t first_cluster_low;
  grub_uint32_t file_size;
} GRUB_PACKED;

用这两行(32字节)数据来解释:

00e5c120: 5445 5354 2020 2020 5458 5420 0079 3080  TEST    TXT .y0.
00e5c130: C656 C756 0000 3080 C656 7901 0600 0000  .V.V..0..Vy.....

两部分cluster拼在一起为,0x00000179,即377,直接看看377号cluster的内容:

$ sudo xxd -a -u -s$((512*(32+14688*2)+32*512*(377-2))) -l512 /dev/sdd1
01438000: 6961 6161 610A 0000 0000 0000 0000 0000  iaaaa...........
01438010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
014381f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

长度刚好是6个字节!

FAT表的377号为:

$ sudo xxd -a -u -s$((512*32+377*4)) -l4 /dev/sdd1
000045e4: FFFF FF0F                                ....

0x0FFFFFFF,表示这是last cluster。如果文件size超过了一个cluster的容量,这个位置记录的就是next cluster,以此类推。

属性

1字节,6个属性:

创建时间为0x8030,二进制为1000 0000 0011 0000,表示时间(加上毫秒进位)16时1分17秒(210毫秒):

创建日期为0x56C6,二进制为0101 0110 1100 0110,表示日期2023年6月6日:

最后访问日期的计算与创建日期相同,最后修改时间最后修改日期的计算同上。

当我在root cluster中touch一个新的空文件t2后,能看到出现了一个新的entry:

00e5c160: 5432 2020 2020 2020 2020 2020 00C7 D46D  T2          ...m
00e5c170: C756 C756 0000 D46D C756 0000 0000 0000  .V.V...m.V......

size为0,也cluster全0。

文件夹Entry

下面是grub目录的entry数据:

00e5c020: 4752 5542 2020 2020 2020 2010 0017 2C94  GRUB       ...,.
00e5c030: C456 C456 0000 2D94 C456 0300 0000 0000  .V.V..-..V......

size为0,cluster是0x00000003,即3号cluster(紧挨着FAT后面的是2号cluster):

$ ll /mnt/u/grub
total 96K
drwxr-xr-x. 4 root root  16K Jun  4 18:33 .
drwxr-xr-x. 5 root root  16K Jan  1  1970 ..
drwxr-xr-x. 2 root root  16K Jun  4 18:33 fonts
-rwxr-xr-x. 1 root root 1.0K Jun  4 18:33 grubenv
drwxr-xr-x. 2 root root  32K Jun  4 18:33 x86_64-efi
$ sudo xxd -a -u -s$((512*(32+14688*2)+512*32*(3-2))) -l512 /dev/sdd1
00e60000: 2E20 2020 2020 2020 2020 2010 0017 2C94  .          ...,.
00e60010: C456 C456 0000 2C94 C456 0300 0000 0000  .V.V..,..V......
00e60020: 2E2E 2020 2020 2020 2020 2010 0017 2C94  ..         ...,.
00e60030: C456 C456 0000 2C94 C456 0000 0000 0000  .V.V..,..V......
00e60040: 4178 0038 0036 005F 0036 000F 00CF 3400  Ax.8.6._.6....4.
00e60050: 2D00 6500 6600 6900 0000 0000 FFFF FFFF  -.e.f.i.........
00e60060: 5838 365F 3634 7E31 2020 2010 0027 2C94  X86_64~1   ..',.
00e60070: C456 C456 0000 2D94 C456 0600 0000 0000  .V.V..-..V......
00e60080: 4166 006F 006E 0074 0073 000F 009A 0000  Af.o.n.t.s......
00e60090: FFFF FFFF FFFF FFFF FFFF 0000 FFFF FFFF  ................
00e600a0: 464F 4E54 5320 2020 2020 2010 0029 2D94  FONTS      ..)-.
00e600b0: C456 C456 0000 2D94 C456 5701 0000 0000  .V.V..-..VW.....
00e600c0: E567 0072 0075 0062 0065 000F 002D 6E00  .g.r.u.b.e...-n.
00e600d0: 7600 2E00 6E00 6500 7700 0000 0000 FFFF  v...n.e.w.......
00e600e0: E552 5542 454E 5620 4E45 5720 0029 2D94  .RUBENV NEW .)-.
00e600f0: C456 C456 0000 2D94 C456 5801 0004 0000  .V.V..-..VX.....
00e60100: 4167 0072 0075 0062 0065 000F 00D8 6E00  Ag.r.u.b.e....n.
00e60110: 7600 0000 FFFF FFFF FFFF 0000 FFFF FFFF  v...............
00e60120: 4752 5542 454E 5620 2020 2020 0029 2D94  GRUBENV     .)-.
00e60130: C456 C756 0000 2D94 C456 5801 0004 0000  .V.V..-..VX.....
00e60140: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00e601f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

长文件名Entry

上一节的数据,多少看着有些怪异,那是因为除了存在短文件名entry之外,还有长文件名entry。这是FAT32增加的特性。

struct grub_fat_long_name_entry
{
  grub_uint8_t id;
  grub_uint16_t name1[5];
  grub_uint8_t attr;
  grub_uint8_t reserved;
  grub_uint8_t checksum;
  grub_uint16_t name2[6];
  grub_uint16_t first_cluster;
  grub_uint16_t name3[2];
} GRUB_PACKED;

同样是32字节。

FAT32文件系统,长短文件名共存!

为了兼容低版本的OS或程序能正确读取长文件名文件,系统自动为所有长文件名文件创建了一个对应的短文件名,使对应数据既保存长文件名,也保存短文件名。不支持长文件名的OS或程序会忽略它认为不合法的长文件名Entry,而支持长文件名的OS或程序则会以长文件名为显式项来记录和编辑,并隐藏起短文件名。(测试发现,创建短文件名时,也会同时增加长文件名对应的entry。)

分析下面这段Entry数据:

00e5c0a0: 4220 0049 006E 0066 006F 000F 0072 7200  B .I.n.f.o...rr.
00e5c0b0: 6D00 6100 7400 6900 6F00 0000 6E00 0000  m.a.t.i.o...n...
00e5c0c0: 0153 0079 0073 0074 0065 000F 0072 6D00  .S.y.s.t.e...rm.
00e5c0d0: 2000 5600 6F00 6C00 7500 0000 6D00 6500   .V.o.l.u...m.e.
00e5c0e0: 5359 5354 454D 7E31 2020 2016 0031 3A6F  SYSTEM~1   ..1:o
00e5c0f0: C656 C656 0000 3B6F C656 7501 0000 0000  .V.V..;o.Vu.....

这段数据一共包含3个entry项,前2个是长名Entry,后1个是短名Entry。完成的名称是System Volume Information。这3项Entry,只对应了一个文件(夹)。

长名Entry里面都是名字的unicode编码,没啥具体信息,具体信息还是要分析最后的短名Entry。

checksum

checksum的作用,是确保长Entry与短Entry是对应起来的,checksum的值通过短Entry中11个字节的name计算出来,计算方法如下:

#include <stdio.h>

int main(void) {
    unsigned char cks = 0;
    char *name = "SYSTEM~1   ";
    for(int i=0; i<11; ++i)
        cks = ((cks>>1) | (cks<<7)) + name[i];
    printf("0x%x\n", cks);
    return 0;
}

输出:0x72,与上面的数据对应!两个长Entry的checksum都是72。

文件的名称大小写和权限

看出来了吧,FAT32文件系统中,短名全是大写!长名才分大小写!

Windows系统文件路径不区分大小写的由来,恐怕就是这里。

FAT32文件系统,没有任何权限相关的信息。

mkfs.fat命令

参考:mkfs.fat命令

至此,FAT系列文件系统学习到这里,就差不多了,基本上主要关注FAT32。

直接写入字节到扇区

读取扇区,就是fopen,正常读,sudo执行。下来尝试一下如何直接写入字节到扇区。

在FAT32分区中,创建一个文件,名abc,内容1234567890

下面是此文件的Entry,一长一短:

00204060: 4161 0062 0063 0000 00FF FF0F 0074 FFFF  Aa.b.c.......t..
00204070: FFFF FFFF FFFF FFFF FFFF 0000 FFFF FFFF  ................
00204080: 4142 4320 2020 2020 2020 2020 0068 A248  ABC         .h.H
00204090: C956 C956 0000 A248 C956 0700 0B00 0000  .V.V...H.V......

此文件存放的cluster号为0x00000007,即7号。文件长度0x0000000B,即10Bytes。

确认一下位置:

$ sudo xxd -a -u -s$((512*(32+2048*2)+4096*(7-2))) -l16 /dev/sdc1
00209000: 3132 3334 3536 3738 3930 0A00 0000 0000  1234567890......

没错,就是这里。现在开始尝试直接修改这部分,用sudo进Python Shell:

>>> f = open('/dev/sdc1', 'rb+')  # rb+
>>> offset = 512*(32+2048*2) + 4096*(7-2)
>>> f.seek(offset)
2134016
>>> f.read(10)
b'1234567890'   # nice
>>> f.seek(offset)
2134016
>>> f.write(b'555')
3
>>> f.close()

rb+打开设备文件,可读可写。检查写入效果:

$ sudo xxd -a -u -s$((512*(32+2048*2)+4096*(7-2))) -l16 /dev/sdc1
00209000: 3535 3534 3536 3738 3930 0A00 0000 0000  5554567890......

Nice....:)

如果用cat命令查看abc文件内容,注意有缓存,重新mount之后再看。

删除文件的本质

接着上面一节,继续测试,现在将abc文件用rm命令删除,查看Entry:

00204060: E561 0062 0063 0000 00FF FF0F 0074 FFFF  .a.b.c.......t..
00204070: FFFF FFFF FFFF FFFF FFFF 0000 FFFF FFFF  ................
00204080: E542 4320 2020 2020 2020 2020 0068 A248  .BC         .h.H
00204090: C956 C956 0000 A248 C956 0700 0B00 0000  .V.V...H.V......

变化的仅仅是两个Entry的第1个字节,都变成了0xE5

文件内容部分也没有变化:

$ sudo xxd -a -u -s$((512*(32+2048*2)+4096*(7-2))) -l16 /dev/sdc1
00209000: 3535 3534 3536 3738 3930 0A00 0000 0000  5554567890......

查看FAT表,全0,已是空闲状态:

$ sudo xxd -a -u -s$((512*32+4*7)) -l4 /dev/sdc1
0000401c: 0000 0000  

这就是删除文件的本质,只是在Entry上设置了一个标志(注意:不同文件系统,这里的细节肯定会不一样)。了解了删除的本质,也就理解了,有一些文件是可以恢复的。

恢复被删除的文件

恢复的过程可以这样:

但是,如果被删除的文件很长,占用了好多个cluster,只有第1个cluster可以顺利恢复出来。其它的cluster位置信息已经丢失了。此时,可以尝试通过文件size来计算一共占用了多少cluster,也许下一个空闲cluster就是(连续存储)。(从这个角度来看,扩大cluster的size还有点意义)

恢复数据不一定需要写入硬盘扇区,完全可以将恢复出来的数据存在别处。

本文链接:https://cs.pynote.net/hd/hdisk/202306061/

-- EOF --

-- MORE --