从零开始写 OS 内核 - 加载并进入 kernel

系列目录

kernel 磁盘镜像

接上一篇 虚拟内存初探,本篇将正式加载并启动 kernel,也就是图中绿色的部分:git

固然 kernel 镜像要从磁盘上读取加载,因此这里回顾一张老图,是 diskmemory(物理内存)的数据对应关系:shell

顺便提一下,上图中斜线阴影打问号的部分,就是上一章讲的 kernel page tables,即第一张图的橙色部分,共 256 张占地 1MB。segmentfault

编写 kernel

回到 kernel ,即图中绿色部分,它如今实际上还不存在,因此首先咱们须要实现、编译一个简单的 demo 性质的 kernel。若是对 kernel 是什么尚未概念的同窗,可能会问:到底 kernel 长什么样?bash

答案很是简单:kernel 和你平时用 C 语言写的可执行程序几乎没有任何区别,也是从一个 main 函数开始。多线程

下面咱们就实现咱们的第一个 kernel:函数

void main() {
  while (1) {}
}

就是这样简单,除了一个 while 循环,没有任何其它东西,但它足以用做咱们这里的 demo。gitlab

编译 kernel

这里有不少编译参数,例如以 32 位编码,禁用 C 标准库等(这是咱们本身定制的 OS,和 C 标准库不可能兼容)。ui

gcc -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -no-pie -fno-pic -c main.c -o main.o

连接 kernel:

ld -m elf_i386 -Tlink.ld -o kernel main.o

这里会用到一个 link 配置文件 link.ld编码

ENTRY(main)
SECTIONS
{
  .text 0xC0800000:
  {
    code = .; _code = .; __code = .;
    *(.text)
  }

  .data ALIGN(4096):
  {
     data = .; _data = .; __data = .;
     *(.data)
     *(.rodata)
  }

  .bss ALIGN(4096):
  {
    bss = .; _bss = .; __bss = .;
    *(.bss)
    . = ALIGN(4096);
  }

  end = .; _end = .; __end = .;
}

这里最重要的就是定义了 text 段的起始地址 0xC0800000,也是整个 kernel 编址的起始。若是你还记得上一篇的内容,咱们规划了 kernel 空间的虚拟内存分布:spa

0xC0800000 将是 kernel 的入口地址,由于 text 段会被加载到此处,日后依次是 databss 等段。loader 结束后将会跳转到该地址。

另外上面还定义了整个可执行文件的入口函数为 main

编译连接后的 kernel 是一个 ELF 格式的二进制,咱们不妨将它反汇编 dump 看一下:

objdump -dsx kernel

能够看到 main 函数的地址为 0xC080000,这是进入 kernel 后的第一条指令。

制做 kernel 镜像

dd if=kernel of=scroll.img bs=512 count=2048 seek=9 conv=notrunc

seek=9 是由于前面 mbrloader 已经在磁盘上占据了前 9 个 sectors。这里 kernel 大小为 2048 个 sectors 共 1MB,对于咱们这个项目而言已经足够大了,彻底够用。

如今磁盘镜像终于变成了这样:

读取并加载 kernel

镜像准备完毕,接下来就能够将 kernel 读取而且加载了。首先仍是给出代码连接 init_kernel,供你参考。

和以前 mbrloader加载不一样,这里将读取加载两个词分开,是由于它们是两个步骤:

  • 读取:是将 kernel 磁盘镜像的 原始二进制 复制到内存中某空闲处,这里的二进制是 ELF 格式的;
  • 加载:是将前一步获得的 ELF 可执行二进制进行解析,将每个 section 复制到它们被 编址 的地方;

首先来看第一步“读取”。咱们选择的是虚拟内存顶部的 1MB,即 (0xFFFFFFFF - 1MB) ~0xFFFFFFFF 的 1MB 空间做为二进制镜像的存放地址,固然这彻底是我的选择,我选这里是由于这里目前不会有人打扰;固然也要为它分配相应的物理页 frames,在 page table 中创建映射,因此我也从剩下的物理内存空间里找了 1MB 空闲位置出来给它映射上去;而后就能够像以前读取 mbr 和 loader 同样,将 kernel 镜像读取进来。

接下来是第二步“加载”。这里涉及到了根据 ELF 文件格式的规范进行解析,须要你花点时间了解相关文档,主要就是从 program header table 中获取每一个 section 的位置和大小,以及加载的内存地址(固然是 virtual 地址),而后将数据 copy 过去。这一次加载的内存地址,才是 0xC0800000 开始的位置。固然在 copy 以前,固然要为它们预先分配好 frames 而且在 page table 中创建好内存映射。这一切工做都在 allocate_pages_for_kernel 这个函数中提早完成了。

进入 kernel

一切准备就绪,接下来就能够真正进入 kernel 了:

init_kernel:
  call allocate_pages_for_kernel
  call load_hd_kernel_image
  call do_load_kernel
  
  ; init floating point unit before entering the kernel
  finit

  ; move stack to 0xF0000000
  mov esp, KERNEL_STACK_TOP - 16
  mov ebp, esp

  ; let's jump to kernel entry :)
  jmp eax
  ret

首先初始化了 CPU 的浮点数单元,防止它后面异常。

而后我将 stack 移到了比较高的地址 0xF0000000 位置,这固然不是必须的,彻底是我我的选择。当前的 stack 位置其实也很不错(大约在 0xC0007B00 如下附近的位置,其中 0x7B00 这是在 mbr 中转移过去的,而打开 paging 后咱们用 0xC0000000 + 0x7B00 访问,若是你还记得的话)。只是我但愿后面进入 kernel 之后的 stack 位置能被移到 一个全新的地方,因此才这么多作了一步。stack 的位置是比较灵活的,只要是一个闲置的,不会受到干扰的地方就能够。


而后很是简单,jmp eax 一条指令跳到了 kernel 入口处。

为何是 eax?这是上面函数 do_load_kernel 的返回值,这个函数就是咱们解析加载 kernel 的 ELF 二进制的函数,它会返回值 kernel 的入口地址,即 main 函数地址,这个地址是由 ELF 文件中 ELF Headere_entry 字段给出的。ELF 可执行二进制的入口地址是在连接阶段肯定的,它其实是由以前的 link.ld 里的 ENTRY(main) 指定的。

顺利的话,运行的结果以下:

程序已经成功地进入 kernel 而且运行到了 0xC0800003 处,就是那个 while 循环的位置,这将是 kernel 征途的真正开篇:)