XV6

引导加载器的 C 语言部分 bootmain.c 目的是在磁盘的第二个扇区开头找到内核程序。内核是 ELF 格式的二进制文件。为了读取 ELF 头,bootmain 通过 readseg() 载入 ELF 文件的前 4096 字节,并将其拷贝到内存中 0x10000 处。

这些内容是 kernel 的前 4 KB 内容。由于 ELF 文件将描述信息放在文件最开始的地方,因此 elf 指向的就是 ELF 文件头。bootmain 根据其是否包含了 ELF_MAGIC 来判定其是否为 ELF 格式的文件,如果不是则返回到 bootasm.S 并进入无限循环。ELF 文件头结构体用 elfhdr 结构体来表示。

bootmain 从磁盘中 ELF 头之后 off 字节处读取扇区的内容,并写到内存中地址 paddr 处。bootmain 调用 readseg() 将数据从磁盘中载入,并调用 stosb() 将段的剩余部分置零。stosb() 使用 x86 指令 rep stosb 来初始化内存块中的每个字节。

如果是 ELF 格式文件,则会根据程序头表的偏移计算出 ELF 程序头表位置 ph 和结束位置 eph。每一个程序头表项记录一个 ELF Segment 在磁盘文件中的位置偏移、应当装载到内存的什么位置等信息。程序头表结构体请见 proghdr

在内核编译和链接后,我们应该能在虚拟地址 0x80100000 处找到它。因此,函数调用指令使用的地址都是 0xf01xxxxx 的形式;你可以在 kernel.asm 中找到类似的例子。这个地址是在 kernel.ld 中设置的。0x80100000 是一个比较高的地址,差不多处在 32 位地址空间的尾部。当然,实际的物理内存中可能并没有这么高的地址。一旦内核开始运行,它会开启分页硬件来将虚拟地址 0x80100000 映射到物理地址 0x00100000。引导程序运行到现在,分页机制尚未被开启。在kernel.ld中指明了内核的 paddr 是 0x00100000,也就是说,引导加载器将内核拷贝到的低地址正是分页硬件最终会映射的物理地址。

内核 ELF 文件只有一个段需要装入,用 readelf -l kernel 查看的信息如下

Elf file type is EXEC (Executable file)
Entry point 0x10000c
There are 2 program headers, starting at offset 52
  
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x80100000 0x00100000 0x0a516 0x154a8 RWE 0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

引导加载器的最后一项工作是调用内核的入口指令,即内核第一条指令的执行地址。在 XV6 中入口指令的地址是 0x10000c:

# objdump -f kernel                

kernel:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c

按照惯例,在 entry.S 中定义的 _start 符号即 ELF 入口。由于 XV6 还没有建立虚拟内存,XV6 的入口即 entry 的物理地址。

装入 kernel 后,entry 获得 kernel 的 ELF 文件头 elf->entry 入口地址,并执行 entry() 调用,从而将控制权从 bootblock 启动扇区转移到 kernel 代码。紧接着进入到 entry() 代码,此时启动代码完成了引导作用,kernel 开始接管系统。

2. 现实情况

引导加载器编译后大概有 470 字节的机器码,具体大小取决于编译优化。为了放入比较小的空间中,XV6 引导加载器做了一个简单的假设:内核放在引导磁盘中从扇区 1 开始的连续空间中。通常内核就放在普通的文件系统中,而且可能不是连续的。也有可能内核是通过网络加载的。这种复杂性就使得引导加载器必须要能够驱动各种磁盘和网络控制器,并能够解读不同的文件系统和网络原型。也就是说,引导加载器本身就已经成为了一个小操作系统。显然这样的引导加载器不可能只有 512 字节,大多数的 PC 操作系统的引导过程分为 2 步。首先,一个类似于本节介绍的简单的引导加载器会从一个已知的磁盘位置上把完整的引导加载器加载进来,通常这步会依靠空间权限更大的 BIOS 来操作磁盘。接下来,这个超过 512 字节的完整加载器就有足够的能力定位、加载并执行内核了。也许在更现代的设计中,会直接用 BIOS 从磁盘中读取完整的引导加载器(并在保护模式和 32 位模式下启动之)。

本文假设在开机后,引导加载器运行前,唯一发生的事即 BIOS 加载引导扇区。但实际上 BIOS 会做相当多的初始化工作来确保现代计算机中结构复杂的硬件能像传统标准中的 PC 一样工作。

3. 文件读入操作

先看一下从硬盘中读取数据的函数,readseg() 从硬盘中读取数据到 pa 开始的内存地址,count 是字节数目,offset 是内核影像在磁盘中的起始位置(字节偏移)。因为在硬盘中的最小单位是扇区 SECTSIZE,所以我们先把 offset 字节偏移转换成 sector 计数的偏移。注意内核是从 sector 1 开始的(sector 0 用于启动扇区),sector 计数的偏移转换成扇区数之后的结果要加 1。下一步就是依次读取从指定 sector 开始的 count 定义的字节数目到内存中了。