FAT12文件系统遍历实验:从磁盘布局到实模式内核实现
2026/9/23 14:23:27 网站建设 项目流程

很多人写 OS 卡在文件系统这一步,其实 FAT12 是最适合入门的那一个

如果你正在跟着各类 OS 开发教程写一个实模式下的 kernel,大概率已经经历过这样的阶段:先搞定引导扇区,再学会加载 loader,接着终于能在屏幕上输出字符了。正当你以为自己已经摸到操作系统的门槛时,下一步往往就是当头一棒——文件系统。

我见过很多初学者在这个地方放弃。原因很简单:文件系统不像中断、分页那样是一个“单点”知识,它是一整套存储规则的集合。你不仅要理解磁盘的结构,还要理解目录项、簇链、根目录区、数据区之间的协作关系,任何一个细节没对上,结果都是读取失败。而更难受的是,失败往往没有报错,只能靠肉眼盯二进制。

这篇文章想聊的是 FAT12,准确说是 FAT12 文件系统下“遍历文件”的基础实验。选择 FAT12 而不是 FAT16 或 FAT32,不是因为它们不重要,而是对于实模式下的 kernel 开发来说,FAT12 是结构最简单、最容易被完整实现的文件系统。它小到可以用一张 1.44MB 软盘完整容纳,小到 BPB、根目录、FAT 表、数据区这些概念可以用一个实验全部串起来。

读完这篇文章,你会理解 FAT12 的磁盘布局、目录项结构、FAT 表链式关系,并完成一次基于实模式 kernel 的 FAT12 文件遍历实验。这篇文章不是纯理论,也不是纯贴代码,而是把“为什么这么设计”和“代码怎么写”放在一起讲清楚。

1. 这篇文章真正要解决的问题

先给一个明确判断:FAT12 文件遍历,是理解“操作系统如何读取磁盘上文件”的最好入口,也是从“能启动”走向“能用文件系统”的关键台阶。

很多教程会教你写引导扇区,也会教你加载 kernel,但文件系统的部分往往一笔带过。结果就是,很多人写的内核其实根本不会读文件,所有的资源、字符串、配置只能硬编码到内核镜像里。这在实际开发中是不可接受的。只要你的内核需要从磁盘加载用户程序、读取配置文件、访问资源文件,就必须引入文件系统。

这篇文章要解决的问题有四个:

  1. FAT12 磁盘布局到底是什么样的。这是基础中的基础。引导扇区、FAT 表、根目录区、数据区,各自在哪个位置、占多大空间、如何计算。
  2. 目录项是怎么描述一个文件的。8.3 文件名、文件大小、起始簇号、属性字节,这些字段分别表示什么。
  3. FAT 表如何形成文件内容的链式结构。文件不一定是连续存放的,FAT 表项记录了下一个簇的编号,这是理解“遍历”的重点。
  4. 如何用实模式代码把上面的规则落地。读取根目录区、解析目录项、读取 FAT 表项、按簇链读取文件内容,完成一次完整的文件遍历实验。

听起来似乎难度不小,但 FAT12 的好处在于:它足够小,小到可以用一个实验跑通全流程;它也足够经典,FAT12 的设计思想在 FAT16、FAT32 甚至 exFAT 中都有延续。把 FAT12 彻底搞懂,后面再接触其他文件系统会轻松很多。

这篇文章适合的读者是有一定汇编基础、已经在做或准备做 OS 开发的初学者。如果你已经能写出一个能加载 kernel 的引导扇区,那就更好了,你会在这篇文章里找到下一步该做的事。

2. FAT12 文件系统的核心概念与适用场景

2.1 为什么偏偏是 FAT12

FAT 是 File Allocation Table 的缩写,中文通常叫“文件分配表”。FAT 系列文件系统的基本思想是用一张表来记录磁盘上每个簇的占用情况和文件数据的链式关系。FAT12 后面的“12”表示 FAT 表项宽度是 12 位,也就是 1.5 字节。

FAT12 最开始是为软盘设计的,最经典的场景就是 1.44MB 3.5 英寸软盘。一张软盘的容量就那么大,所以 FAT12 的设计目标很纯粹:简单、够用、容易实现。它不需要支持超大分区、不需要复杂的权限模型、不需要日志,连文件名都是 8.3 格式。

在实模式 kernel 开发中,FAT12 几乎是新手的“模块化实验平台”。你在虚拟机里挂一个软盘镜像,然后让内核读取镜像里的文件并回调输出。这个过程不需要考虑 IDE、SATA、USB 等复杂硬件驱动,因为我们可以直接用 BIOS 中断读取磁盘扇区。这样,文件的物理读取其实还是由 BIOS 完成,你的 kernel 只需要按照 FAT12 规则找到正确的扇区号即可。

从学习曲线的角度看,FAT32 需要处理更大的 FAT 表、更复杂的目录项(长文件名、32 字节目录项链)、数据区起始位置计算也更多变;FAT12 则把这些都砍到了最少状态。先跑通 FAT12,你会获得完整的“文件系统工作过程”心智模型。

2.2 FAT12 磁盘布局

FAT12 把整个软盘分成了几个连续的区段,顺序如下:

区段说明1.44MB 软盘的典型值
保留区通常是引导扇区所在区域,BPB 记录了保留扇区数1 个扇区
FAT 区存储文件分配表,一般有两份,互为备份每份 9 个扇区,共 18 个扇区
根目录区存储根目录下的目录项,每个目录项 32 字节14 个扇区(224 个目录项)
数据区实际存放文件内容的地方剩余全部扇区

这些数字不是固定的,而是由引导扇区的 BPB(BIOS Parameter Block)决定的。每个字段的偏移、长度、含义是 FAT12 实现里最先要面对的细节。

一个典型的 1.44MB FAT12 软盘 BPB 关键字段如下:

偏移(十六进制)长度含义常见值
0x0B2每扇区字节数512
0x0D1每簇扇区数1
0x0E2保留扇区数1
0x101FAT 表数量2
0x112根目录项数224
0x162每 FAT 扇区数9
0x182每磁道扇区数18
0x1A2磁头数2

这些字段共同决定了整个文件系统的几何布局。从这些字段可以推出:

  • 根目录区起始扇区 = 保留扇区数 + FAT 表数量 × 每 FAT 扇区数
  • 根目录区占用的扇区数 = 根目录项数 × 32 / 每扇区字节数
  • 数据区起始扇区 = 根目录区起始扇区 + 根目录区占用的扇区数

2.3 目录项结构:文件信息从哪里来

FAT12 的根目录区由若干个 32 字节的目录项组成。每个目录项描述一个文件或子目录。对于文件名和扩展名,采用 8.3 格式:文件名部分 8 字节,扩展名部分 3 字节,若不是 8 个字节,用空格补齐。

目录项的字段分布如下:

偏移(十进制)长度含义
08文件名
83扩展名
111属性字节
1210保留
222最后修改时间
242最后修改日期
262起始簇号
284文件长度(字节)

属性字节非常关键,常见值如下:

  • 0x01 只读
  • 0x02 隐藏
  • 0x04 系统
  • 0x08 卷标
  • 0x10 子目录
  • 0x20 归档
  • 0x0F 长文件名条目

目录项全 00 表示后续目录项全部为空;文件名首字节是 0xE5 时,表示该目录项对应的文件已被删除。

2.4 FAT 表项:文件内容不是连续存放的

FAT 表的核心作用是把数据区里的“簇”串成链。每个簇在 FAT 表中都对应一个表项,表项的值就是下一个簇的编号。如果某个文件占用了多个簇,那么它的簇链就是:起始簇号 → FAT[起始簇号] → FAT[FAT[起始簇号]] → ...,直到表项的值为 0xFFF(文件结束标记)。

FAT12 比较特殊的地方是,每个表项只有 12 位,也就是 1.5 字节。因此,访问 FAT 表项时不是简单的fat[cluster],而是要按位拼接。从第三个表项开始,FAT 表还存储了两个特殊值:0xFF0 表示坏簇,0xFF7 表示保留簇。

由于 FAT12 表项的宽度不是整数字节,索引访问时要小心处理。假设要读取第 N 个表项:

  • 计算字节偏移:N + N / 2
  • 读取 16 位数据:此时可能跨越两个字节,所以必须读取一个完整的 16 位值
  • 如果 N 是偶数,保留低 12 位;如果 N 是奇数,保留高 12 位

这个位运算在学习和面试中都很经典,也是实验里最容易出错的地方之一。

2.5 实模式下的实际情况

在实模式下,寻址使用的是 段:偏移 的形式,读取磁盘需要依赖 BIOS 中断 0x13。这意味着你不需要自己写 IDE 控制器驱动,只需要设置好寄存器,调用 BIOS 的读扇区功能即可。

但实模式也有内存限制。经典情况下,可用内存大概在 1MB 以内,而且前 640KB 是常规内存。FAT12 的整个 FAT 表和根目录区加起来并不大,加载到内存中的缓冲区是可行的。实验里的典型做法是:

  1. 把根目录区读到内存中的缓冲区。
  2. 遍历每个目录项,匹配文件名。
  3. 读取起始簇号和文件大小。
  4. 根据 FAT 表项追踪簇链,逐个簇读取数据。

这里的“读取数据”依然依赖 BIOS 0x13,每次读取一个簇(1 个扇区,512 字节),然后根据 FAT 表项跳到下一个簇。

3. 环境准备与前置条件

在这个实验里,我们主要使用汇编语言(NASM 语法)和 QEMU 模拟器。版本不做过高要求,我用到的工具如下,读者可根据自己的环境调整:

  • NASM 汇编器,推荐 2.x 版本。
  • QEMU 模拟器,用于测试软盘镜像启动。
  • 一个能生成 FAT12 软盘镜像的工具,如 Linux 下的mkfs.fat,或者 Windows 下的 WinImage。
  • 建议使用 Linux 环境,如果没有,WSL 也能满足实验要求。

需要确认的知识点如下:

  • 熟悉 8086 实模式下的分段寻址。
  • 了解 BIOS 中断 0x13 的调用参数。
  • 能写一个最简单的引导扇区,并跑通 QEMU 启动。

如果你对引导扇区还不熟悉,建议先把下面这个最小引导代码跑通,再继续文件系统实验。

org 0x7c00 start: mov ax, 0x0003 int 0x10 mov si, msg print_loop: lodsb or al, al jz hang mov ah, 0x0e int 0x10 jmp print_loop hang: hlt jmp hang msg db "Booting...", 0 times 510 - ($ - $$) db 0 dw 0xaa55

编译并运行:

nasm -f bin boot.asm -o boot.bin dd if=/dev/zero of=floppy.img bs=512 count=2880 dd if=boot.bin of=floppy.img conv=notrunc qemu-system-x86_64 -fda floppy.img

如果 QEMU 窗口中出现了Booting...,说明环境没有问题。

这里要提醒一点:实验里读写的是软盘镜像,不是真实软盘。用 QEMU 的好处就是随便折腾,不会损毁硬件。

4. 核心流程拆解:从磁盘布局到文件遍历

文件遍历这个实验看起来复杂,实际上拆开之后只有四个环节。

4.1 计算文件系统参数

拿到一个 FAT12 软盘镜像后,首先要读取引导扇区的 BPB,得到每扇区字节数、每簇扇区数、保留扇区数、FAT 数量、根目录项数、每 FAT 扇区数等参数。

这些参数一旦确定,就可以算出三个关键位置:

  • 根目录区起始扇区
  • 根目录区占用的扇区数
  • 数据区起始扇区

计算逻辑如下:

根目录区起始扇区 = 保留扇区数 + (FAT 表数量 × 每 FAT 扇区数)

根目录区占用的扇区数 = (根目录项数 × 32 + 每扇区字节数 - 1) / 每扇区字节数

数据区起始扇区 = 根目录区起始扇区 + 根目录区占用的扇区数

4.2 读取根目录区并匹配文件名

根目录区是一批连续的 32 字节目录项。把整个根目录区按扇区读取到内存后,就可以逐项检查:

  • 文件名字段首字节为 0x00,说明后面没有目录项了,停止遍历。
  • 文件名字段首字节为 0xE5,说明该项被删除,跳过。
  • 属性字段为 0x0F,这是长文件名条目,跳过。
  • 其他情况下,比较 11 字节的文件名和扩展名。

如果文件名匹配成功,取出起始簇号和文件大小。

4.3 读取 FAT 表项并跟踪簇链

FAT 表可能不止一个扇区,正常情况下按顺序加载到内存中即可。FAT12 表项不是整齐的字节边界,必须用位运算提取 12 位值。

拿到起始簇号后,先把簇号转换为数据区的逻辑扇区号:

数据区扇区号 = 数据区起始扇区 + (簇号 - 2) × 每簇扇区数

文件内容不足一个簇时直接读完;超过一个簇时,用当前簇号去查 FAT 表,得到下一个簇号,继续读取。直到 FAT 表项值为 0xFFF。

4.4 按簇读取文件内容

由于实验重点是文件遍历,可以在读取每个簇后,直接把该簇内容按字节输出到屏幕。这样可以直观地看到文件内容被成功解析出来的过程。

实际内核开发中,这一步通常会改成“加载到内存并跳转执行”,但实验阶段用输出验证最简单。

5. 完整示例:FAT12 文件遍历实验代码实现

下面给出一个完整的实验代码。代码分为两个文件:

  • fat12.asm包含引导扇区、读取磁盘、解析 FAT12、遍历文件并输出的逻辑。
  • Makefile用于构建软盘镜像并启动 QEMU。

这个示例在 QEMU 里运行后,会扫描软盘镜像根目录区,读取一个名为HELLO TXT(8.3 格式:HELLO+TXT)的文件,并把文件内容打印到屏幕上。

5.1 镜像准备

先用 Linux 自带的工具生成 FAT12 软盘镜像,并写入一个测试文件。

dd if=/dev/zero of=floppy.img bs=512 count=2880 mkfs.fat -F 12 floppy.img mkdir -p /tmp/fat12_mount sudo mount -o loop floppy.img /tmp/fat12_mount echo "Hello from FAT12!" > /tmp/fat12_mount/HELLO.TXT sudo umount /tmp/fat12_mount

特别说明:实际读取时,文件名要按照 8.3 格式比较,所以HELLO.TXT在目录项中会保存为文件名HELLO,扩展名TXT,不足部分用空格补齐,也就是HELLOTXT

5.2 完整汇编代码

代码较长,但每个段落在逻辑上是可拆解的。建议读者不要直接复制后就不管,而是先阅读注释,理解每个步骤的意图。

; 文件路径:fat12.asm ; 功能:启动时读取 FAT12 软盘镜像根目录,匹配 HELLO.TXT, ; 通过 FAT 表跟踪簇链,输出文件内容。 ; 编译:nasm -f bin fat12.asm -o fat12.bin org 0x7c00 jmp short start nop ; ---------- BPB 区域(兼容 FAT12 引导扇区) ---------- OEMName db "MYOS " BytesPerSec dw 512 SecPerClus db 1 RsvdSecCnt dw 1 NumFATs db 2 RootEntCnt dw 224 TotSec16 dw 2880 Media db 0xF0 FATSz16 dw 9 SecPerTrk dw 18 NumHeads dw 2 HiddSec dd 0 TotSec32 dd 0 ; 扩展 BPB DrvNum db 0 Reserved1 db 0 BootSig db 0x29 VolID dd 0x12345678 VolLab db "MYOS FLOPPY" FilSysType db "FAT12 " ; ---------- 实模式代码起始 ---------- start: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 mov ax, 0x0003 int 0x10 ; 清屏,切换到文本模式 mov si, msg_init call print_string ; 计算根目录区起始扇区 ; RootDirStart = RsvdSecCnt + (NumFATs * FATSz16) xor ax, ax mov al, [NumFATs] mul word [FATSz16] add ax, [RsvdSecCnt] mov [RootDirStart], ax ; 计算根目录区占用的扇区数 ; RootDirSectors = (RootEntCnt * 32 + BytesPerSec - 1) / BytesPerSec mov ax, [RootEntCnt] shl ax, 5 ; 乘 32 xor dx, dx mov bx, [BytesPerSec] add ax, bx dec ax div bx mov [RootDirSectors], ax ; 计算数据区起始扇区 ; DataStart = RootDirStart + RootDirSectors mov ax, [RootDirStart] add ax, [RootDirSectors] mov [DataStart], ax ; 根目录区加载到内存 mov ax, 0x1000 mov es, ax xor bx, bx mov ax, [RootDirStart] mov cx, [RootDirSectors] call read_sectors ; 在根目录区查找 HELLO TXT mov di, 0 mov cx, [RootEntCnt] find_loop: cmp cx, 0 jz file_not_found push cx mov si, target_file_name push di mov cx, 11 repe cmpsb pop di je found_entry pop cx add di, 32 ; 下一个目录项 dec cx jmp find_loop found_entry: pop cx ; 目录项偏移 26 处是起始簇号 mov ax, [es:di+26] mov [file_cluster], ax ; 目录项偏移 28 处是文件大小 mov ax, [es:di+28] mov [file_size_low], ax mov si, msg_found call print_string ; 加载 FAT 表到内存 mov ax, 0x2000 mov es, ax xor bx, bx mov ax, [RsvdSecCnt] ; FAT 起始扇区 mov cx, [FATSz16] call read_sectors ; 按簇链读取文件内容 xor ebx, ebx mov ebx, [file_cluster] mov eax, ebx file_read_loop: cmp ax, 0x0FF0 jae file_read_done ; 计算数据区扇区号 = DataStart + (cluster - 2) * SecPerClus sub ax, 2 xor ch, ch mov cl, [SecPerClus] mul cx add ax, [DataStart] ; 读取一个簇到 0x3000:0 mov si, ax mov ax, 0x3000 mov es, ax xor bx, bx mov ax, si mov cx, 1 call read_sectors ; 输出这个簇的内容 mov si, 0 print_cluster: mov al, [es:si] cmp al, 0 jz skip_char mov ah, 0x0e int 0x10 skip_char: inc si cmp si, 512 jb print_cluster ; 通过 FAT 表查找下一个簇 mov ax, [file_cluster] call get_fat_entry mov [file_cluster], ax jmp file_read_loop file_read_done: ; 回到内核外壳(此处演示用,直接停机) mov si, msg_done call print_string jmp hang file_not_found: mov si, msg_not_found call print_string jmp hang ; ---------- 读取扇区 ---------- ; 输入:ax = 起始扇区号, cx = 扇区数, es:bx = 缓冲区 read_sectors: pusha mov [read_lba], ax mov [read_count], cx read_loop: ; 将 LBA 转为 CHS xor dx, dx mov ax, [read_lba] mov cx, [SecPerTrk] div cx mov cl, dl inc cl xor dx, dx mov ax, [read_lba] mov cx, [SecPerTrk] div cx xor dx, dx mov ax, ax mov cx, [NumHeads] div cx mov dh, dl mov ch, al mov dl, [DrvNum] mov ax, 0x0201 int 0x13 jc disk_error inc word [read_lba] dec word [read_count] jz read_done ; 缓冲区前进一个扇区 mov ax, es add ax, 0x20 mov es, ax jmp read_loop read_done: popa ret disk_error: mov si, msg_disk_error call print_string jmp hang ; ---------- 获取 FAT12 表项 ---------- ; 输入:ax = 簇号 ; 输出:ax = FAT 表项值 get_fat_entry: push bx push cx push dx push si push di mov bx, ax shr bx, 1 ; 相当于乘 1.5,先取整数部分 mov si, bx mov di, 0 mov dx, [es:si] test ax, 1 jz even_fat shr dx, 4 jmp fat_done even_fat: and dx, 0x0fff fat_done: mov ax, dx pop di pop si pop dx pop cx pop bx ret ; ---------- 输出字符串 ---------- print_string: pusha print_loop: lodsb or al, al jz print_done mov ah, 0x0e int 0x10 jmp print_loop print_done: popa ret hang: hlt jmp hang ; ---------- 数据区 ---------- read_lba dw 0 read_count dw 0 RootDirStart dw 0 RootDirSectors dw 0 DataStart dw 0 file_cluster dw 0 file_size_low dw 0 msg_init db "Loading FAT12...", 13, 10, 0 msg_found db "Found HELLO.TXT, content:", 13, 10, 0 msg_not_found db "HELLO.TXT not found!", 13, 10, 0 msg_disk_error db "Disk read error!", 13, 10, 0 msg_done db 13, 10, "Done.", 0 ; 目标文件名:HELLO + TXT,共 11 字节 target_file_name db "HELLO TXT" times 510 - ($ - $$) db 0 dw 0xaa55

5.3 代码关键逻辑解释

这个代码虽然看起来长,但核心逻辑只有几块:

第一块是BPB 定义。这个结构对 FAT12 文件系统至关重要。引导扇区以jmp short start; nop开头,然后紧跟着 BPB。很多磁盘工具、操作系统引导程序都依赖 BPB 来识别文件系统参数,所以即使我们是自己写引导代码,也最好把这段结构补齐。

第二块是读取根目录区。代码先把根目录区起始扇区和扇区数算出来,然后用read_sectors读取到内存。这里把根目录区放在0x1000:0处,也就是物理地址0x10000

第三块是目录项匹配。目录项 32 字节一个,程序从第 0 项开始逐项比较。匹配时比较的是 11 字节,文件名的空格补齐已经写死在target_file_name里。

第四块是读取 FAT 表并跟踪簇链。找到目录项后,取出起始簇号,然后把 FAT 表加载到内存。get_fat_entry函数负责处理 12 位表项跨字节边界的问题。具体做法是:

  • 先用簇号乘 1.5 得到字节偏移。
  • 读取 16 位数据。
  • 根据簇号的奇偶性取高 12 位或低 12 位。

第五块是按簇输出文件内容。每读取一个簇,就把 512 字节内容直接打印到屏幕。如果遇到 FAT 表项值大于等于 0x0FF0,说明文件结束。

5.4 构建并运行

把代码保存为fat12.asm,然后执行:

nasm -f bin fat12.asm -o fat12.bin dd if=/dev/zero of=floppy.img bs=512 count=2880 mkfs.fat -F 12 floppy.img mkdir -p /tmp/fat12_mount sudo mount -o loop floppy.img /tmp/fat12_mount echo "Hello from FAT12!" > /tmp/fat12_mount/HELLO.TXT sudo umount /tmp/fat12_mount dd if=fat12.bin of=floppy.img conv=notrunc qemu-system-x86_64 -fda floppy.img

这里有一个非常关键的细节:必须先 mkfs.fat 建立文件系统,再把引导代码写入镜像。如果反过来,mkfs.fat 会把引导扇区覆盖掉,导致无法启动。

6. 运行结果与效果验证

如果一切正常,QEMU 窗口中会输出类似下面的内容:

Loading FAT12... Found HELLO.TXT, content: Hello from FAT12! Done.

看到这个输出,基本可以确定:

  • 引导扇区正确加载并进入实模式代码。
  • BPB 参数被正确解析。
  • 根目录区被正确读取。
  • 目录项匹配成功。
  • FAT 表项访问正确。
  • 文件内容按簇读取成功。

如果输出不完整,比如只看到了Loading FAT12...,说明问题出在后面的环节。可以先按下面的顺序层层排查:

  1. 是否匹配到了文件?如果没有,检查target_file_name的 8.3 格式是否准确。在 FAT12 目录项里,HELLO.TXT应该写作HELLO TXT,注意文件名部分是 8 字节,不足补空格,扩展名部分是 3 字节。
  2. 是否加载了正确的根目录区?检查 RootDirStart 的计算结果是否和预期一致。可以用十六进制工具查看软盘镜像的 0x2600 偏移处,那里通常是 1.44MB FAT12 根目录区起始位置。
  3. 是否访问了正确的 FAT 表项?FAT12 表项跨字节问题非常隐蔽,建议先验证一个只占一个簇的小文件,再尝试多簇文件。
  4. 文件内容打印是否完整?注意每次读取一个簇后,下一个簇的查表逻辑是否正确。如果get_fat_entry的输入簇号没有更新,就会出现无限重复同一簇的“死循环”输出。

在实际调试中,最推荐的方式是用 QEMU 的-d int,cpu_reset或者直接加日志,把每个关键寄存器的值打印出来。不过更朴素的做法是:先在代码里手动输出变量的值,比如根目录起始扇区、数据区起始扇区、匹配到的簇号,逐项对比预期值。

6.1 用十六进制工具验证磁盘布局

如果你想自己动手确认 FAT12 镜像的布局,推荐使用xxdhexdump。例如:

xxd -s 0x2600 -l 512 floppy.img

这条命令会从偏移 0x2600 开始显示 512 字节,这正好是 1.44MB FAT12 软盘的根目录区起始位置。你会看到类似这样的内容:

HELLO TXT ......

如果你看到的全是 0x00,说明根目录区可能没有数据,或者镜像根本没有被正确制作成 FAT12。

7. 常见问题与排查思路

我在做这个实验时,也踩过不少坑。这里把最典型的几类问题整理成表格,方便大家对照排查。

问题现象可能原因排查方式解决方案
QEMU 启动后黑屏或重启引导扇区没有正确写入镜像检查镜像前 512 字节是否以 0x55AA 结尾重新编译并覆盖写入引导扇区
能输出Loading FAT12...,但找不到文件目标文件名格式不对xxd查看根目录区实际目录项字节把文件名补齐为 8.3 格式,注意空格
找到了文件但内容为空或乱码数据区起始扇区计算错误手动计算根目录区扇区数,并验证数据区位置核对 BPB 字段,尤其注意RootEntCnt
只打印了第一个扇区内容就卡住FAT 表项读取逻辑有问题检查get_fat_entry的奇偶判断和位运算用单簇文件验证,再测试多簇文件
读取扇区时报 Disk read error使用真实软盘时的 CHS 参数不对确认每磁道扇区数和磁头数优先使用 QEMU 虚拟软盘,确认后扩展到真实硬件
文件内容重复循环输出簇号更新失败,FAT 表项一直返回同一个簇检查簇号是否写回file_cluster确认循环里每次查表都更新簇号
输出中多出很多空格或不可见字符文件本身包含换行,代码直接打印0x00时不输出,但0x0d0x0a会显示为换行先用xxd查看文件内容如果不需要显示控制字符,可以在打印时过滤

这里最值得反复检查的是get_fat_entry。FAT12 表项的位拼接并不复杂,但初学者很容易把奇偶条件写反。你可以用一个小文件(一个簇能放下)验证,文件只有十几个字节,读取第一个簇就结束。如果这个文件能正常打印,至少说明根目录解析和起始簇号读取正确;之后再去测试多簇文件,专门验证 FAT 表项是否正常。

8. 最佳实践与工程建议

FAT12 遍历实验本身是一个很小的程序,但如果想把它做成可复用的内核基础设施,有一些建议值得提前考虑。

8.1 把 BPB 解析封装成独立函数

不要在主流程里到处读取 BPB 字段。更推荐的方式是,在一开始就把所有关键参数解析出来,存入固定的内存地址或段内变量。比如:

  • g_bpb_rvscrvd
  • g_bpb_fatsz
  • g_bpb_rootent
  • g_bpb_next_free

这样后续函数只需要引用这些变量即可,代码的阅读性会好很多。

8.2 用链表形式维护簇链信息

如果只是打印文件内容,按簇读取循环就够了。但如果后续你要实现“按文件读入内存”的功能,建议先遍历一遍 FAT 表,把簇链表写入一个结构体数组,再根据链表去读取数据。这样即使中间某个簇读取失败,也能准确知道是哪个簇出了问题。

8.3 为文件系统操作定义统一接口

典型的内核文件系统接口可能会长这样:

int fs_open(const char *path); int fs_read(int fd, void *buf, int len); void fs_close(int fd);

虽然这个实验还在实模式汇编阶段,但先在汇编层面定义好“打开文件”“读取文件”的标号约定,后面迁移到 C 语言时会更自然。

8.4 注意缓冲区位置

实模式下内存很有限,要避免把缓冲区放在会被引导代码覆盖的地址。通常0x1000:00x2000:0这些地址用于临时缓冲区问题不大,但要坐实每个缓冲区的位置和大小,避免互相覆盖。

8.5 使用 QEMU 进行快速迭代

开发过程中,最痛苦的是反复制作软盘镜像。建议写一个Makefile,把构建、文件写入、启动集成好。下面是一个直接可用的参考:

# 文件路径:Makefile ASM=nasm QEMU=qemu-system-x86_64 IMG=floppy.img MOUNT=/tmp/fat12_mount all: run build: $(ASM) -f bin fat12.asm -o fat12.bin image: dd if=/dev/zero of=$(IMG) bs=512 count=2880 mkfs.fat -F 12 $(IMG) mount: mkdir -p $(MOUNT) sudo mount -o loop $(IMG) $(MOUNT) unmount: sudo umount $(MOUNT) write-boot: build dd if=fat12.bin of=$(IMG) conv=notrunc run: write-boot image $(QEMU) -fda $(IMG) clean: rm -f fat12.bin $(IMG)

使用方式:

make build # 只编译 make image # 生成空白 FAT12 镜像 make mount # 挂载镜像 make run # 完整构建并启动

这个 Makefile 没有把“写入 HELLO.TXT”自动化,读者可以根据自己的需要扩展。提醒一下,挂载和卸载需要 root 权限,运行 QEMU 不需要。

8.6 保留 FAT 表备份的处理

FAT12 镜像里通常有两份相同的 FAT 表。第一份损坏时,可以从第二份恢复。实验阶段我们只读取第一份,但如果未来做真实的文件系统工具,一定要考虑 FAT 表的一致性检查和备份恢复逻辑。

9. 总结与后续学习方向

通过这个实验,你已经把 FAT12 文件系统中最核心的机制走了一遍:从 BPB 解析磁盘布局,到根目录区查找目录项,再到用 FAT 表跟踪文件的簇链。这个过程虽然是在软盘镜像上完成的,但它背后的设计思想在 FAT16、FAT32 以及许多简化嵌入式文件系统中都是一脉相承的。

这个实验做完后,有几个方向值得继续深入:

  1. 把文件读取从“屏幕输出”改成“加载到内存”。这是写一个可加载用户程序的 kernel 的必要步骤。只要文件可以在内核中被定位并复制到指定内存,就可以实现最原始的“可执行文件加载器”。

  2. 支持子目录遍历。目录项中属性字节为 0x10 时表示子目录,子目录的读取方式和文件类似,但它的内容是另一个“目录区”。实现子目录遍历之后,文件系统才算真正可用。

  3. 对比 FAT16 和 FAT32 的差异。FAT12 的 FAT 表项是 12 位,FAT16 是 16 位,FAT32 是 32 位且根目录不再固定占用空间。当你理解了 FAT12 的整个布局,再看 FAT32 就会容易很多。

  4. 把文件系统操作从汇编迁移到 C。很多 OS 教程在完成汇编引导阶段后,会切换到 C 语言进行内核开发。C 语言下结构体访问目录项会方便很多,但底层的扇区读取和 FAT 表项解析思路完全一致。

最后说一个提醒:文件系统实验的坑,几乎都出在“数据布局”和“位运算”上。如果你在调试时找不到问题,不要只盯着代码看,回到磁盘二进制层面去核对。用十六进制工具把根目录区、FAT 表、数据区一个个字节看清楚,往往比反复改代码更快。

希望这篇文章能帮你跨过 FAT12 这道坎,顺利进入内核文件系统开发的下一站。建议收藏备用,做实验时如果卡住了,按文中的排查顺序逐项对照,大概率能定位到问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询