主引导程序格式化(FAT12) 在文章BIOS 中编写了一个最简单的主引导程序boot.bin
(第一句org 7c00
,最后一句db 0x55,db 0xaa
),并且将二进制代码boot.bin
输入到了虚拟软盘的起始位置并且成功运行了
主引导程序位于0扇区,以0x55aa结束标记,大小不可以超过512字节 512字节的大小代码量成为了一个限制,那么应该如何突破限制?
突破思路如下,主引导程序:
首先主引导程序本身要加载到内存中,并且完成最基本的初始化工作
然后主引导程序从存储介质中找到一段新程序并且将新程序加载到内存中
主引导程序将控制权交由新加载的程序执行
问题就变为了 主引导程序如何加载存储介质中的其它程序?
文件系统 :存储介质上组织文件数据的方法( 数据组织的方式 )
FAT12文件格式如上,引导扇区里有主引导程序,而FAT1和FAT2并无差别,可以相互备份
文件系统示例
FAT12 是 DOS 时代的早期文件系统
FAT12 结构非常简单,一直沿用于软盘
FAT12 的基本组织单位 • 字节( Byte ) : 基本数据单位 • 扇区( Sector ) : 磁盘中的最小数据单元 • 簇( Cluster ) : 一个或多个扇区
解决方案
使用FreeDos对软盘(data.img)进行FAT12 格式化
编写可执行程序(Loader),并将其拷贝到软盘中
主引导程序(Boot)在FAT12文件系统 中查找 Loader
将Loader
复制到内存中,并跳转到入口处执行
如何对软盘进行FAT12格式化 实验 : 往虚拟软盘中写入文件 原材料 : FreeDos , Bochs , bximage 步 骤 : 1.创建一块新的虚拟软盘 data.img
执行命令bximage
2.借助FreeDos这个原始的操作系统中对data.img进行格式化 (FreeDos格式化后就是FAT12格式)
橙色方框表示软盘data.img插入到FreeDos里面去,并且是B盘
bash执行bochs
启动FreeDos这个操作系统
在freedos中对B盘进行格式化format,这个时候B盘将拥有FAT12文件系统,但是这个时候并没有文件
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
自此我们拥有了一块虚拟软盘data.img,它拥有FAT12文件系统和创建的两个文件test.txt和loader.bin。 而这个文件格式就是FAT12文件系统,软盘拥有了特殊的数据组织方式。
读取FAT格式化后的软盘的文件系统信息(第0扇区) FAT12文件系统由引导程序区 , FAT表 , 根目录项表和文件数据区组成
主引导区:
存储介质的第0扇区,主引导区存储的比较重要的信息是文件系统的类型,文件系统逻辑扇区总数,每簇包含的扇区数,等。主引导区最后以0X55AA
两个字节作为结束,共占用1个扇区。
对于FAT12主引导程序 (偏移量62开始的位置处)和文件引导相关信息 都是存放在第0扇区的
实验 : 读取 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) { QFile file (p) ; if ( file.open (QIODevice::ReadOnly) ) { QDataStream in (&file) ; file.seek (3 ); in.readRawData (reinterpret_cast <char *>(&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 ); 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 (); }
此外尝试过打印引导代码,但是引导代码似乎是以二进制写入了,打印出来是乱码的,可以通过vscode以16进制方式查看,如下所示,引导代码似乎是打印一条语句。
接着将data.img作为启动盘,借助bochs启动data.img这个被FAT12格式化后的软盘
FreeDos中的 format 程序在格式化软盘的时候自动在第0扇区生成了1个主引导程序 , 这个主引导程序只打印1个字符串This is not a bootable disk.Please insert a bootable floppy and press any key to try again ...
文件格式和文件系统都是用于定义数据如何存放的规则 , 只要遵循这个规则就能够成功读写目标数据
小结:
主引导程序的代码量不能超过 512 字节
可以通过主引导程序加载新程序的方式突破限制
加载新程序需要依赖于文件系统
FAT12 是一种早期用于软盘的简单文件系统
FAT12 文件系统的重要信息存储于 0 扇区
加载指定的扩展程序 查找并打印根目录区目录文件项(19扇区) 有了文件系统,我们才能再写一个新程序并且从存储介质加载这个新目标程序,那么我们如何在 FAT12 根目录中查找到目标程序?
根目录区 根目录区起始于19扇区并且大小为14个扇区大小(7168B),文件项具体记录了文件相关信息(包括文件名,文件大小,文件的修改日期等) ,每个文件目录项32字节大小
根目录区的大小公式是BPB_RootEntCnt*sizeof(RootEntry) / BPB_ BytsPerSec
而BPB_RootEntCnt
(根目录区文件项数目)和BPB_ BytsPerSec
(512字节)这些参数都在第0扇区中记录了,sizeof(RootEntry)
是32字节大小
实验 : 读取data.img的根目录信息 步骤 :
根据上述表格创建RootEntry 结构体
使用文件流顺序定位到19扇区后依次读取每个项的内容
解析并打印根目录区的目录项相关的信息
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) { RootEntry ret = {{0 }}; QFile file (p) ; if ( file.open (QIODevice::ReadOnly) && (0 <= i) && (i < rf.BPB_RootEntCnt) ) { QDataStream in (&file) ; file.seek (19 * rf.BPB_BytsPerSec + i * sizeof (RootEntry)); 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; } } }
发现竟然打印出了4个目录项,但实际而言我只写了2个目录项test.txt和loader.bin。这是因为freedos格式化软盘,再将软盘挂载到linux,然后通过linux将文件内容test.txt和loader.bin写入拷贝到软盘当中,因此产生了不合法的目录文件项。
根据目录项加载指定文件数据 原理及步骤
先去在根目录区查找目标文件对应的目录项 (目录项记录了文件的详细信息)
获取目标文件的起始簇号和文件大小
再根据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表中对应的表项就是下一簇的起始位置
如图所示第0簇是C地址,根据第0簇对应的FAT表项知道下一簇起始地址是O,因此第1簇是O地址,根据第1簇对应的FAT表项知道下一簇起始地址是Z,因此第2簇的起始地址是Z地址….依此类推可以读到最后一个数据簇,最后一个数据簇对应的FAT表项的值是空,表示没有接下来的数据簇了
加载文件数据
先去在根目录区查找目标文件对应的目录项 (目录项记录了文件的详细信息)
获取目标文件的起始簇号和文件大小
再根据FAT表中记录的逻辑先后关系 读取数据
代码编写注意点:
FAT表中的**每个表项只占用12比特( 1.5字节)**并不是整数大小的字节数
FAT表一共记录了BPB_BytsPerSec* 9 *2/ 3
个表项(512 * 9 / 1.5
个)
可以使用一个short(2字节)表示一个表项(1.5字节)的值
如果表项值大于等于0xFF8 ,则说明已经到达最后一个簇
如果表项值等于0xFF7 ,则说明当前簇已经损坏
数据区起始簇(扇区)号为33地址为0x4200 数据区起始地址所对应的编号为2 ( 不为0 ),(因为FAT表项的第0个和第1个不可使用) 因此,DIR_FstClus 对应的地址为: 0x4200 + (DIR_FstClus - 2) * 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 QVector<ushort> ReadFat (Fat12Header& rf, QString p) { QFile file (p) ; int size = rf.BPB_BytsPerSec * 9 ; uchar* fat = new uchar[size]; QVector<ushort> ret (size * 2 / 3 , 0xFFFF ) ; if ( file.open (QIODevice::ReadOnly) ) { QDataStream in (&file) ; file.seek (rf.BPB_BytsPerSec * 1 ); in.readRawData (reinterpret_cast <char *>(fat), size); for (int i=0 , j=0 ; i<size; i+=3 , j+=2 ) { ret[j] = static_cast <ushort>((fat[i+1 ] & 0x0F ) << 8 ) | fat[i]; ret[j+1 ] = static_cast <ushort>(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<ushort> vec = ReadFat (rf, p); 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]) { file.seek (rf.BPB_BytsPerSec * (33 + j - 2 )); in.readRawData (buf, sizeof (buf)); for (uint k=0 ; k<sizeof (buf); k++) { if ( count < ret.size () ) { ret[i+k] = buf[k]; count++; } } } } file.close (); } return ret; }