02_FAT12文件系统
写在前面
本文首先罗列了FAT12文件系统的数据结构,然后写出自己在学习、编码过程中遇到的问题及思考,最后进行编码实现:
- 输出引导扇区结构数据。
- 输出根目录区结构数据。
- 在文件系统内存入一份文本,通过在FAT表中查询簇号输出数据区的文件内容。
FAT12文件系统结构
FAT12文件系统分为5个区,引导扇区中记录了整个文件系统的基础信息。FAT1表与FAT2表完全一致,属于相互备份的关系,里面存储了簇号,与数据区存在特殊的关系,后续会详细介绍。根目录区记录了文件系统内所有存储的文件信息,包括文件名、簇号、属性、时间等等。数据区不言而喻,是存储文件主体内容的地方。
| 序号 | 扇区位置 | 扇区个数 | 备注 | 
|---|---|---|---|
| 01 | 0 | 1 | 引导扇区 | 
| 02 | 1 | 9 | FAT1 表 | 
| 03 | 10 | 9 | FAT2 表 | 
| 04 | 19 | 14 | 根目录区 | 
| 05 | 33 | 2847 | 数据区 | 
引导扇区结构
| 标识 | 偏移量 | 类型 | 大小 | 默认值 | 描述 | 
|---|---|---|---|---|---|
| BS_JmpBoot | 0 | db | 3 | - | 跳转指令 | 
| BS_OEMName | 3 | db | 8 | FREEDOS | OEM字符串,必须为 8 个字符,不足会以空格填充 | 
| BPB_BytePerSec | 11 | dw | 2 | 0x200 | 每个扇区字节数 | 
| BPB_SecPerClus | 12 | db | 1 | 1 | 每簇占用的扇区数 | 
| BPB_RsvdSecCnt | 14 | dw | 2 | 1 | Boot占用的扇区数 | 
| BPB_NumFATs | 16 | db | 1 | 2 | FAT表的数量 | 
| BPB_RootEntCnt | 17 | dw | 2 | 0xE0 | 根目录可容纳的目录项数 | 
| BPB_TotSec16 | 19 | dw | 2 | 0xB40 | 逻辑扇区总数 | 
| BPB_Media | 21 | db | 1 | 0xF0 | 媒体描述符 | 
| BPB_FATSz16 | 22 | dw | 2 | 9 | 每个FAT占用扇区数 | 
| BPB_SecPerTrk | 24 | dw | 2 | 0x12 | 每个磁道扇区数 | 
| BPB_NumHeads | 26 | dw | 2 | 2 | 磁头数 | 
| BPB_HiddSec | 28 | dd | 4 | 0 | 隐藏扇区数 | 
| BPB_TotSec32 | 32 | dd | 4 | 0 | 若BPB_TotSec16是0,则在这里记录扇区总数 | 
| BS_DrvNum | 36 | db | 1 | 0 | 中断 13(int 13h)的驱动器号 | 
| BS_Reserved1 | 37 | db | 1 | 0 | 未使用 | 
| BS_Bootsig | 38 | db | 1 | 0x29 | 扩展引导标志 | 
| BS_VolID | 39 | dd | 4 | 0 | 卷序列号 | 
| BS_VolLab | 43 | db | 11 | - | 卷标,必须为11个字符,不足会以空格填充 | 
| BS_FileSysType | 54 | db | 8 | FAT12 | 文件系统类型,必须是8个字符,不足以空格填充 | 
| BOOT_Code | 62 | db | 448 | 0x00 | 引导代码,由偏移0字节(BS_JmpBoot)跳转过来 | 
| END | 510 | db | 2 | 0x55, 0xAA | 系统引导标识,引导扇区结束标识 | 
FAT表结构
| FAT 项 | 可取值 | 描述 | 
|---|---|---|
| 0 | BPB_Media | 磁盘标识字,低字节需与 BPB_Media 数值保持一致 | 
| 1 | FFFh | 表示第一个簇已占用 | 
| 2 ~ N | 000h | 可用簇 | 
| 002h~FEFh | 已用簇 | |
| FF0h~FF6h | 保留簇 | |
| FF7h | 坏簇 | |
| FF8h~FFFh | 文件的最后一个簇 | 
根目录区
| 名称 | 偏移 | 长度 | 描述 | 
|---|---|---|---|
| DIR_Name | 0x00 | 11 | 文件名 8B,扩展名 3B | 
| DIR_Attr | 0x0B | 1 | 文件属性 | 
| 保留 | 0x0C | 10 | 保留位 | 
| DIR_WrtTime | 0x16 | 2 | 最后一次写入时间 | 
| DIR_WrtDate | 0x18 | 2 | 最后一次写入日期 | 
| DIR_FstCtus | 0x1A | 2 | 起始簇号 | 
| DIR_FileSize | 0x1C | 4 | 文件大小 | 
编码须知
- 以1字节对齐
#pragma pack(push)
#pragma pack(1)
struct结构体
#pragma pack(pop)
- 输出11个字节的字符串,可以在输出时加入描述符,而不需要在字符串结尾加入\0
printf("DIR_Name: %11.11s\n", re.DIR_Name);
- 根据FAT表在数据区查找文件内容
 重点:FAT表中的每个表项只占用12比特(1.5字节)
- 
FAT表起始地址为第二个簇,MBR结构中BPB_FATSz16参数记录了每个FAT表所占簇的个数,占用9个,那么需要偏移一个簇开始读文件。大小为FAT簇个数*每个簇所占字节数。 
- 
此时获得的字符串即为FAT表的内存数据,那么需要以12bit为步长,获取其值,存入一个新的数据结构中【我malloc申请了一段内存,指针类型:unsigned short,指针变量名:cluster_list】,接下来的数据转化按照网上的讲解,我分析了好久,咨询了大神后,得到如下清晰易懂的步骤: 
先取前两个字节,得到数据0x3412,然后左移4bit,得到0x412
再取后两个字节,得到数据0x5634,然后右移4bit,得到0x563

个人猜想,或许是设计此文件结构的人,是为了在固定的内存空间中,保留更多的数据段。并且让FAT表更好的与数据区进行对应,才想到如此方法压缩数据的吧
- FAT表与数据区对应关系
 下图是我自身理解的过程,其中某些名字,并非官方命名。
 ![image]() 
编码前准备data.img
- 下载 freedos.img,使用freedos格式化data.img磁盘
 ![image]() 
- 挂载data.img盘,拷贝进去几个文件
 mount -o loop data.img /mnt/hgfs/
 拷贝一些文件
 umount /mnt/hgfs/
编码:查看FAT12的MBR信息
#include <fcntl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define uint8 unsigned char
#define uint16 unsigned short
#define uint32 unsigned int
#pragma pack(push)
#pragma pack(1)
typedef struct _fat12header
{
	uint8 BS_JmpBoot[3];
	uint8 BS_OEMName[8];
	uint16 BPB_BytePerSec;
	uint8 BPB_SecPerClus;
	uint16 BPB_RsvdSecCnt;
	uint8 BPB_NumFATs;
	uint16 BPB_RootEntCnt;
	uint16 BPB_TotSec16;
	uint8 BPB_Media;
	uint16 BPB_FATSz16;
	uint16 BPB_SecPerTrk;
	uint16 BPB_NumHeads;
	uint32 BPB_HiddSec;
	uint32 BPB_TotSec32;
	uint8 BS_DrvNum;
	uint8 BS_Reserved1;
	uint8 BS_Bootsig;
	uint8 BS_VolID[4];
	uint8 BS_VolLab[11];
	uint8 BS_FileSysType[8];
	uint8 BOOT_Code[448];
	uint8 MBR_Flag[2];
}fat12header;
#pragma pack(pop)
void printheader(fat12header* fat12, const char* path)
{
	FILE* fp = fopen(path, "r");
	fread(fat12, sizeof(fat12header), 1, fp);
	fat12->BS_OEMName[7] = 0;
	fat12->BS_VolLab[10] = 0;
	fat12->BS_FileSysType[7] = 0;
	printf("BS_OEMName: %s\n", fat12->BS_OEMName);
	printf("BPB_BytePerSec: %d\n", fat12->BPB_BytePerSec);
	printf("BPB_SecPerClus: %d\n", fat12->BPB_SecPerClus);
	printf("BPB_RsvdSecCnt: %d\n", fat12->BPB_RsvdSecCnt);
	printf("BPB_NumFATs: %d\n", fat12->BPB_NumFATs);
	printf("BPB_RootEntCnt: %d\n", fat12->BPB_RootEntCnt);
	printf("BPB_TotSec16: %d\n", fat12->BPB_TotSec16);
	printf("BPB_Media: %d\n", fat12->BPB_Media);
	printf("BPB_FATSz16: %d\n", fat12->BPB_FATSz16);
	printf("BPB_SecPerTrk: %d\n", fat12->BPB_SecPerTrk);
	printf("BPB_NumHeads: %d\n", fat12->BPB_NumHeads);
	printf("BPB_HiddSec: %d\n", fat12->BPB_HiddSec);
	printf("BPB_TotSec32: %d\n", fat12->BPB_TotSec32);
	printf("BS_DrvNum: %d\n", fat12->BS_DrvNum);
	printf("BS_Reserved1: %d\n", fat12->BS_Reserved1);
	printf("BS_Bootsig: 0x%x\n", fat12->BS_Bootsig);
	printf("BS_VolID: %d %d %d %d\n", fat12->BS_VolID[0], fat12->BS_VolID[1], fat12->BS_VolID[2], fat12->BS_VolID[3]);
	printf("BS_VolLab: %s\n", fat12->BS_VolLab);
	printf("BS_FileSysType: %s\n", fat12->BS_FileSysType);
	printf("Byte 510: 0x%x\n", fat12->MBR_Flag[0]);
	printf("Byte 511: 0x%x\n", fat12->MBR_Flag[1]);
	fclose(fp);
	return;
}
int main()
{
	fat12header fat12 = {0};
	printheader(&fat12, "data.img");
	return 0;
}

编码:查看根目录区结构
#include <fcntl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define uint8 unsigned char
#define uint16 unsigned short
#define uint32 unsigned int
#pragma pack(push)
#pragma pack(1)
typedef struct _fat12header
{
	uint8 BS_JmpBoot[3];
	uint8 BS_OEMName[8];
	uint16 BPB_BytePerSec;
	uint8 BPB_SecPerClus;
	uint16 BPB_RsvdSecCnt;
	uint8 BPB_NumFATs;
	uint16 BPB_RootEntCnt;
	uint16 BPB_TotSec16;
	uint8 BPB_Media;
	uint16 BPB_FATSz16;
	uint16 BPB_SecPerTrk;
	uint16 BPB_NumHeads;
	uint32 BPB_HiddSec;
	uint32 BPB_TotSec32;
	uint8 BS_DrvNum;
	uint8 BS_Reserved1;
	uint8 BS_Bootsig;
	uint8 BS_VolID[4];
	uint8 BS_VolLab[11];
	uint8 BS_FileSysType[8];
	uint8 BOOT_Code[448];
	uint8 MBR_Flag[2];
}fat12header;
typedef struct _rootentry
{
	uint8 DIR_Name[11];
	uint8 DIR_Attr;
	uint8 reserve[10];
	uint16 DIR_WrtTime;
	uint16 DIR_WrtDate;
	uint16 DIR_FstClus;
	uint32 DIR_FileSize;
}rootentry;
#pragma pack(pop)
void printheader(fat12header* fat12, const char* path)
{
	FILE* fp = fopen(path, "r");
	fread(fat12, sizeof(fat12header), 1, fp);
	fat12->BS_OEMName[7] = 0;
	fat12->BS_VolLab[10] = 0;
	fat12->BS_FileSysType[7] = 0;
	fclose(fp);
	return;
}
void findrootentry(fat12header* rf, const char* path, int i, rootentry* re)
{
	FILE* fp = fopen(path, "rb");
	if(fp && (0 <= i) &&(i < rf->BPB_RootEntCnt))
	{
		fseek(fp, 19*rf->BPB_BytePerSec+i*sizeof(rootentry), SEEK_SET);
		fread((void*)re, 1, sizeof(rootentry), fp);
	}
	fclose(fp);
	return;
}
void printrootentry(fat12header* rf, const char* path)
{
	for(int i = 0; i < rf->BPB_RootEntCnt;i++)
	{
		rootentry re = {0};
		findrootentry(rf, path, i, &re);
		if(0 == re.DIR_FstClus || '\0' == re.DIR_Name[0])
            continue;
		printf("====%d==== : \n", i);
		printf("DIR_Name: %11.11s\n", re.DIR_Name);
		printf("DIR_Attr: 0x%x\n", re.DIR_Attr);
		printf("DIR_WrtTime: %d\n", re.DIR_WrtTime);
		printf("DIR_WrtDate: %d\n", re.DIR_WrtDate);
		printf("DIR_FstClus: %d\n", re.DIR_FstClus);
		printf("DIR_FileSize: %d\n", re.DIR_FileSize);
	}
}
int main()
{
	fat12header fat12 = {0};
	printheader(&fat12, "data.img");
	printrootentry(&fat12, "data.img");
	return 0;
}
MacOS M1,结果为:

Ubuntu,结果为:

编码:根据根目录区文件名,加载文件数据
#include <fcntl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define uint8 unsigned char
#define uint16 unsigned short
#define uint32 unsigned int
#pragma pack(push)
#pragma pack(1)
typedef struct _fat12header
{
	uint8 BS_JmpBoot[3];
	uint8 BS_OEMName[8];
	uint16 BPB_BytePerSec;
	uint8 BPB_SecPerClus;
	uint16 BPB_RsvdSecCnt;
	uint8 BPB_NumFATs;
	uint16 BPB_RootEntCnt;
	uint16 BPB_TotSec16;
	uint8 BPB_Media;
	uint16 BPB_FATSz16;
	uint16 BPB_SecPerTrk;
	uint16 BPB_NumHeads;
	uint32 BPB_HiddSec;
	uint32 BPB_TotSec32;
	uint8 BS_DrvNum;
	uint8 BS_Reserved1;
	uint8 BS_Bootsig;
	uint8 BS_VolID[4];
	uint8 BS_VolLab[11];
	uint8 BS_FileSysType[8];
	uint8 BOOT_Code[448];
	uint8 MBR_Flag[2];
}fat12header;
typedef struct _rootentry
{
	uint8 DIR_Name[11];
	uint8 DIR_Attr;
	uint8 reserve[10];
	uint16 DIR_WrtTime;
	uint16 DIR_WrtDate;
	uint16 DIR_FstClus;
	uint32 DIR_FileSize;
}rootentry;
#pragma pack(pop)
void printheader(fat12header* fat12, const char* path)
{
	FILE* fp = fopen(path, "r");
	fread(fat12, sizeof(fat12header), 1, fp);
	fat12->BS_OEMName[7] = 0;
	fat12->BS_VolLab[10] = 0;
	fat12->BS_FileSysType[7] = 0;
	fclose(fp);
	return;
}
void findrootentry(fat12header* rf, const char* path, int i, rootentry* re)
{
	FILE* fp = fopen(path, "rb");
	if(fp && (0 <= i) &&(i < rf->BPB_RootEntCnt))
	{
		fseek(fp, 19*rf->BPB_BytePerSec+i*sizeof(rootentry), SEEK_SET);
		fread((void*)re, 1, sizeof(rootentry), fp);
	}
	fclose(fp);
	return;
}
int searchrootentry(fat12header* rf, rootentry* re, const char* path, const char* filename)
{
	for(int i = 0; i < rf->BPB_RootEntCnt;i++)
	{
		findrootentry(rf, path, i, re);
		if(0 == re->DIR_FstClus || '\0' == re->DIR_Name[0])
            continue;
		char dirname[11] = {0};
		char *token = NULL;
		token = strtok(re->DIR_Name, " ");
		sprintf(dirname, "%s", token);
		token = strtok(NULL, " ");
		sprintf(dirname, "%s.%s", dirname, token);
		if(0 != strcmp(dirname, filename))
			continue;
		printf("%s\n", dirname);
		return 1;
	}
	return 0;
}
void readfat(fat12header* rf, const char* path, uint16* cluster_list)
{
	int fat_size = rf->BPB_FATSz16 * rf->BPB_BytePerSec;
	uint8* fat = (uint8*)malloc(fat_size);
	FILE* fp = fopen(path, "rb");
	if(fp)
	{
		fseek(fp, rf->BPB_BytePerSec * 1, SEEK_SET);
		fread((uint8*)fat, 1, fat_size, fp);
		for(int i = 0; i < fat_size; i+=3)
		{
			*cluster_list = ((uint16)(fat[i+1] & 0xF) << 8) + (uint16)fat[i];
			cluster_list += 1;
			*cluster_list = (fat[i+1] >> 4) + ((uint16)fat[i+2] << 4);
			cluster_list += 1;
		}
	}
	fclose(fp);
	free(fat);
}
void printfilecontent(fat12header* rf, rootentry* re, const char* path, const char* filename)
{
	// 根据目录项的名字,查找该文件的信息
	int flag = searchrootentry(rf, re, path, filename);
	if(0 == flag)
		return;
	// 将FAT项全部转化并存入 cluster_list 链表
	int fat_size = rf->BPB_FATSz16 * rf->BPB_BytePerSec;
	int cluster_list_size = fat_size*8/12;
	uint16* cluster_list = (uint16*)malloc(cluster_list_size*2);
	readfat(rf, path, cluster_list);
	short DIR_FstClus = re->DIR_FstClus;
	FILE* fp = fopen(path, "rb");
	while(fp)
	{
		// 计算簇地址
		short DIR_FstClus_addr = 33 * rf->BPB_SecPerClus * rf->BPB_BytePerSec + (DIR_FstClus - 2) * rf->BPB_SecPerClus * rf->BPB_BytePerSec;
		//printf("DIR_FstClus:%d\tDIR_FstClus_addr:0x%x\n", DIR_FstClus, DIR_FstClus_addr);
		char *filemessage = (char*)malloc(re->DIR_FileSize);
		fseek(fp, DIR_FstClus_addr, SEEK_SET);
		fread((void*)filemessage, 1, rf->BPB_SecPerClus * rf->BPB_BytePerSec, fp);
		printf("%s", filemessage);
		free(filemessage);
		DIR_FstClus = cluster_list[DIR_FstClus];
		if(DIR_FstClus >= 0xFF8)
			break;
	}
	fclose(fp);
	free(cluster_list);
}
int main()
{
	fat12header fat12 = {0};
	rootentry re = {0};
	printheader(&fat12, "data.img");
	printfilecontent(&fat12, &re, "data.img", "LOADER.BIN");
	return 0;
}

写在后面(心得)
如果我们站在FAT12文件系统上面看它,无非就是设计者制定了一套数据结构和规则,使用者按照它的规则去使用。
上学的时候学数据结构,总觉得数组、队列、链表、树之类的,只需要多记多练甚至考前背一背,就没什么难的。
刚工作时,见到的数据结构比较大,为了让其内存空间足够小会用union。为了节约数据拷贝的内存,声明时会按字节数对齐。
如今经历了FAT12,感觉它更难了,更多的是设计能力。
 
                     
                    
                 
                    
                


 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号