从零开始写 OS 内核 - BIOS 启动到实模式

系列目录

接上一篇准备工做,从这篇开始咱们将进入 boot loader 的编写。网上有一些相似的教程可能跳过了这个阶段,直接为你准备好了 boot loader,从而你能够直接开始 kernel 的编写,例如以前推荐的 JamesM's kernel development tutorials 就是这样的。不过我仍是强烈建议将 boot loader 也本身实现了,尤为是对初学者,缘由以下:java

  • 它并不困难,相比于后面的 kernel;
  • 有助于你快速提升汇编能力,这在后面的 C 语言 kernel 编写、调试中仍然很重要;
  • boot 裸机运行阶段的的编程有助于你创建起对磁盘、内存、指令和 data 之间的加载、映射关系的正确认识,为后面的内核、可执行程序的加载,以及虚拟内存的创建作好准备,尤为是若是你感受对这一块比较模糊的话;
  • boot 阶段其实会初步搭建起 segment 以及虚拟内存的框架,为后续 kernel 编写打下基础;

开机进入 BIOS

这是一个经典的问题,就是计算机主板开机上电后到启动,发生了什么?git

首先咱们须要知道开机后 CPU 和内存所处的状态,开机后 CPU 初始模式是实模式,地址宽度为 20 位,即最大地址空间 1MB。这 1MB 空间的划分是固定的,每一块都有规定的用途的,被映射到不一样的设备上:shell

BIOS 的工做

咱们来看一下开机后发生的事情:编程

  1. 开机后 CPU 的指令寄存器 ip 被强置为地址 0xFFFF0,这一地址被映射到 BIOS 固件上的代码,这就是计算机开机后的第一条指令的地址;
  2. CPU 开始执行 BIOS 上的代码,这一部分主要是硬件输入输出设备相关的检查,以及创建一个最初的中断向量表,目前没必要深究;
  3. BIOS 代码最后阶段的工做,就是检查启动盘上的 mbr 分区,所谓 mbr 分区就是磁盘上的第一个 512B 内容,又叫引导分区;BIOS 会对这 512B 作一个检查:它的最后2个字节必须是两个 magic number:0x550xaa,不然它就不是一个合法的启动盘;
  4. 检查经过后,BIOS 将这 512B 加载到内存 0x7C00 处,到 0x7E00 为止,而后指令跳转到 0x7C00 开始执行;至此 BIOS 退出舞台;

将上面那张表格画成图,去掉干扰项,只留下咱们关心的部分:segmentfault

  • 黄色部分是加载到内存的 mbr,起始地址 0x7C00;
  • 白色部分是咱们后面能够自由使用的内存空间;
  • 斜线阴影部分为 BIOS 代码;

图中标出了 BIOS 的主要工做流程,从地址 0xFFFF0 开始,通过一系列代码执行,最终校验并读取磁盘第一个 512B 扇区,加载到黄色部分即为 mbr,地址为 0x7C00,而后指令跳转过去,进入 mbr 的执行;bash

mbr 的工做

那么 mbr 须要作什么事情?由于 mbr 大小被限制在了 512B,你不可能在里面放不少代码和数据,因此它最重要的工做只有一个:多线程

  • 将后面的 loader 部分从磁盘加载到内存,并跳转到 loader 继续执行;

内存布局规划

因此咱们须要规划一下整个 boot load 阶段的内存布局,这里咱们直接给出磁盘以及内存的全貌:框架

咱们目前重点关注内存 1MB 如下部分的内容,不一样部分用了不一样的颜色标识出来:函数

  • 黄色:mbr
  • 蓝色:loader
  • 白色:可自由使用

通过 BIOS 的工做,如今指令已经来到了 mbr 部分,它须要将蓝色部分的 loader 从磁盘上加载到内存,地址就定为 0x8000,注意这个地址能够自由指定的,只要在图中白色区域内,而且空间足够用便可。咱们的 loader 部分也不会很大,按照比较富余地估计,4KB 足以。

mbr 代码

先给出我项目里的代码,路径为 src/boot/mbr.S,供你参考。

首先关注开始部分:

SECTION mbr vstart=MBR_BASE_ADDR

MBR_BASE_ADDR 定义在了 boot.inc 中,为 0x7C00,这表示了整个 mbr 里的内容都是从 0x7C00 开始编址,包括代码和数据。这一点很是重要,由于咱们已经提早知道了 BIOS 会将 mbr 加载到这个位置,因此整个 mbr 里的内容的编址必须从这里开始,这样 BIOS 在跳转到 mbr 的第一条指令后,后续对 mbr 中代码和数据的访问才能正确寻址。

mbr 的入口我标记为了 mbr_entry,后面我定义了几个函数,后面的讲解咱们不妨用 C 语言给它作注释:

void init_segments();

这里初始化了几个 segment 寄存器,初始值均为 0,这表示 flat mode 的段内存分布方式,如今你也没必要深究。另外我还将 stack 移动到了 0x7B00 的位置,这只是自由发挥,彻底不是必须的。

接下来加载 loader:

void load_loader_img();
// 这个函数的汇编代码直接使用寄存器传参。
void read_disk(short load_mem_addr,
               short load_start_sector,
               short sectors_num);

这里就是 mbr 最主要的工做,把 loader 从磁盘上加载进来到内存 0x8000 的位置,
read_disk 三个参数传参分别为:

// loader 加载地址为 0x8000;
short load_mem_addr = LOADER_BASE_ADDR;
// loader 镜像在磁盘上起始位置为第 2 个sector,紧接着 mbr 以后;
short load_start_sector = 1;
// loader 大小为 8 个 sectors,共 4KB;
short sectors_num = 8;

read_disk 函数涉及到了读取磁盘,须要用到一堆 CPU 控制磁盘的端口和中断功能,你须要查阅文档使用,冗长繁杂,我是照搬了《操做系统真相还原》一书第三章的内容。你其实也没必要深究,拿来用就能够,只须要知道它作了什么工做便可。

加载完 loader 以后,就能够跳转到 loader 地址 0x8000 执行:

jmp LOADER_BASE_ADDR

整个从 BIOS -> mbr -> loader 的指令运行跳转流程以下图所示。loader 部分用浅蓝色阴影标出,由于它实际上目前没有有效数据,等待咱们后续将它实现并加载入内存:

最后还有个关键的小东西:
这一通代码下来,所用的空间还远未到 512B,咱们将剩余的空间所有用 0 填充(其实随便填什么都行,反正执行不到),最后在 512B 的末尾处写上 0x550xaa 两个 magic number:

times 510-($-$$) db 0
db 0x55, 0xaa

至此 mbr 便编码完成了,很是短小简单。接下来咱们须要将它编译而且制做成启动镜像,加载到 Bochs 里运行。

运行 mbr

首先你须要制做一个磁盘镜像文件,这里又用到了 Bochs 自带的 bximage 这个命令行工具:

>> bximage -hd -mode="flat" -size=3 -q scroll.img 1>/dev/null

它其实就是产生了一个 3MB 的写满了 0 的文件,3MB 的大小的磁盘对于咱们的项目已经足够容纳 mbr,boot,kernel 以及其它用户程序等全部数据。bximage 的打印日志还会告诉你,应该给配置文件 bochsrc.txt 里的磁盘设置什么参数,很方便。

接下来使用 nasm 编译 mbr.S:

nasm -o mbr mbr.S

而后你就获得一个 512B 大小的 mbr 文件。接下来将它刻写进磁盘镜像文件,这里用到了 dd 这个命令:

dd if=mbr of=scroll.img bs=512 count=1 seek=0 conv=notrunc

注意到这里把 mbr 写到了磁盘镜像文件的第一个扇区(512B)。


如今咱们获得一个这样的磁盘镜像文件:

而后你就能够把磁盘镜像文件加载到 Bochs 里运行了,和以前同样:

bochs -f bochsrc.txt

不过在此以前,mbr 最好先作一个小小的改动。由于此时咱们镜像里尚未任何 loader 内容,加载完的 loader 其实全是 0,这不是能够执行的代码,所以 mbr 的最后一条指令 jmp LOADER_BASE_ADDR 以后 CPU 就会挂掉,因此你能够在这条指令以前加一句 jmp $,这至关因而死循环 while (true) {},让程序悬停在这里,你就能够暂停 Bochs 而后看它是否是停在这条指令了,若是是的话,说明 mbr 的运行已经成功了。

总结

mbr 短小精悍,自己没有太多难点在里面,不过完事开头难,做为整个内核镜像的开篇,咱们须要开始提早对整个内存的布局进行谋划。若是是对汇编,指令,内存等在裸机上运行的原理还不太熟悉的同窗,mbr 也是一个很是好的练手机会,建议你多对照着反编译后 mbr 代码,以及 Bochs 调试,能快速地帮助你创建相关的认知。