主引导程序格式化(FAT12)

在文章BIOS中编写了一个最简单的主引导程序boot.bin(第一句org 7c00,最后一句db 0x55,db 0xaa),并且将二进制代码boot.bin输入到了虚拟软盘的起始位置并且成功运行了

主引导程序位于0扇区,以0x55aa结束标记,大小不可以超过512字节
512字节的大小代码量成为了一个限制,那么应该如何突破限制?

突破思路如下,主引导程序:

  1. 首先主引导程序本身要加载到内存中,并且完成最基本的初始化工作
  2. 然后主引导程序从存储介质中找到一段新程序并且将新程序加载到内存中
  3. 主引导程序将控制权交由新加载的程序执行

image-20220424152231008

问题就变为了 主引导程序如何加载存储介质中的其它程序?

文件系统:存储介质上组织文件数据的方法( 数据组织的方式 )

image-20220424152614428

FAT12文件格式如上,引导扇区里有主引导程序,而FAT1和FAT2并无差别,可以相互备份

文件系统示例

  1. FAT12 是 DOS 时代的早期文件系统
  2. FAT12 结构非常简单,一直沿用于软盘
  3. FAT12 的基本组织单位
    • 字节( Byte ) : 基本数据单位
    • 扇区( Sector ) : 磁盘中的最小数据单元
    • 簇( Cluster ) : 一个或多个扇区

解决方案

  1. 使用FreeDos对软盘(data.img)进行FAT12 格式化
  2. 编写可执行程序(Loader),并将其拷贝到软盘中
  3. 主引导程序(Boot)在FAT12文件系统中查找 Loader
  4. Loader复制到内存中,并跳转到入口处执行

如何对软盘进行FAT12格式化

实验 : 往虚拟软盘中写入文件
原材料 : FreeDos , Bochs , bximage
步 骤 :
1.创建一块新的虚拟软盘 data.img

执行命令bximage

2.借助FreeDos这个原始的操作系统中对data.img进行格式化(FreeDos格式化后就是FAT12格式)

橙色方框表示软盘data.img插入到FreeDos里面去,并且是B盘

image-20220424161711715

bash执行bochs启动FreeDos这个操作系统

在freedos中对B盘进行格式化format,这个时候B盘将拥有FAT12文件系统,但是这个时候并没有文件

image-20220424161608967

3.将 data.img 挂载到 Linux中 , 并写收入文件

1
2
3
4
5
6
fengyun@ubuntu:~/share/os$ sudo mount -o loop data.img /mnt/hgfs 
fengyun@ubuntu:~/share/os$ vim test.txt
fengyun@ubuntu:~/share/os$ vim loader.bin
fengyun@ubuntu:~/share/os$ sudo cp test.txt /mnt/hgfs/
fengyun@ubuntu:~/share/os$ sudo cp loader.bin /mnt/hgfs/
fengyun@ubuntu:~/share/os$ sudo umount /mnt/hgfs

image-20220424161016875

自此我们拥有了一块虚拟软盘data.img,它拥有FAT12文件系统和创建的两个文件test.txt和loader.bin。
而这个文件格式就是FAT12文件系统,软盘拥有了特殊的数据组织方式。

读取FAT格式化后的软盘的文件系统信息(第0扇区)

FAT12文件系统由引导程序区 , FAT表 , 根目录项表和文件数据区组成

image-20220424162657093

主引导区:

存储介质的第0扇区,主引导区存储的比较重要的信息是文件系统的类型,文件系统逻辑扇区总数,每簇包含的扇区数,等。主引导区最后以0X55AA两个字节作为结束,共占用1个扇区。

对于FAT12主引导程序(偏移量62开始的位置处)和文件引导相关信息都是存放在第0扇区的

image-20220424162936866

实验 : 读取 data.img 中的文件系统信息
步 骤 :
• 创建 Fat12Header 结构体类型
• 使用文件流读取前 512 字节的内容
• 解析并打印相关的信息

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <QtCore/QCoreApplication>
#include <QFile>
#include <QDataStream>
#include <QDebug>

#pragma pack(push)
#pragma pack(1)

struct Fat12Header
{
char BS_OEMName[8];
ushort BPB_BytsPerSec;
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];
};

#pragma pack(pop)

void PrintHeader(Fat12Header& rf, QString p)//打开p(文件路径)
{
QFile file(p);

if( file.open(QIODevice::ReadOnly) )
{
QDataStream in(&file);

file.seek(3);//定位到3字节地方,因为存储介质的第0扇区即主引导区的第三个字节才开始存储内容

in.readRawData(reinterpret_cast<char*>(&rf), sizeof(rf));//读取表格信息,读取到Fat12Header结构体当中

rf.BS_OEMName[7] = 0;//赋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);

uchar b510 = 0;
uchar b511 = 0;

in.readRawData(reinterpret_cast<char*>(&b510), sizeof(b510));
in.readRawData(reinterpret_cast<char*>(&b511), sizeof(b511));

qDebug() << "Byte 510: " << hex << b510;
qDebug() << "Byte 511: " << hex << b511;
}

file.close();
}

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

Fat12Header f12;

PrintHeader(f12, "E:\\data.img");

return a.exec();
}

image-20220428160022523

此外尝试过打印引导代码,但是引导代码似乎是以二进制写入了,打印出来是乱码的,可以通过vscode以16进制方式查看,如下所示,引导代码似乎是打印一条语句。

image-20230214224027314

接着将data.img作为启动盘,借助bochs启动data.img这个被FAT12格式化后的软盘

image-20220424164339763

  1. FreeDos中的 format 程序在格式化软盘的时候自动在第0扇区生成了1个主引导程序 , 这个主引导程序只打印1个字符串This is not a bootable disk.Please insert a bootable floppy and press any key to try again ...
  2. 文件格式和文件系统都是用于定义数据如何存放的规则 , 只要遵循这个规则就能够成功读写目标数据

小结:

  1. 主引导程序的代码量不能超过 512 字节
  2. 可以通过主引导程序加载新程序的方式突破限制
  3. 加载新程序需要依赖于文件系统
  4. FAT12 是一种早期用于软盘的简单文件系统
  5. FAT12 文件系统的重要信息存储于 0 扇区

加载指定的扩展程序

查找并打印根目录区目录文件项(19扇区)

有了文件系统,我们才能再写一个新程序并且从存储介质加载这个新目标程序,那么我们如何在 FAT12 根目录中查找到目标程序?

根目录区

根目录区起始于19扇区并且大小为14个扇区大小(7168B),文件项具体记录了文件相关信息(包括文件名,文件大小,文件的修改日期等),每个文件目录项32字节大小

image-20220424174312858

根目录区的大小公式是BPB_RootEntCnt*sizeof(RootEntry) / BPB_ BytsPerSec
BPB_RootEntCnt(根目录区文件项数目)和BPB_ BytsPerSec(512字节)这些参数都在第0扇区中记录了,sizeof(RootEntry)是32字节大小

实验 : 读取data.img的根目录信息

步骤 :

  1. 根据上述表格创建RootEntry 结构体
  2. 使用文件流顺序定位到19扇区后依次读取每个项的内容
  3. 解析并打印根目录区的目录项相关的信息
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
struct RootEntry
{
char DIR_Name[11];
uchar DIR_Attr;
uchar reserve[10];
ushort DIR_WrtTime;
ushort DIR_WrtDate;
ushort DIR_FstClus;
uint DIR_FileSize;
};

RootEntry FindRootEntry(Fat12Header& rf, QString p, int i)//查找根目录区第i个文件项内容并返回RootEntry结构体
{
RootEntry ret = {{0}};

QFile file(p);//打开虚拟软盘文件
//先读取 data.img 第0扇区中的文件系统信息,通过文件系统信息才能再定位到文件目录项
if( file.open(QIODevice::ReadOnly) && (0 <= i) && (i < rf.BPB_RootEntCnt) )//i范围在文件项最小和最大数目之间
{
QDataStream in(&file);

file.seek(19 * rf.BPB_BytsPerSec + i * sizeof(RootEntry));//定位到19扇区起始地址处并且读取第i个文件项的位置

in.readRawData(reinterpret_cast<char*>(&ret), sizeof(ret));//读取内容
}

file.close();

return ret;
}

RootEntry FindRootEntry(Fat12Header& rf, QString p, QString fn)//根据文件名查找对应的文件目录项
{
RootEntry ret = {{0}};

for(int i=0; i<rf.BPB_RootEntCnt; i++)//遍历根目录区的每一个目录项,匹配文件名是否是我想要的文件目录项
{
RootEntry re = FindRootEntry(rf, p, i);

if( re.DIR_Name[0] != '\0' )
{
int d = fn.lastIndexOf(".");
QString name = QString(re.DIR_Name).trimmed();

if( d >= 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<rf.BPB_RootEntCnt; i++)
{
RootEntry re = FindRootEntry(rf, p, i);

if( re.DIR_Name[0] != '\0' )//文件名不为空才打印信息
{
qDebug() << i << ":";
qDebug() << "DIR_Name: " << hex << re.DIR_Name;
qDebug() << "DIR_Attr: " << hex << re.DIR_Attr;
qDebug() << "DIR_WrtDate: " << hex << re.DIR_WrtDate;
qDebug() << "DIR_WrtTime: " << hex << re.DIR_WrtTime;
qDebug() << "DIR_FstClus: " << hex << re.DIR_FstClus;
qDebug() << "DIR_FileSize: " << hex << re.DIR_FileSize;
}
}
}

image-20220428162420944

发现竟然打印出了4个目录项,但实际而言我只写了2个目录项test.txt和loader.bin。这是因为freedos格式化软盘,再将软盘挂载到linux,然后通过linux将文件内容test.txt和loader.bin写入拷贝到软盘当中,因此产生了不合法的目录文件项。

根据目录项加载指定文件数据

原理及步骤

  1. 先去在根目录区查找目标文件对应的目录项(目录项记录了文件的详细信息)
  2. 获取目标文件的起始簇号和文件大小
  3. 根据FAT表中记录的逻辑先后关系读取数据

目录项的中的关键成员

DIR_Name 文件名( 用于判断是否为目标文件 )
DIR_FstClus 文件数据起始存储位置( 用于确定读取位置 )
DIR_FileSize 文件大小( 用于确定读取的字节数 )

文件的数据是以簇为单位来存储的,FAT12文件一簇就是一扇区即512字节,但是我们的文件总是大于512字节的

假设一个文件需要5个簇存储,是需要离散的还是连续的呢?非常可能的是离散存储的。
那么如何区分各个簇的先后关系?那么就要通过FAT表来标识。

FAT表 — FAT12 的数据组织核心

  • FAT1和 FAT2 是相互备份的关系,数据内容完全一致
  • FAT表是一个关系图,记录了文件数据的先后关系
  • 每一个FAT表项占用12比特(1.5B)
  • FAT表的前两个表项规定不使用

FAT表中的先后关系

以簇( 扇区 ) 为单位存储文件数据

每个表项(vec[i] )表示文件数据的实际位置(第i簇的位置 )

  • DIR_FstClus 表示文件第 0 簇( 扇区 ) 的位置
  • vec[DIR_FstClus] 表示文件第1簇( 扇区 ) 的位置
  • vec[ vec[DIR_FstClus] ] 表示文件第 2 簇( 扇区) 的位置

FAT表和数据区存在一一映射关系,每个簇在FAT表中对应的表项就是下一簇的起始位置

image-20220428164412942

如图所示第0簇是C地址,根据第0簇对应的FAT表项知道下一簇起始地址是O,因此第1簇是O地址,根据第1簇对应的FAT表项知道下一簇起始地址是Z,因此第2簇的起始地址是Z地址….依此类推可以读到最后一个数据簇,最后一个数据簇对应的FAT表项的值是空,表示没有接下来的数据簇了

加载文件数据

  1. 先去在根目录区查找目标文件对应的目录项(目录项记录了文件的详细信息)
  2. 获取目标文件的起始簇号和文件大小
  3. 根据FAT表中记录的逻辑先后关系读取数据

代码编写注意点:

  1. FAT表中的**每个表项只占用12比特( 1.5字节)**并不是整数大小的字节数
  2. FAT表一共记录了BPB_BytsPerSec* 9 *2/ 3个表项(512 * 9 / 1.5个)
  3. 可以使用一个short(2字节)表示一个表项(1.5字节)的值
  4. 如果表项值大于等于0xFF8 ,则说明已经到达最后一个簇
  5. 如果表项值等于0xFF7 ,则说明当前簇已经损坏
  6. 数据区起始簇(扇区)号为33地址为0x4200
    数据区起始地址所对应的编号为2 ( 不为0 ),(因为FAT表项的第0个和第1个不可使用)
    因此,DIR_FstClus 对应的地址为: 0x4200 + (DIR_FstClus - 2) * 512

image-20220428172221986

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
QVector<ushort> ReadFat(Fat12Header& rf, QString p)
{
QFile file(p);
int size = rf.BPB_BytsPerSec * 9;//FAT表的大小,9个扇区的大小
uchar* fat = new uchar[size]; //存储之间从文件系统读出来的FAT表
QVector<ushort> ret(size * 2 / 3, 0xFFFF);//定义返回值对象,应有 FAT表大小/1.5 这么多个

if( file.open(QIODevice::ReadOnly) )//只读方式打开文件
{
QDataStream in(&file);

file.seek(rf.BPB_BytsPerSec * 1);//定位到FAT表的位置,定位到第1个扇区即FAT1表的起始位置

in.readRawData(reinterpret_cast<char*>(fat), size);//将FAT表读取出来到uchar* fat中

for(int i=0, j=0; i<size; i+=3, j+=2)//一个表项1.5字节大小
{
ret[j] = static_cast<ushort>((fat[i+1] & 0x0F) << 8) | fat[i];//取出第i+1个字节低4位然后左移8位和第i个字节组成第j项
ret[j+1] = static_cast<ushort>(fat[i+2] << 4) | ((fat[i+1] >> 4) & 0x0F);
}//fat表存储到ret表中
}

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<ushort> 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])
//开始读了,j表示读取数据簇的地址,i表示返回组的下标,
{
file.seek(rf.BPB_BytsPerSec * (33 + j - 2));

in.readRawData(buf, sizeof(buf));//簇里面的数据读到buf里面去

for(uint k=0; k<sizeof(buf); k++)
{
if( count < ret.size() )
{
ret[i+k] = buf[k];//将buf数据拷贝到返回对象ret中
count++;
}
}
}
}
file.close();
}
return ret;
}