进入保护模式不是一件很困难的事情,按照指定的步骤进行操作就可以了。但是我们要做的事情是很多的,如果我们总是局限在512字节的引导扇区上,那么很多事情就都做不了了。这显然不是我们想要的,我们需要一个方法帮助我们突破这512字节的引导扇区的限制,让我们可以做更多的事情。
虽然引导扇区小,但是我们现在使用的软盘容量却大的多。所以,可以再建立一个文件,将其通过引导扇区加载入内存,然后将控制权交给它。这样,512字节的限制就没有了。
那么,被引导扇区加载进内存的是不是就应该是操作系统内核了呢?一个操作系统从开机到开始运行,大致经历“引导 -> 加载内核入内存 -> 跳入保护模式 -> 开始执行内核”这样一个过程。也就是说,在内核开始执行之前不但要加载内核,而且还有准备保护模式等一些列工作,如果全都交给引导扇区来做,512字节有可能是不够用的,所以,我们把这个过程交给另外的模块来完成,我们把这个模块叫做 Loader。引导扇区负责把 Loader 加载入内存并且将控制权交给它,其它工作都交给 Loader 来做,因为它没有512字节的限制,可以做更多的事情。
在这里,书上记录的方式是将软盘做成 FAT12 格式,这样对 Loader 以及今后的 Kernel(内核)的操作将会简单易行。
FAT12
FAT12 是DOS时代就开始使用的文件系统(File System),直到现在仍然在软盘上使用(可能现在已经有很多人没有见过软盘了,而且软盘也不再被电脑使用,但是本书就是使用这样的方式。我个人理解的是,如果将这种方式理解了,那么换成其它的,应该也是一样的操作方式,比如FAT32)。几乎所有的文件系统都会把磁盘划分为若干层次以方便组织和管理,这些层次包括:
- 扇区(Sector):磁盘上的最小数据单元。
- 簇(Clust):一个或多个扇区。
- 分区(Partition):通常指整个文件系统。
引导扇区是整个软盘的第 0 个扇区,在这个扇区中有一个很重要的数据结构叫做 BPB(BIOS Parameter Block),引导扇区的格式如下表所示,其中名称以BPB_开头的域属于BPB,以BS_ 开头的域不属于BPB,只是引导扇区的一部分。
名称 | 偏移 | 长度 | 内容 | Orange's 的值 |
BS_jmpBoot | 0 | 3 | 一个短跳转指令 | jmp LABEL_START nop |
BS_OEMName | 3 | 8 | 厂商名 | 'ForrsetY' |
BPB_BytePerSec | 11 | 2 | 每扇区字节数 | 0x200 |
BPB_SecPerClus | 13 | 1 | 每簇扇区数 | 0x1 |
BPB_RsvdSecCnt | 14 | 2 | Boot记录占用多少扇区 | 0x1 |
BPB_NumFATs | 16 | 1 | 共有多少个FAT表 | 0x2 |
BPB_RootEntCnt | 17 | 2 | 根目录文件数最大值 | 0xE0 |
BPB_TotSec16 | 19 | 2 | 扇区总数 | 0xB40 |
BPB_Media | 21 | 1 | 介质描述符 | 0xF0 |
BPB_FATSz16 | 22 | 2 | 每个FAT扇区数 | 0x9 |
BPB_SecPerTrk | 24 | 2 | 每磁道扇区数 | 0x12 |
BPB_NumHeads | 26 | 2 | 磁头数(面数) | 0x2 |
BPB_HiddSec | 28 | 4 | 隐藏扇区数 | 0 |
BPB_TotSec32 | 32 | 4 | 如果BPB_TotSec16是0,由这个值记录扇区数 | 0 |
BS_DrvNum | 36 | 1 | 中断13的驱动器号 | 0 |
BS_Reservedl | 37 | 1 | 未使用 | 0 |
BS_BootSig | 38 | 1 | 扩展引导标记(29h) | 0x29 |
BS_VolID | 39 | 4 | 卷序列号 | 0 |
BS_VolLab | 43 | 11 | 卷标 | 'OrangeS0.02' |
BS_FileSysType | 54 | 8 | 文件系统类型 | 'FAT12' |
引导代码及其它 | 62 | 448 | 引导代码、数据及其它填充字符等 | 引导代码(剩余空间被0填充) |
结束标志 | 510 | 2 | 0xAA55 | 0xAA55 |
紧接着引导扇区的是两个完全相同的FAT表,每个占用9个扇区。第二个FAT之后是根目录区的第一个扇区。根目录区的后面是数据区,如下图所示。
接下来要做的事情就是把 Loader 复制到软盘上并让引导扇区找到并加载它,那么就来看一下引导扇区通过怎样的步骤才能找到文件,以及如何把文件内容全部都读出来并放进内存里。为简单起见,我们规定 Loader 只能放在根目录中,而根目录信息放在FAT2后面的根目录区中。那么,先来看一下根目录区。
根目录区位于第二个FAT表之后,开始的扇区编号为19,它由若干个目录条目(Directory Entry)组成,条目最多有 BPB_RootEntCnt 个。由于根目录区的大小是依赖于BPB_RootEntCnt的,所以长度不固定。根目录区的每一个条目占用 32 字节,它的格式如下表所示。
名称 | 偏移 | 长度 | 描述 |
DIR_Name | 0 | 0xB | 文件名8字节,扩展名3字节 |
DIR_Attr | 0xB | 1 | 文件属性 |
保留位 | 0xC | 10 | 保留位 |
DIR_WrtTime | 0x16 | 2 | 最后一次写入时间 |
DIR_WrtDate | 0x18 | 2 | 最后一次写入日期 |
DIR_FstClus | 0x1A | 2 | 此条目对应的开始簇号 |
DIR_FileSize | 0x1C | 4 | 文件大小 |
条目格式结构并不复杂,但是我们要是能直观看到一个真实的目录条目就好了。这并不难做到,我们可以使用上一篇文章创建的虚拟软盘 fd.img,然后把它作为 FreeDos 的A盘,格式化后就可以方便地往其中添加文件和目录了。这样,当我们想查看它的格式时,只需要用二进制查看器打开 fd.img 就可以了。
接下来,按照书上所示,通过FreeDos在这张虚拟软盘中添加以下2个文本文件进行查看:
- RIVER.TXT,内容为 riverriverriver。
- FLOWER.TXT,内容输入大量字符(重复单词:flower),使该文件大小超过512字节,用来测试文件跨越扇区的情况。
完成后,我们使用二进制查看器打开 fd.img 文件进行查看。我这里选用的二进制文件查看器是:Bz1621.lzh,你可以选择自己习惯的二进制查看器软件进行使用。
由于根目录区是从第19扇区开始,每个扇区512字节,所以根目录区第一个字节位于偏移 19×512=9728(十进制)=0x2600(十六进制)处。现在使用Bz二进制查看器软件打开 fd.img 文件,转到偏移 0x2600处查看一下内容,如下图所示:
看到这里,可以清楚的看到创建的两个文件的单词 RIVER、FLOWER,那么以 RIVER.TXT 为例,按照条目格式结构进行分析理解(DOS是小端存储)。
名称 | 偏移 | 长度 | 描述 | 值 |
DIR_Name | 0 | 0xB | 文件名8字节,扩展名3字节 | RIVER TXT |
DIR_Attr | 0xB | 1 | 文件属性 | 0x20 |
DIR_WrtTime | 0x16 | 2 | 最后一次写入时间 | 0x553A |
DIR_WrtDate | 0x18 | 2 | 最后一次写入日期 | 0x5787 |
DIR_FstClus | 0x1A | 2 | 此条目对应的开始簇号 | 0x0002 |
DIR_FileSize | 0x1C | 4 | 文件大小 | 0x00000011 |
文件名和大小很容易理解,时间日期信息对我们没用。当我们寻找Loader时,只要发现文件名正确就认为它是我们要找的那个文件。最后剩下最重要的信息DIR_FstClus,即文件开始簇号,它告诉我们文件存放在磁盘的什么位置,从而让我们找到它。由于一个簇只包含一个扇区,所以为了简化计算过程,下文中将“簇”替换成“扇区”进行记录。
需要注意的是,数据区的第一个簇的簇号是2,而不是0或者1。RIVER.TXT的开始簇号是2,也就是说,此文件的数据开始于数据区第一个簇。数据区的第一个簇即第一个扇区位置在哪里呢?这需要通过计算根目录区所占的扇区数才能知道。我们既然已经知道了根目录区条目最多有BPB_RootEntCnt个,扇区数也就可以计算出来了,假设根目录区共占用 RootDirSectors 个扇区,则有公式:
将数字带入公式,结果取整,得到 RootDirSectors=14。所以:数据区开始扇区号=根目录区开始区号+14=19+14=33。第33扇区偏移量为512×33=16896,转换为16进制为:0x4200,让我们到这个位置看一下里面的内容。从下图中可以看出,和我们写入的文件内容是一样的。
这时可能会有一个问题,那就是我们已经通过根目录区找到了文件并看到了内容,那我们还要FAT表做什么呢?实际上,对于小于512字节的文件来说,FAT表用处不大,但是如果文件大于512字节,我们就需要用FAT表来找到所有的簇(扇区),毕竟文件大于512字节还是很常见的。
FAT表有两个,FAT2可看做是FAT1的备份,它们通常是一样的。那么FAT表的结构是什么样的呢?我们还是先来一点直观的认识,FAT1的扇区编号是1,偏移512字节,转换为16进制是0x200,我们到这个位置看一下里面的内容。如下图:
根本看不懂里面的内容是什么。书上讲的是,这里的内容有点像一个位图,其中,每12位称为一个FAT项(FATEntry),代表一个簇。第0个和第1个FAT项始终不使用,从第2个FAT项开始表示数据区的每一个簇,依此类推。
需要注意的是,由于每个FAT项占12位,包含一个字节和另一个字节的一半,所以显得有点别扭。具体是这样的,假设连续3个字节分别如下图所示,那么灰色框表示的是前一个FAT项(FATEntry1),BYTE1是FATEntry1的低8位,BYTE2的低4位是FATEntry1的高4位;白色框表示的是后一个FAT项(FATEntry2),BYTE2的高4位是FATEntry2的低4位,BYTE3是FATEntry2的高8位。
通常,FAT项的值代表的是文件下一个簇号,但如果值大于或等于0xFF8,则表示当前簇已经是本文件的最后一个簇。如果值为0xFF7,表示它是一个坏簇。文件中的簇号和FAT项相对应,例如文件RIVER.TXT的开始簇号是2,对应FAT表中第2个FAT项,该项的值为0xFFF,该值大于0xFF7,表示这个簇已经是最后一个簇了。
接下来我们来看一下FLOWER.TXT文件,这个文件比较大,超过512字节,看一下它对应的FAT项是什么样的。FLOWER.TXT文件的簇号为3,对应第3个FAT项,该项的值为0x004,也就是说,这个簇不是文件的最后一个簇,下一个簇号为4。我们再看第4个FAT项,该项值为0x005;不是最后一个簇,接着看第5个FAT项,该项值为0x006;往下看第6个FAT项,该项值为0xFFF,大于0xFF8,为最后一个簇。所以,FLOWER.TXT占用了第3、4、5、6,共计4个簇。第3个簇对应的是第34个扇区,第34扇区偏移量为512×34=17408,转换为16进制为:0x4400。让我们到这里看一下里面的内容(如下图),可以看到好多单词“flower”,和写入文件的内容是一样的。
这里需要注意一点,一个FAT项可能会跨越两个扇区,这种情况在编码实现的过程中要考虑在内。
好了,到这里为止,如何在一个软盘中找到自己想要的文件应该已经清楚了。