看前提醒:这一系列笔记完全是按照我的思考顺序写的,中间可能会绕弯路
定义
为了避免概念的混淆,我先在这里作一下(仅适用于本文的)名词的解释:
- 引导程序 /boot程序:特指磁盘MBR或者VBR扇区中存放的程序
- 加载器 /loader程序:指由引导程序加载执行的程序,用于加载操作系统的内核
- 引导:指从BIOS调转到引导程序到真正执行内核之间的整个流程
背景
在研究引导程序时,我发现很多书籍和文章都设定系统从软盘引导,因此引导扇区的代码通常是直接写死,从第一个软盘读取加载器(loader)。为了迁移到硬盘引导,顺便改成一个通用的引导,我参考了IBM的BIOS文档进行设计。
BIOS中关于引导的规定
由于引导扇区仅有512字节 大小,所以需要有引导程序加载另外的loader程序来打破大小的限制,否则仅凭这点空间什么都做不了。首先,boot程序无法确定引导位于哪个磁盘上也就无法加载loader,很多的书也都是默认在第一个软盘 上来写的,如果需要读取磁盘中的loader就需要磁盘的设备号。
但考虑到现在的引导都没有这种问题,肯定是由解决方案的。传统BIOS引导都遵循着IBM的规范,所以我决定到IBM的文档中去寻找答案。
于是经过一番快速搜索,我在BIOS的中断调用功能中找到了可能的答案:
根据IBM BIOS文档,INT 19 中断功能的目的是启动Bootstrap Loader。在调用该中断后时,BIOS会设置以下寄存器:
- CS = 0000H,IP = 7C00H:指定引导扇区的内存加载地址。
- DL:保存引导设备的驱动器号。
看到这个7C00H 就感觉DNA动了,虽然文档表明这似乎是用于从指定驱动器重新引导的功能,但我们不妨大胆的猜测在初次启动Boot程序 是也是同样的状态,因为重新从磁盘引导时磁盘上的引导程序大概率也是按照直接启动的情况写的。
实践验证
为了验证引导过程的具体行为,我使用了QEMU 配合GDB进行调试,在0x7C00处设置断点并观察寄存器的值。测试结果如下:
- 当参数指定从软盘(0号软盘) 引导时,DL = 0x00。
- 当参数指定从硬盘(0号硬盘) 引导时,DL = 0x80。
由于在BIOS中:
- 软盘驱动器号范围为 0x00 ~ 0x7F。
- 硬盘驱动器号范围为 0x80 ~ 0xFF。
由此可以确定,启动时,引导设备的驱动器号会保存在DL寄存器中。
引导程序设计
基于以上分析,我的引导程序设计思路如下:
- 保存设备号:在引导程序运行时,直接保存DL的值,作为后续加载程序(loader)的驱动器号。这使得引导程序能够同时兼容新老磁盘。
- 读取磁盘功能:
- 支持软盘的标准CHS(柱面-磁头-扇区)寻址模式读取。
- 支持硬盘的扩展LBA (逻辑块地址)模式读取,适配超过CHS寻址范围 (约8GB)的大容量硬盘。
设计思路
- 引导程序(boot)的设计 :
- 位置:引导程序位于硬盘的第0扇区,同时兼容MBR(主引导记录)。
- 结构:从扇区偏移0x1BE开始的64字节保留用于保存分区表信息。
- 加载程序(loader)的功能 :
- 遍历分区表,寻找唯一具有可引导标志的分区。
- 读取启动磁盘参数,选择CHS或LBA模式,读取目标分区的第0扇区。
- 仅支持FAT文件系统(也许兼容FAT系列文件系统)。
- 检测FAT的BPB(BIOS参数块)结构,根据BPB信息定位到根目录的数据扇区。
- 循环读取文件目录项,找到名为KERNEL.ELF的文件(FAT短目录项文件名均为大写),并加载到内存中。
- 内核加载 :
- 通过支持FAT文件系统的设计,内核文件的大小可以动态变化,并且能够被文件管理系统灵活修改。
- 读取内核文件到内存后,跳转执行,完成引导流程。
注意事项
在实现过程中需要特别注意:
- 使用DX寄存器时,应提前保存设备号,以免在后续操作中丢失。
- 引导程序应尽可能简洁,以节省空间并提高兼容性。
结局
咳咳,看到这个小标题就能说明不会有什么好下场。实际上原本经过好一番折磨终于在16位模式下写出了加载内核的汇编代码,先是因为16位模式的寻址范围限制在1MB导致BIOS加载的时候直接卡住,原本想着留着做纪念的结果又不小心把他给删了TAT