运行 shellhtml
接上一篇准备工做,从这篇开始咱们将进入 boot loader 的编写。网上有一些相似的教程可能跳过了这个阶段,直接为你准备好了 boot loader,从而你能够直接开始 kernel 的编写,例如以前推荐的 JamesM's kernel development tutorials 就是这样的。不过我仍是强烈建议将 boot loader 也本身实现了,尤为是对初学者,缘由以下:java
这是一个经典的问题,就是计算机主板开机上电后到启动,发生了什么?git
首先咱们须要知道开机后 CPU 和内存所处的状态,开机后 CPU 初始模式是实模式,地址宽度为 20 位,即最大地址空间 1MB。这 1MB 空间的划分是固定的,每一块都有规定的用途的,被映射到不一样的设备上:shell
咱们来看一下开机后发生的事情:编程
0xFFFF0
,这一地址被映射到 BIOS 固件上的代码,这就是计算机开机后的第一条指令的地址;mbr
分区,所谓 mbr 分区就是磁盘上的第一个 512B 内容,又叫引导分区
;BIOS 会对这 512B 作一个检查:它的最后2个字节必须是两个 magic number:0x55
和 0xaa
,不然它就不是一个合法的启动盘;0x7C00
处,到 0x7E00 为止,而后指令跳转到 0x7C00 开始执行;至此 BIOS 退出舞台;将上面那张表格画成图,去掉干扰项,只留下咱们关心的部分:segmentfault
图中标出了 BIOS 的主要工做流程,从地址 0xFFFF0 开始,通过一系列代码执行,最终校验并读取磁盘第一个 512B 扇区,加载到黄色部分即为 mbr,地址为 0x7C00,而后指令跳转过去,进入 mbr 的执行;bash
那么 mbr 须要作什么事情?由于 mbr 大小被限制在了 512B,你不可能在里面放不少代码和数据,因此它最重要的工做只有一个:多线程
因此咱们须要规划一下整个 boot load 阶段的内存布局,这里咱们直接给出磁盘以及内存的全貌:框架
咱们目前重点关注内存 1MB 如下部分的内容,不一样部分用了不一样的颜色标识出来:函数
通过 BIOS 的工做,如今指令已经来到了 mbr 部分,它须要将蓝色部分的 loader 从磁盘上加载到内存,地址就定为 0x8000,注意这个地址能够自由指定的,只要在图中白色区域内,而且空间足够用便可。咱们的 loader 部分也不会很大,按照比较富余地估计,4KB 足以。
先给出我项目里的代码,路径为 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 的末尾处写上 0x55
和 0xaa
两个 magic number:
times 510-($-$$) db 0 db 0x55, 0xaa
至此 mbr 便编码完成了,很是短小简单。接下来咱们须要将它编译而且制做成启动镜像,加载到 Bochs 里运行。
首先你须要制做一个磁盘镜像文件,这里又用到了 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 调试,能快速地帮助你创建相关的认知。