在上一章节《FAT12文件系统剖析1》中,我们把a.img启动软盘使用FreeDos系统格式化为FAT12文件系统的组织方式,并且向a.img软盘写入两个文件,同时通过FreeDos操作系统也查看了a.img软盘中的文件。之所以这么做,是因为主引导程序不可以超过512字节,所以主引导程序需要在基本的初始化工作完成后加载启动介质(这里是软盘a.img)中的启动程序到内存,然后跳转到对应内存处执行。
上一节我们是通过FreeDos操作系统来查看a.img软盘中FAT12根目录的目标文件,那么在程序中我们怎去判断目标文件是否存在FAT12根目录中?要解决这个问题,我们首先来了解FAT12的根目录在文件系统中的位置
上图中直接给了计算根目录大小的公式:根目录个数 * 每个根目录项大小 / 每个扇区大小 = 根目录占用的扇区数量,
其中根目录个数在上一章节我们已经求出,为0xe0,十进制为224,不熟悉的可以参考上一章节。每个根目录项大小为32字节,每个扇区大小为512字节,那么有 224 * 32 / 512 = 14,即根目录文件项在FAT12文件系统中总共占用14个扇区,也就是7168字节。
除了上边说的根目录区,我们还需要了解下根目录项,根目录由根目录项组成,上文中的224,就是FAT12文件系统有224个根目录项,一个根目录项代表根目录中的一个文件索引,每个根目录项中,包含了这个文件的基本信息,如下:
下边需要做的实验,便是读取FAT12中每个根目录项的内容并打印(代码在上章代码基础上增加更目录项的打印)
#include
#include
#include
#include
#pragma pack(push)
#pragma pack(1)
struct Fat12Header
{
char BS_OEMName[8];
ushort BPB_BytsPerSec; //每扇区字节数,默认512字节
uchar BPB_SecPerClus; //每簇扇区数
ushort BPB_RsvdSecCnt;
uchar BPB_NumFATs;
ushort BPB_RootEntCnt; //最大跟目录文件数
ushort BPB_TotSec16;
uchar BPB_Media;
ushort BPB_FATSz16;
ushort BPB_SecPerTrk;
ushort BPB_NumHeads;
uint BPB_HiddSec;
uint BPB_TotSec32;
uchar BS_DrvNum;
uchar BS_Reserved1;
uchar BS_BootSig;
uint BS_VolID;
char BS_VolLab[11];
char BS_FileSysType[8];
};
//根目录项结构体
struct RootEntry
{
char DIR_Name[11];
uchar DIR_Attr;
uchar reserve[10];
ushort DIR_WrtTime;
ushort DIR_WrtDate;
ushort DIR_FstClus;
uint DIR_FileSize;
};
#pragma pack(pop)
void PrintHeader(Fat12Header& rf, QString p)
{
QFile file(p);
if( file.open(QIODevice::ReadOnly) )
{
QDataStream in(&file);
file.seek(3); //偏移开头3字节
in.readRawData(reinterpret_cast(&rf), sizeof(rf));
rf.BS_OEMName[7] = 0;
rf.BS_VolLab[10] = 0;
rf.BS_FileSysType[7] = 0;
qDebug() << "BS_OEMName: " << rf.BS_OEMName;
qDebug() << "BPB_BytsPerSec: " << hex << rf.BPB_BytsPerSec;
qDebug() << "BPB_SecPerClus: " << hex << rf.BPB_SecPerClus;
qDebug() << "BPB_RsvdSecCnt: " << hex << rf.BPB_RsvdSecCnt;
qDebug() << "BPB_NumFATs: " << hex << rf.BPB_NumFATs;
qDebug() << "BPB_RootEntCnt: " << hex << rf.BPB_RootEntCnt;
qDebug() << "BPB_TotSec16: " << hex << rf.BPB_TotSec16;
qDebug() << "BPB_Media: " << hex << rf.BPB_Media;
qDebug() << "BPB_FATSz16: " << hex << rf.BPB_FATSz16;
qDebug() << "BPB_SecPerTrk: " << hex << rf.BPB_SecPerTrk;
qDebug() << "BPB_NumHeads: " << hex << rf.BPB_NumHeads;
qDebug() << "BPB_HiddSec: " << hex << rf.BPB_HiddSec;
qDebug() << "BPB_TotSec32: " << hex << rf.BPB_TotSec32;
qDebug() << "BS_DrvNum: " << hex << rf.BS_DrvNum;
qDebug() << "BS_Reserved1: " << hex << rf.BS_Reserved1;
qDebug() << "BS_BootSig: " << hex << rf.BS_BootSig;
qDebug() << "BS_VolID: " << hex << rf.BS_VolID;
qDebug() << "BS_VolLab: " << rf.BS_VolLab;
qDebug() << "BS_FileSysType: " << rf.BS_FileSysType;
file.seek(510); //偏移512字节的最后两字节处
uchar b510 = 0;
uchar b511 = 0;
//读取最后两字节
in.readRawData(reinterpret_cast(&b510), sizeof(b510));
in.readRawData(reinterpret_cast(&b511), sizeof(b511));
qDebug() << "Byte 510: " << hex << b510;
qDebug() << "Byte 511: " << hex << b511;
}
file.close();
}
//获取一个根目录项
RootEntry FindRootEntry(Fat12Header& rf, QString p, int i)
{
RootEntry ret = {{0}};
QFile file(p);
//BPB_RootEntCnt为最大根目录文件数
if( file.open(QIODevice::ReadOnly) && (0 <= i) && (i < rf.BPB_RootEntCnt) )
{
QDataStream in(&file);
//定位到19扇区的各个根目录项开始处
file.seek(19 * rf.BPB_BytsPerSec + i * sizeof(RootEntry));
//每次只读一个根目录项
in.readRawData(reinterpret_cast(&ret), sizeof(ret));
}
file.close();
return ret; //返回读取到的根目录项
}
//获取指定文件名的根目录项
RootEntry FindRootEntry(Fat12Header& rf, QString p, QString fn)
{
RootEntry ret = {{0}};
for(int i=0; i= 0 )
{
QString n = fn.mid(0, d);
QString p = fn.mid(d + 1);
if( name.startsWith(n) && name.endsWith(p) )
{
ret = re;
break;
}
}
else
{
if( fn == name )
{
ret = re;
break;
}
}
}
}
return ret;
}
//打印根目录项
void PrintRootEntry(Fat12Header& rf, QString p)
{
//依次遍历每个根目录项并获取打印
for(int i=0; i
从输出看到,已经成功打印了我们上章节往a.img软盘拷贝的两个文件,test.txt和loader.bin ,置于其他两个文件输出,暂时不需要理会,这是FAT12文件系统默认的处理方式;同时也可以打印指定的文件信息。
以上我们打印了根目录目录项的信息和指定文件的文件信息,那么下边需要做的就是读取文件指定文件内容,在上述读取的目录项信息中,对我们比较有用的是如下三个成员:
理论上有了文件名、文件数据存储起始位置、文件大小,就可以读取文件的内容,但是在FAT12中,需要知道的是:文件数据不是连续存储的,可能是存储于不同的扇区中,也就是分散存储的。为了读取分散在不同扇区的内容,需要借助FAT表项来组织存储在不同扇区上的数据,这里可以理解为数据结构中的链表,虽然是分散的,但是读取过程给人的感觉是读的一片连续的内存。
FAT12文件系统中有两个FAT表项(FAT1和FAT2),他们内容是一样的,这里我们只关注FAT1即可,下图为FAT表与数据区物理组织示意图:
当我们读取一个文件项的内容时,可以获取DIR_FstClus(即存储的第一个扇区的位置),这里用C表示,当我们读完C内存处的数据库时,需要读下一个内存数据,对应到FAT表中查找位置,得到内存O的地址,然后读取数据区中O内存处的数据,一次类推,直到最后一个内存S对应的FAT表项没有内存地址(读取结束),这个过程,可以把数据区的C->O->Z->Q->S共5片内存的数据全部读取完成。FAT表与数据区的关系更直观一点则如下逻辑示意图:
逻辑示意图跟数据结构里边的链表是一样的。
以上是FAT表与数据区的基本关系,下边就通过FAT表来读取a.img软盘中的文件内容,在此之前有一些小点需要注意:
以下是代码,在上文代码基础上增加两个函数:获取FAT表ReadFat、读取文件内容ReadFileContent
#include
#include
#include
#include
#pragma pack(push)
#pragma pack(1)
struct Fat12Header
{
char BS_OEMName[8];
ushort BPB_BytsPerSec; //每扇区字节数,默认512字节
uchar BPB_SecPerClus; //每簇扇区数
ushort BPB_RsvdSecCnt;
uchar BPB_NumFATs;
ushort BPB_RootEntCnt; //最大跟目录文件数
ushort BPB_TotSec16;
uchar BPB_Media;
ushort BPB_FATSz16;
ushort BPB_SecPerTrk;
ushort BPB_NumHeads;
uint BPB_HiddSec;
uint BPB_TotSec32;
uchar BS_DrvNum;
uchar BS_Reserved1;
uchar BS_BootSig;
uint BS_VolID;
char BS_VolLab[11];
char BS_FileSysType[8];
};
//根目录项结构体
struct RootEntry
{
char DIR_Name[11];
uchar DIR_Attr;
uchar reserve[10];
ushort DIR_WrtTime;
ushort DIR_WrtDate;
ushort DIR_FstClus;
uint DIR_FileSize;
};
#pragma pack(pop)
void PrintHeader(Fat12Header& rf, QString p)
{
QFile file(p);
if( file.open(QIODevice::ReadOnly) )
{
QDataStream in(&file);
file.seek(3); //偏移开头3字节
in.readRawData(reinterpret_cast(&rf), sizeof(rf));
rf.BS_OEMName[7] = 0;
rf.BS_VolLab[10] = 0;
rf.BS_FileSysType[7] = 0;
qDebug() << "BS_OEMName: " << rf.BS_OEMName;
qDebug() << "BPB_BytsPerSec: " << hex << rf.BPB_BytsPerSec;
qDebug() << "BPB_SecPerClus: " << hex << rf.BPB_SecPerClus;
qDebug() << "BPB_RsvdSecCnt: " << hex << rf.BPB_RsvdSecCnt;
qDebug() << "BPB_NumFATs: " << hex << rf.BPB_NumFATs;
qDebug() << "BPB_RootEntCnt: " << hex << rf.BPB_RootEntCnt;
qDebug() << "BPB_TotSec16: " << hex << rf.BPB_TotSec16;
qDebug() << "BPB_Media: " << hex << rf.BPB_Media;
qDebug() << "BPB_FATSz16: " << hex << rf.BPB_FATSz16;
qDebug() << "BPB_SecPerTrk: " << hex << rf.BPB_SecPerTrk;
qDebug() << "BPB_NumHeads: " << hex << rf.BPB_NumHeads;
qDebug() << "BPB_HiddSec: " << hex << rf.BPB_HiddSec;
qDebug() << "BPB_TotSec32: " << hex << rf.BPB_TotSec32;
qDebug() << "BS_DrvNum: " << hex << rf.BS_DrvNum;
qDebug() << "BS_Reserved1: " << hex << rf.BS_Reserved1;
qDebug() << "BS_BootSig: " << hex << rf.BS_BootSig;
qDebug() << "BS_VolID: " << hex << rf.BS_VolID;
qDebug() << "BS_VolLab: " << rf.BS_VolLab;
qDebug() << "BS_FileSysType: " << rf.BS_FileSysType;
file.seek(510); //偏移512字节的最后两字节处
uchar b510 = 0;
uchar b511 = 0;
//读取最后两字节
in.readRawData(reinterpret_cast(&b510), sizeof(b510));
in.readRawData(reinterpret_cast(&b511), sizeof(b511));
qDebug() << "Byte 510: " << hex << b510;
qDebug() << "Byte 511: " << hex << b511;
}
file.close();
}
//获取一个根目录项
RootEntry FindRootEntry(Fat12Header& rf, QString p, int i)
{
RootEntry ret = {{0}};
QFile file(p);
//BPB_RootEntCnt为最大根目录文件数
if( file.open(QIODevice::ReadOnly) && (0 <= i) && (i < rf.BPB_RootEntCnt) )
{
QDataStream in(&file);
//定位到19扇区的各个根目录项开始处
file.seek(19 * rf.BPB_BytsPerSec + i * sizeof(RootEntry));
//每次只读一个根目录项
in.readRawData(reinterpret_cast(&ret), sizeof(ret));
}
file.close();
return ret; //返回读取到的根目录项
}
//获取指定文件名的根目录项
RootEntry FindRootEntry(Fat12Header& rf, QString p, QString fn)
{
RootEntry ret = {{0}};
for(int i=0; i= 0 )
{
QString n = fn.mid(0, d);
QString p = fn.mid(d + 1);
if( name.startsWith(n) && name.endsWith(p) )
{
ret = re;
break;
}
}
else
{
if( fn == name )
{
ret = re;
break;
}
}
}
}
return ret;
}
//打印根目录项
void PrintRootEntry(Fat12Header& rf, QString p)
{
//依次遍历每个根目录项并获取打印
for(int i=0; i ReadFat(Fat12Header& rf, QString p)
{
QFile file(p);
int size = rf.BPB_BytsPerSec * 9; //FAT表占用的大小(9个扇区字节)
uchar* fat = new uchar[size];
QVector ret(size * 2 / 3, 0xFFFF); //每个FAT表项占用1.5字节,FAT表个数为: 占用内存 / 1.5,默认值为0xFFFF
if( file.open(QIODevice::ReadOnly) )
{
QDataStream in(&file);
file.seek(rf.BPB_BytsPerSec * 1); //定位到第一个扇区(FAT表起始扇区)
in.readRawData(reinterpret_cast(fat), size); //读取FAT表内容
//分配规划FAT表
for(int i=0, j=0; i((fat[i+1] & 0x0F) << 8) | fat[i];
ret[j+1] = static_cast(fat[i+2] << 4) | ((fat[i+1] >> 4) & 0x0F);
}
}
file.close();
delete[] fat;
return ret;
}
//获取文件内容
QByteArray ReadFileContent(Fat12Header& rf, QString p, QString fn)
{
QByteArray ret;
RootEntry re = FindRootEntry(rf, p, fn); //读取设定文件名的根目录文件项
if( re.DIR_Name[0] != '\0' )
{
QVector vec = ReadFat(rf, p); //获取FAT表
QFile file(p);
if( file.open(QIODevice::ReadOnly) )
{
char buf[512] = {0};
QDataStream in(&file);
int count = 0;
ret.resize(re.DIR_FileSize);
//遍历所有簇获取文件内容
for(int i=0, j=re.DIR_FstClus; j<0xFF7; i+=512, j=vec[j])
{
//定位到文件数据扇区(33区),数据区起始地址对应编号为2,所以要-2
file.seek(rf.BPB_BytsPerSec * (33 + j - 2));
in.readRawData(buf, sizeof(buf));
for(uint k=0; k
main函数中调用读取文件内容函数ReadFileContent,读取TEST.TXT文件内容 如上便是TEXT.TXT文件中的内容。
如果对文件内容的读取不大理解,暂时不需要深究,只要知道FAT12文件系统是通过FAT表来将不同扇区的文件数据组织起来的,也就是数据结构中的单链表思想。
总结:
1、FAT12根目录记录了文件的文件名、起始簇号、文件长度
2、通过查找根目录区能够确定是否存在目标文件
3、FAT12文件数据离散的分布于存储介质不同的扇区中
4、文件数据通过FAT表进行关联,采用了单链表的思想
学自 --《狄泰软件学院》- 门徒操作系统