第二节 计算机启动过程和对内核的要求

计算机启动时,顺序引导 BIOS、BootLoader 和内核。我们的实验基于 RiscV64 架构 qemu 模拟器,它对应模拟了这一过程。

启动过程

模拟器 qemu-riscv64 启动时,将会经历几个阶段:

  1. 程序寄存器 PC 首先被初始化为 0x1000 的地址;

  2. 地址 0x1000 处被 qemu 预先放置了一个 ROM,顺序执行其中包含的寥寥几行代码,PC 跳转到 0x8000_0000 地址;

  3. 地址 0x8000_0000 同样被 qemu 预先埋伏了 OpenSBI(后文简称 SBI),并且入口就在这个开始地址。SBI 由此处启动,进行一系列的硬件初始化工作,并提供一组基本的功能调用,为后面操作系统内核的启动准备好条件和环境;最后一步,SBI 从 M-Mode 切换到 S-Mode,并跳转到地址 0x8020_0000 继续执行;

  4. 地址 0x8020_0000 就是为内核准备的位置,只要我们把内核加载到此处,并且保证内核入口在开头,就能够获得计算机的控制权。

RiscV 体系结构及平台在很多方面都体现了设计上的简洁。RISC-V SBI 规范定义了平台固件应当具备的功能和服务接口,多数情况下 SBI 本身就可以代替传统上固件 BIOS/UEFI + BootLoader 的位置和作用,qemu-riscv64 模拟器同样参照模拟了这一情况,并且把 OpenSBI 集成到 qemu 工程内部。而对于 X86 等体系结构,qemu 仍是延用传统方式,即从内嵌 seabios 开始引导,经过 grub 等 BootLoader 的进一步准备,最后再启动操作系统内核。

总结一下,我们的目标是,让自己开发的内核等候在正确的位置上,并以正确的形式存在。具体来说满足以下要求:

  1. 内核被加载到 0x8020_0000 地址

    • 这是 qemu 的职责,我们只需要指定正确的参数。
  2. 内核编译后的形式必须是 binary

    • Rust 编译器输出的默认执行程序格式是 ELF,这种格式需要被 ELF 加载器解析和加载。

    • 显然,内核的上一级加载器 SBI 并不支持 ELF 功能,所以只能让编译出来的内核以原始 binary 形式提供。

      至少目前 OpenSBI 还没有支持 ELF 的计划。但是确实存在一些其它的 BootLoader 支持这样的功能。

  3. 内核入口必须在 Image 文件的开头

    • Rust 编译器默认情况下,会自己安排可执行程序文件的分段与符号布局。由于我们必须确保内核入口在最前面,所以需要通过自定义 LDS 文件的方式,控制内核 Image 文件的布局。
    • 后面的实验将会用到下面的 LDS 文件 linker.lds:
    OUTPUT_ARCH(riscv)
    
    BASE_ADDRESS = 0x80200000;
    
    ENTRY(_start)
    SECTIONS
    {
        . = BASE_ADDRESS;
        _skernel = .;
    
        .text : ALIGN(4K) {
            _stext = .;
            *(.text.boot)
            *(.text .text.*)
            . = ALIGN(4K);
            _etext = .;
        }
        
        ...
    }
    
    • 有两个地方需要注意:

      • 首先是把代码区 .text 作为第一个 section,并且其中 *(.text.boot) 在 *(.text .text.*) 之前,后者是代码默认情况下所属的 section 属性。将来我们把内核入口的代码标记在 .text.boot 区域中,就可以确保它会最早被执行。

      • 其次起始地址 BASE_ADDRESS 是 0x8020_0000,正是内核的运行地址,这样就可以把内核的链接地址和运行地址一致起来。如果它们不一致,基于绝对寻址方式的指令将无法正常运行,进而导致内核崩溃。将来当我们启用分页机制之后,会把这个地址固定改成对应的虚拟地址 0xffff_ffc0_8020_0000。直观看来,这个虚拟地址相对物理地址存在一个偏移量 0xffff_ffc0_0000_0000,这个偏移的名字是 PHYS_VIRT_OFFSET,将来它会在虚实地址转换中发挥重要作用,后面第二章第一节会介绍这个偏移量是如何得出的。

        注:如果此时就把 BASE_ADDRESS 设置为 0xffff_ffc0_8020_0000 或者其它的什么值,似乎程序最初也可以正常运行一部分代码。主要原因是,内核启动早期的那些汇编指令,通常会被有意保持为相对寻址,即它们是位置无关指令,所以 BASE_ADDRESS 对它们不起作用。但是相对寻址的地址范围受到限制,我们不能要求内核完全采用这种寻址方式,通常只是要求在启用分页之前的指令必须是相对寻址方式。

      LDS 中还有一些其它的关键信息,在后边章节再详细介绍。

      完整的 linker.lds 文件见 附录A。可以在 arceos 根目录下预先建立这个文件,第四节中会用到它。