乐趣区

关于操作系统:从零开始写-OS-内核-BIOS-开始启动到实模式

系列目录

  • 序篇
  • 筹备工作
  • 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 的工作

咱们来看一下开机后产生的事件:

  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 开始执行;

将下面那张表格画成图,去掉烦扰项,只留下咱们关怀的局部:

  • 黄色局部是加载到内存的 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 的开端处写上 0x550xaa 两个 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 的运行曾经胜利了。

退出移动版