从保护模式走来,现在开始一步步深入操作系统了。从最简单的 512 字节的引导扇区到现在引入文件系统,从最简单的文件系统 fat12 入手,一步步揭开文件系统的面纱。
创建一个 FAT12 的软盘
使用 dd 创建一个空软盘:
1
|
dd if=/dev/zero of=floppy.img bs=512 count=2880
|
格式化成 FAT12:
1
|
mkfs.fat -F12 floppy.img
|
可选,写入引导扇区:
1
|
dd if=boot.bin of=floppy.img bs=512 count=1 conv=notrunc
|
这时候我们就得到了 fat12 的软盘:floppy.img。
可以挂载到目录里面进行操作:
1
|
sudo mount -o loop -t msdos floppy.img floppy
|
这里挂载到当前目录下的 floppy 目录里。
真实的二进制数据
获得了 floppy.img 后,可以使用 bless 来查看里面的数据:

可以看到一些字符串段的信息,非常明显。
读取 BPB (BIOS Parameter Block)
引导扇区是整个软盘的第 0 个扇区,这里有一个很重要的数据结构叫做 BPB。
引导扇区的格式:
| 偏移量 (十六进制) |
长度 (字节) |
字段名称 |
说明 |
| 0x00 |
3 |
BS_jmpBoot |
跳转指令 |
| 0x03 |
8 |
BS_OEMName |
OEM 名称 |
| 0x0B |
2 |
BPB_BytsPerSec |
每扇区字节数 |
| 0x0D |
1 |
BPB_SecPerClus |
每簇扇区数 |
| 0x0E |
2 |
BPB_RsvdSecCnt |
保留扇区数 |
| 0x10 |
1 |
BPB_NumFATs |
FAT 表数量 |
| 0x11 |
2 |
BPB_RootEntCnt |
根目录最大文件数 |
| 0x13 |
2 |
BPB_TotSec16 |
总扇区数(小于65536时有效) |
| 0x15 |
1 |
BPB_Media |
媒体描述符 |
| 0x16 |
2 |
BPB_FATSz16 |
每FAT表占用扇区数 |
| 0x18 |
2 |
BPB_SecPerTrk |
每磁道扇区数 |
| 0x1A |
2 |
BPB_NumHeads |
磁头数 |
| 0x1C |
4 |
BPB_HiddSec |
隐藏扇区数 |
| 0x20 |
4 |
BPB_TotSec32 |
总扇区数(大于65535时有效) |
| 0x24 |
1 |
BS_DrvNum |
驱动器号 |
| 0x25 |
1 |
BS_Reserved1 |
保留 |
| 0x26 |
1 |
BS_BootSig |
扩展引导标志 |
| 0x27 |
4 |
BS_VolID |
卷序列号 |
| 0x2B |
11 |
BS_VolLab |
卷标 |
| 0x36 |
8 |
BS_FilSysType |
文件系统类型(如 “FAT12 “) |
| 0x3E |
448 |
BootCode |
引导代码 |
| 0x1FE |
2 |
Signature_word |
引导扇区结束标志(0x55AA) |
由此,我们可以创建一个结构体存储这些信息,并读取该扇区的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
typedef struct
{
uint8_t BS_jmpBoot[3]; // 跳转指令
uint8_t BS_OEMName[8]; // OEM 名称
uint16_t BPB_BytsPerSec; // 每扇区字节数 (固定512)
uint8_t BPB_SecPerClus; // 每簇扇区数
uint16_t BPB_RsvdSecCnt; // 保留扇区数 (引导扇区数)
uint8_t BPB_NumFATs; // FAT表个数 (固定2)
uint16_t BPB_RootEntCnt; // 根目录最大文件数
uint16_t BPB_TotSec16; // 总扇区数
uint8_t BPB_Media; // 介质类型 (0xF0 软盘)
uint16_t BPB_FATSz16; // 一个FAT表占的扇区数
uint16_t BPB_SecPerTrk; // 每磁道扇区数
uint16_t BPB_NumHeads; // 磁头数
uint32_t BPB_HiddSec; // 隐藏扇区数
uint32_t BPB_TotSec32; // 总扇区数
uint8_t BS_DrvNum; // 驱动器号
uint8_t BS_Reserved1; // 保留位
uint8_t BS_BootSig; // 扩展引导标志
uint32_t BS_VolID; // 卷序列号
uint8_t BS_VolLab[11]; // 卷标
uint8_t BS_FileSysType[8];// 文件系统类型
uint8_t BootCode[448]; // 引导代码
uint16_t Signature_word; // 引导扇区结束标志
} __attribute__((packed)) BPB;
|
BPB 结构体清晰的写出了 fat12 引导扇区的数据,并使用 __attribute__((packed)) 取消结构在编译过程的优化对齐。
在 main 函数中读取软盘:
1
2
3
4
5
6
7
|
FILE *fp = fopen("floppy.img", "rb");
BPB bpb;
unsigned char sector[512];
// 读取引导扇区
fread(sector, 512, 1, fp);
memcpy(&bpb, sector, sizeof(BPB));
|
先读取到 sector 中,再使用 memcpy 复制 BPB 大小的数据防止栈溢出。
这样 bpb 这个结构体就真实保存了软盘第一个扇区的数据。
1
2
3
4
5
6
7
8
9
|
printf("每扇区字节数: %d\n", bpb.BPB_BytsPerSec);
printf("厂商名:%.8s\n", bpb.BS_OEMName);
printf("FAT表个数: %d\n", bpb.BPB_NumFATs);
printf("根目录最大文件数:%d\n", bpb.BPB_RootEntCnt);
printf("一个FAT表扇区数: %d\n", bpb.BPB_FATSz16);
printf("卷标:%.11s\n", bpb.BS_VolLab);
printf("文件系统类型:%.8s\n", bpb.BS_FileSysType);
printf("引导代码:%s\n", bpb.BootCode + 29);
printf("结束标志:0x%x\n", bpb.Signature_word);
|
输出:
1
2
3
4
5
6
7
8
9
10
11
|
每扇区字节数: 512
厂商名:mkfs.fat
FAT表个数: 2
根目录最大文件数:224
一个FAT表扇区数: 9
卷标:FLOPPY
文件系统类型:FAT12
引导代码:This is not a bootable disk. Please insert a bootable floppy and
press any key to try again ...
结束标志:0xaa55
|
和我们在 bless 看到的数据是一样的,到这我们就成功读取了 fat12 的引导扇区数据。
参考资料
- Youtube Building an OS - 3 - The FAT file system
- 自己动手写操作系统 作者:于渊
- OSDev.org FAT