Part 1: PC Bootstrap
这一部分是为了引入 x86 汇编语言和了解 PC 的启动过程。相比较于直接运行在真机上,整个 Lab 都选择在一个叫 QEMU 的模拟器上进行。根据其指引编译,可以获得下图,代表编译成功。
PC 物理地址空间
第一台 PC 搭载的是 Intel 8088,寻址范围只有 1MB。其中有 384KB 被保留用于特定用途,例如显示输出的缓冲和其他硬件,所以低 640KB 空间是早期计算机唯一能使用的内存空间,称为 Low Memory。高 64KB 保留给了 BIOS ,是系统启动的关键。
随后的 CPU 对于内存大小的限制突破了 1MB,但是出于兼容性,0x0A0000 到 0x100000 这一范围仍被保留作为这些用途。
ROM BIOS
在 lab 中打开两个终端,分别运行 make qemu-gdb
和 make gdb
,则会在第一条语句前停止。
1
2
3
4
The target architecture is assumed to be i8086
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
在输出中可以看到第一句话是 [f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
,从中可以得到
[f000:fff0]
:最初的时候 CS = 0xf000,IP = 0xfff0。0xffff0
:第一条运行的指令在 0xffff0。ljmp $0xf000,$0xe05b
:第一条指令跳转至了 0xfe05b 的位置。
Intel CPU 中用 CS 和 IP 两个寄存器一同来表示内存地址,前者表示段地址,后者表示偏移地址。真实地址为 CS 乘以 16 后加上 IP。如上所示,地址为 0xffff0 = 16 * 0xf000 + 0xfff0。
0xffff0 这一地址位于 BIOS 中最高的位置,所以利用 jmp 指令开始执行其他指令,例如初始化和寻找可引导的设备。
Part 2: The Boot Loader
硬盘中最小操作单元称为一个扇区(sector),512 字节大小。当 BIOS 找到了可引导的扇区(boot sector),会将其载入到内存中 0x7c00 到 0x7dff,再用 jmp 跳转到 0000:7c00。其中存放着 Boot Loader,用于执行如下两个功能:
- 从实模式(real mode)切换到保护模式(protected mode),从而能够访问所有的内存。
- 从硬盘中读取出内核。
加载内核
在编译器链接前会为源文件生成目标文件,用的是一种叫 可执行可链接格式(Executable and Linkable Format, ELF)。可以用 objdump
查看文件信息。
.text 的 LMA(加载地址)代表着被载入内存的地址,VMA(链接地址)代表着期望运行时的地址。
Boot loader 会根据 ELF 中的 Program Headers 来决定如何载入以及载入到何处。
LOAD 即代表着已被载入。在 boot/Makefrag 中用 -Ttext 0x7c00
来生成正确的内存地址。
Part 3: The Kernel
使用虚拟内存
之前查看 kernel 的 ELF 信息时可以发现 VMA 为 0xf0100000,这里用到了虚拟地址的技术,将其映射到了 0x0010000 的位置。
虚拟内存可以使得各程序的地址空间分离,从而使得程序自己不会因为错误的访问内存导致其他程序的错误。此处内核用了一个很高的位置,目的是将低地址留给用户程序。
输出到控制台
相对于 C 语言中的 printf()
,在操作系统中并没有这样的函数,而是需要自行实现。
涉及到输入输出的有 kern/printf.c、lib/printfmt.c 和 kern/console.c 三个文件,其中最后一个文件处理了硬件上的输入输出,前二者都依赖于此。
在 printfmt.c 中,对于 %o
没有处理,可以模仿其他进制代码。
1
2
3
4
5
case 'o':
// Replace this with your code.
num = getuint(&ap, lflag);
base = 8;
goto number;