系列目录
- 序篇
- 筹备工作
- BIOS 开始启动到实模式
- 进入保护模式
- 加载并进入内核
- 显示与打印
- GDT 和 IDT,中断解决
- 关上虚拟内存
- 实现堆和 malloc
- 创立第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 过程的实现
- 进入用户态
- 一个简略的文件系统
- 加载可执行程序
- 零碎调用的实现
- 键盘驱动
- 运行 shell
实现 Boot Loader
从这篇开始咱们将进入 boot loader 的编写。网上有一些相似的教程可能跳过了这个阶段,间接为你筹备好了 boot loader,从而你能够间接开始 kernel 的编写,例如之前举荐的 JamesM’s kernel development tutorials 就是这样的。不过我还是强烈建议将 boot loader 也本人实现了,尤其是对初学者,起因如下:
- 它并不艰难,相比于前面的 kernel;
- 有助于你疾速进步汇编能力,这在前面的 C 语言 kernel 编写、调试中依然很重要;
- boot 裸机运行阶段的的编程有助于你建设起对磁盘、内存、指令和 data 之间的加载、映射关系的正确认识,为前面的内核、可执行程序的加载,以及虚拟内存的建设做好筹备,尤其是如果你感觉对这一块比拟含糊的话;
- boot 阶段其实会初步搭建起 segment 以及虚拟内存的框架,为后续 kernel 编写打下基础;
开机进入 BIOS
这是一个经典的问题,就是计算机主板开机上电后到启动,产生了什么?
首先咱们须要晓得开机后 CPU 和内存所处的状态,开机后 CPU 初始模式是实模式,地址宽度为 20 位,即最大地址空间 1MB。这 1MB 空间的划分是固定的,每一块都有规定的用处的,被映射到不同的设施上:
BIOS 的工作
咱们来看一下开机后产生的事件:
- 开机后 CPU 的指令寄存器 ip 被强置为地址
0xFFFF0
,这一地址被映射到 BIOS 固件上的代码,这就是计算机开机后的第一条指令的地址; - CPU 开始执行 BIOS 上的代码,这一部分次要是硬件输入输出设施相干的查看,以及建设一个最后的中断向量表,目前不用深究;
- BIOS 代码最初阶段的工作,就是查看启动盘上的
mbr
分区,所谓 mbr 分区就是磁盘上的第一个 512B 内容;BIOS 会对这 512B 做一个查看:它的最初 2 个字节必须是两个 magic number:0x55
和0xaa
,否则它就不是一个非法的启动盘; - 查看通过后,BIOS 将这 512B 加载到内存
0x7C00
处,到 0x7E00 为止,而后指令跳转到 0x7C00 开始执行;
将下面那张表格画成图,去掉烦扰项,只留下咱们关怀的局部:
- 黄色局部是加载到内存的 mbr,起始地址 0x7C00;
- 红色局部是咱们前面能够自在应用的内存空间;
- 斜线暗影局部为 BIOS 代码;
图中标出了 BIOS 的次要工作流程,从地址 0xFFFF0 开始,通过一系列代码执行,最终校验并读取磁盘第一个 512B 扇区,加载到黄色局部即为 mbr,地址为 0x7C00,而后指令跳转过来,进入 mbr 的执行;
mbr 的工作
那么 mbr 须要做什么事件?因为 mbr 大小被限度在了 512B,你不可能在外面放很多代码和数据,所以它最重要的工作只有一个:
- 将前面的 loader 局部从磁盘加载到内存,并跳转到 loader 继续执行;
内存布局布局
所以咱们须要布局一下整个 boot load 阶段的内存布局,这里咱们间接给出磁盘以及内存的全貌:
咱们目前重点关注内存 1MB 以下局部的内容,不同局部用了不同的色彩标识进去:
- 黄色:mbr
- 蓝色:loader
- 绿色:kernel
- 红色:可自在应用
通过 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(int load_mem_addr, int load_start_sector, int sectors_num);
这里就是 mbr 最次要的工作,把 loader 从磁盘上加载进来到内存 0x8000 的地位,read_disk
三个参数传参别离为:
// loader 加载地址为 0x80000
int load_mem_addr = LOADER_BASE_ADDR;
// loader 镜像在磁盘上起始地位为第 2 个 sector,紧接着 mbr
int load_start_sector = 1;
// loader 大小为 8 个 sectors,共 4KB;int sectors_num = 8;
read_disk
函数波及到了读取磁盘,须要用到一堆 CPU 管制磁盘的端口和中断性能,你须要查阅文档应用,简短繁冗,我是照搬了《操作系统假相还原》一书第三章的内容。你其实也不用深究,拿来用就能够,只须要晓得它做了什么工作即可。
加载完 loader 之后,就能够跳转到 loader 地址 0x8000 执行:
jmp LOADER_BASE_ADDR
最初还有个要害的小东西:
这一通代码下来,所用的空间还远未到 512B,咱们将残余的空间全副用 0 填充(其实轻易填什么都行,反正执行不到),最初在 512B 的开端处写上 0x55
和 0xaa
两个 magic number:
times 510-($-$$) db 0
db 0x55, 0xaa
至此 mbr 便编码实现了,十分短小简略。接下来咱们须要将它编译并且制作成启动镜像,加载到 Bochs 里运行。
运行 mbr
首先你须要制作一个磁盘镜像文件,这里又用到了 bximage
这个工具:
bximage -hd -mode="flat" -size=3 -q scroll.img 1>/dev/null
它其实就是产生了一个 3MB 的写满了 0 的文件。
接下来应用 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 写到了磁盘镜像文件的第一个扇区。
而后你就能够把磁盘镜像文件加载到 Bochs 里运行了,和之前一样:
bochs -f bochsrc.txt
不过在此之前,mbr 最好先做一个小小的改变。因为此时咱们镜像里还没有任何 loader 内容,加载完的 loader 其实全是 0,这不是能够执行的代码,因而 mbr 的最初一条指令 jmp LOADER_BASE_ADDR
之后 CPU 就会挂掉,所以你能够在这条指令之前加一句 jmp $
,这相当于是死循环 while (true) {}
,让程序悬停在这里,你就能够暂停 Bochs 而后看它是不是停在这条指令了,如果是的话,阐明 mbr 的运行曾经胜利了。