大二学习操作系统的时候,老师给我们介绍了MIT6.828的实验,由于课程安排的原因,我们并没有完成MIT6.828太多的实验,记忆中应该是只看了和xv6相关的内容辅助理解操作系统,以至于我现在几乎忘记了当时做了哪些实验。趁大三上课不是很多,想重新自己完整完成这7个实验。

Part 0: 6.828 实验环境的搭建

  • 虚拟机环境:Ubuntu 18.04(64位)
  • 仿真器(qemu):git clone https://github.com/mit-pdos/6.828-qemu.git qemu
  • 实验代码(lab):git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab

虚拟机环境32位,因为JOS就是32位的操作系统。
仿真器使用MIT进行patched过的(见上链接)。原因是实验中分页机制是有意修改过的,使用patched version的话在后面Exercise中不需要手动转换地址。
关于实验代码,默认熟悉GitMakeFile。每做完一个Exercise可以使用make grade进行测试。
./configure时候可能会出现库缺失导致无法完成配置,可以根据报错提示将缺失的库重新安装补全,Google一下。详细的搭建过程见Tools Guide

关于Tool Guide中给出的配置指令,如果[--prefix=PFX]参数没有指定的话,默认会安装在/usr/local/share/qemu中,这个目录需要管理员权限才能修改,所以安装时需要使用sudo make install


Part 1: PC Bootstrap

如果您还不熟悉 x86 汇编语言,那么在本课程中您将很快熟悉它!PC 汇编语言手册是一个很好的起点。希望这本书包含新旧材料的混合供你参考。

*警告:*不幸的是,书中的例子是为 NASM 汇编器编写的,而我们将使用 GNU 汇编器。NASM 使用所谓的 Intel 语法,而 GNU 使用 AT&T 语法。虽然在语义上是等效的,但程序集文件将有很大差异,至少在表面上是这样,具体取决于使用的语法。幸运的是,两者之间的转换非常简单,Brennan’s Guide to Inline Assembly 中对此进行了介绍。

  • [ ] Exercise 1. Familiarize yourself with the assembly language materials available on the 6.828 reference page. You don’t have to read them now, but you’ll almost certainly want to refer to some of this material when reading and writing x86 assembly.
    We do recommend reading the section “The Syntax” in Brennan’s Guide to Inline Assembly. It gives a good (and quite brief) description of the AT&T assembly syntax we’ll be using with the GNU assembler in JOS.

Simulating the x86

我们不是在真实的物理个人计算机 (PC) 上开发操作系统,而是使用忠实模拟完整 PC 的程序:您为仿真器编写的代码也可以在真实 PC 上启动。使用仿真器可以简化调试;例如,您可以在模拟的 x86 中设置断点,这对于 x86 的 Silicon 版本来说很难做到。
在 6.828 中,我们将使用 QEMU Emulator,这是一种现代且相对较快的仿真器。虽然 QEMU 的内置监视器仅提供有限的调试支持,但 QEMU 可以充当 GNU 调试器 (GDB) 的远程调试目标,我们将在本实验中使用它来逐步完成早期启动过程。

接下来我们就可以编译并尝试在QEMU上运行JOS了,进入之前clone的lab文件夹,执行make指令,可以看到下面的输出

tommygong@TommyGong:~/lab$ make
+ as kern/entry.S
+ cc kern/entrypgdir.c
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc kern/kdebug.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
ld: warning: section `.bss' type changed to PROGBITS
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 397 bytes (max 510)
+ mk obj/kern/kernel.img

这就表示已经成功编译出了镜像文件。
现在可以运行qemu,将上面创建的obj/kern/kernel.img作为模拟PC的“虚拟硬盘”的内容提供。这个硬盘映像包含我们的引导加载程序 ( obj/boot/boot ) 和内核 ( obj/kernel )。

tommygong@TommyGong:~/lab$ make qemu
sed "s/localhost:1234/localhost:26000/" < .gdbinit.tmpl > .gdbinit
qemu-system-i386 -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::26000 -D qemu.log
6828 decimal is XXX octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>

要退出QEMU,请键入 Ctrl+a x

The PC’s Physical Address Space

PC 的物理地址空间是硬连线的,具有以下常规布局:

+------------------+  <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\
/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
| |
| |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000

第一台基于 16 位 Intel 8088 处理器的 PC 只能寻址 1MB 的物理内存。因此,早期 PC 的物理地址空间将从 0x00000000 开始,但以 0x000FFFFF 结束,而不是 0xFFFFFFFF。标记为“Low Memory”的 640KB 区域是早期 PC 唯一可以使用的随机存取存储器 (RAM);事实上,最早的 PC 只能配置 16KB、32KB 或 64KB 的 RAM!

从 0x000A0000 到 0x000FFFFF 的 384KB 区域由硬件保留用于特殊用途,例如视频显示缓冲区和非易失性存储器中保存的固件。此保留区域最重要的部分是基本输入/输出系统 (BIOS),它占据了从 0x000F0000 到 0x000FFFFF 的 64KB 区域。在早期的 PC 中,BIOS 保存在真正的只读存储器 (ROM) 中,但当前的 PC 将 BIOS 存储在可更新的闪存中。BIOS 负责执行基本的系统初始化,例如激活视频卡和检查安装的内存量。执行此初始化后,BIOS 会从某个适当的位置(如软盘、硬盘、CD-ROM 或网络)加载操作系统,并将计算机的控制权传递给操作系统。

当英特尔最终用分别支持 16MB 和 4GB 物理地址空间的 80286 和 80386 处理器“打破 1MB 的障碍”时,PC 架构师仍然保留了 1MB 物理地址空间的原始布局,以确保与现有软件的向后兼容性。因此,现代 PC 在物理内存上有一个从 0x000A0000 到 0x00100000 的“漏洞”,将 RAM 分为“低内存”或“传统内存”(前 640KB)和“扩展内存”(其他所有内存)。此外,PC 的 32 位物理地址空间最顶部的一些空间(尤其是物理 RAM)现在通常由 BIOS 保留供 32 位 PCI 设备使用。

最新的 x86 处理器可以支持超过 4GB 的物理 RAM,因此 RAM 可以进一步扩展到 0xFFFFFFFF 以上。在这种情况下,BIOS 必须在系统 RAM 的 32 位可寻址区域顶部留出第二个孔,以便为这些 32 位设备留出空间进行映射。由于设计限制,JOS 无论如何都会只使用 PC 物理内存的前 256MB,所以现在我们假设所有 PC 都“只有”一个 32 位的物理地址空间。但是,处理复杂的物理地址空间和多年来发展起来的硬件组织的其他方面是操作系统开发的重要实际挑战之一。

The ROM BIOS

打开两个终端窗口和 cd 两个 shell 进入您的实验室目录。在一个版本中,输入 make qemu-gdb 。这将启动 QEMU,但 QEMU 在处理器执行第一条指令并等待来自 GDB 的调试连接之前停止。在第二个终端中,从您运行的同一目录中运行 make gdb。然后应该就能看到下面的内容

tommygong@TommyGong:~/lab$  make gdb
gdb -n -x .gdbinit
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
+ target remote localhost:26000
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration
of GDB. Attempting to continue with the default i8086 settings.

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是GDB对QEMU执行的第一条指令的反汇编。

  • IBM PC 从物理地址 0x000ffff0 开始执行,该地址位于为 ROM BIOS 保留的 64KB 区域的最顶部。

  • PC 以 CS(Code Segment) = 0xf000IP(Instruction Pointer) = 0xfff0 开始执行。

  • 要执行的第一条指令是一条 jmp 指令,该指令跳转到分段地址 CS = 0xf000IP = 0xe05b

这条指令 0xffff0: ljmp $0xf000,$0xe05b 是一个远跳转(ljmp)指令,它用于将程序的执行流跳转到特定的段和偏移地址。
0xffff0: 这是指令在内存中的地址,意味着当前指令位于内存的 0xFFFF0 地址处。ljmp $0xf000, $0xe05b: 这是一条远跳转指令(long jump,简称 ljmp),它包含两个部分:

  • 段选择符 $0xf000: 段寄存器的值,即代码段的基地址。
  • 偏移地址 $0xe05b: 相对于段基地址的偏移量。

在 x86 保护模式之前的实模式下,内存地址是通过段和偏移组合的形式访问的:

  • 物理地址 = 段选择符 × 16 + 偏移地址

因此,执行这条指令后,CPU 会跳转到段 $0xf000 和偏移 $0xe05b 组合形成的物理地址:

  • 物理地址 = 0xf000 * 16 + 0xe05b
  • 物理地址 = 0xf0000 + 0xe05b = 0xfe05b

这种指令通常出现在系统启动时(例如,BIOS 启动阶段)。当 CPU 加电或者复位时,它会从 0xFFFF0 这个地址开始执行,通常这是一条跳转指令,将 CPU 引导到系统 BIOS 的实际启动代码处。

实模式 (Real Mode)

实模式是x86处理器上电或重置后默认的工作模式,最早用于8086处理器,并且向后兼容现代处理器。

  • 内存寻址:处理器只能访问 1MB 的内存空间。这是由于实模式只能使用20位地址(段寄存器16位+偏移量16位,实际结果为20位地址线)。
  • 段寄存器:内存寻址采用分段机制,内存地址是通过段寄存器和偏移量相加来计算的。例如,物理地址 = 段基址 × 16 + 偏移量
  • 没有内存保护:在实模式下,程序可以直接访问任何内存地址,导致多个程序之间可能会互相覆盖内存,容易出现系统崩溃。
  • 多任务处理:没有内建的硬件支持多任务处理,处理器无法有效地管理多个程序的并行执行。
  • 应用:实模式主要用于早期的操作系统(如DOS),以及一些简单的嵌入式系统。

保护模式 (Protected Mode)

保护模式是现代x86处理器的主要工作模式,最早引入于80286处理器,后来在80386及以后的处理器中得到了大幅改进。

  • 内存寻址:使用32位地址总线,最多可以寻址 4GB 的内存。并且支持更复杂的内存管理机制,如分页(Paging)和虚拟内存(Virtual Memory)。
  • 段管理:保护模式中的段寄存器不再简单地提供段基址,而是与**全局描述符表(GDT)局部描述符表(LDT)**关联,提供段保护。每个段都有自己的权限、大小等信息。
  • 内存保护:每个程序运行在自己的地址空间内,处理器能够检测非法的内存访问。通过段和分页机制,操作系统可以防止不同程序互相干扰,增强系统的稳定性和安全性。
  • 多任务处理:硬件支持多任务处理,处理器能够通过任务状态段(TSS)快速切换任务。内存保护机制使得每个任务在自己的地址空间中运行,确保系统的稳定性。
  • 虚拟内存:保护模式支持虚拟内存,通过分页机制将虚拟地址映射到物理地址,允许程序使用比实际物理内存更大的地址空间。
  • 应用:所有现代操作系统(如Windows、Linux、macOS)都运行在保护模式下。

实模式和保护模式的对比

特性 实模式 保护模式
内存寻址 最大 1MB 最大 4GB(支持分页)
段寄存器 简单的段+偏移 与 GDT/LDT 关联,支持权限
内存保护 无内存保护 内存保护,防止进程冲突
多任务处理 不支持 支持,硬件层面支持
虚拟内存 不支持 支持(通过分页实现)
应用场景 早期操作系统、嵌入式系统 现代操作系统和应用
  • [ ] Exercise 2. Use GDB’s si (Step Instruction) command to trace into the ROM BIOS for a few more instructions, and try to guess what it might be doing. You might want to look at Phil Storrs I/O Ports Description, as well as other materials on the 6.828 reference materials page. No need to figure out all the details - just the general idea of what the BIOS is doing first.

当 BIOS 运行时,它会设置中断描述符表并初始化各种设备,例如 VGA 显示器。这就是您在 QEMU 窗口中看到的 “ Starting SeaBIOS ” 消息的来源。
在初始化 PCI 总线和 BIOS 知道的所有重要设备后,它会搜索可启动设备,例如软盘、硬盘驱动器或 CD-ROM。最终,当它找到可启动磁盘时,BIOS 会从磁盘中读取 boot loader 并将控制权转移给它。

Part 2: The Boot Loader

用于 PC 的软盘和硬盘被划分为 512 字节的区域,称为扇区。扇区是磁盘的最小传输粒度:每个读取或写入操作的大小必须是一个或多个扇区,并在扇区边界上对齐。如果磁盘是可引导的,则第一个扇区称为引导扇区,因为这是引导加载程序代码所在的位置。当 BIOS 找到可启动的软盘或硬盘时,它会将 512 字节的引导扇区加载到物理地址的内存中,0x7c00 到 0x7dff,然后使用 jmp 指令将 CS:IP 设置为 0000:7c00 ,将控制权传递给引导加载程序。与 BIOS 加载地址一样,这些地址相当任意 - 但它们对于 PC 来说是固定和标准化的。

在 PC 的发展过程中,从 CD-ROM 启动的能力出现得要晚得多,因此 PC 架构师借此机会稍微重新考虑了启动过程。因此,现代 BIOS 从 CD-ROM 启动的方式稍微复杂一些(也更强大)。CD-ROM 使用的扇区大小为 2048 字节而不是 512 字节,并且 BIOS 可以在将控制权转移到磁盘之前将更大的引导映像从磁盘加载到内存中(而不仅仅是一个扇区)。有关更多信息,请参见“El Torito”可启动 CD-ROM 格式规范

然而,对于 6.828,我们将使用传统的硬盘驱动器启动机制,这意味着我们的启动加载程序必须适合区区 512 字节。引导加载程序由一个汇编语言源文件 boot/boot.S 和一个 C 源文件组成, boot/main.c 请仔细查看这些源文件,并确保您了解发生了什么。引导加载程序必须执行两个主要功能:

  1. 首先,boot loader 将处理器从实模式切换到 32 位保护模式,因为只有在这种模式下,软件才能访问处理器物理地址空间中 1MB 以上的所有内存。保护模式在 PC 汇编语言的 1.2.7 和 1.2.8 节中简要描述,在 Intel 架构手册中也有非常详细的描述。此时,您只需要了解分段地址 (segment:offset pairs) 到物理地址的转换在保护模式下的发生方式不同,并且在转换后偏移量是 32 位而不是 16 位。
  2. 其次,引导加载程序通过 x86 的特殊 I/O 指令直接访问 IDE 磁盘设备寄存器,从硬盘读取内核。如果您想更好地理解此处的特定 I/O 指令的含义,请查看 6.828 参考页面上的“IDE 硬盘驱动器控制器”部分。在本课程中,您不需要学习太多有关特定设备编程的知识:编写设备驱动程序实际上是操作系统开发中非常重要的部分,但从概念或体系结构的角度来看,它也是最不有趣的部分之一。

了解引导加载程序源代码后,请查看文件 obj/boot/boot.asm .这个文件是我们的 GNUmakefile 在编译 boot loader 创建的 boot loader 的反汇编。这个反汇编文件可以很容易地看到所有 boot loader 代码在物理内存中的确切位置,并且更容易跟踪在 GDB 中单步执行 boot loader 时发生的情况。同样, obj/kern/kernel.asm 包含 JOS 内核的反汇编,这通常对调试很有用。

您可以使用该 b 命令在 GDB 中设置地址断点。例如, b *0x7c00 在地址 0x7C00 处设置断点。到达断点后,您可以使用 c and si 命令继续执行: c 使 QEMU 继续执行,直到下一个断点(或直到您按下 Ctrl-C ),并 si N 一次单步执行 N 指令。

要检查内存中的指令(除了 GDB 自动打印的下一个要执行的指令),请使用命令 x/i 。此命令的语法 x/Ni ADDR 为 ,其中 N 是要反汇编的连续指令数,ADDR 是开始反汇编的内存地址。

  • [ ] Exercise 3. Take a look at the lab tools guide, especially the section on GDB commands. Even if you’re familiar with GDB, this includes some esoteric GDB commands that are useful for OS work.
    Set a breakpoint at address 0x7c00, which is where the boot sector will be loaded. Continue execution until that breakpoint. Trace through the code in boot/boot.S, using the source code and the disassembly file obj/boot/boot.asm to keep track of where you are. Also use the x/i command in GDB to disassemble sequences of instructions in the boot loader, and compare the original boot loader source code with both the disassembly in obj/boot/boot.asm and GDB.
    Trace into bootmain() in boot/main.c, and then into readsect(). Identify the exact assembly instructions that correspond to each of the statements in readsect(). Trace through the rest of readsect() and back out into bootmain(), and identify the begin and end of the for loop that reads the remaining sectors of the kernel from the disk. Find out what code will run when the loop is finished, set a breakpoint there, and continue to that breakpoint. Then step through the remainder of the boot loader.

Quesiton:

  1. At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
  2. What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?
  3. Where is the first instruction of the kernel?
  4. How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?

Loading the Kernel

  • [ ] Exercise 4. Read about programming with pointers in C. The best reference for the C language is The C Programming Language by Brian Kernighan and Dennis Ritchie (known as ‘K&R’). We recommend that students purchase this book (here is an Amazon Link) or find one of MIT’s 7 copies.
    Read 5.1 (Pointers and Addresses) through 5.5 (Character Pointers and Functions) in K&R. Then download the code for pointers.c, run it, and make sure you understand where all of the printed values come from. In particular, make sure you understand where the pointer addresses in printed lines 1 and 6 come from, how all the values in printed lines 2 through 4 get there, and why the values printed in line 5 are seemingly corrupted.
    There are other references on pointers in C (e.g., A tutorial by Ted Jensen that cites K&R heavily), though not as strongly recommended.
    Warning: Unless you are already thoroughly versed in C, do not skip or even skim this reading exercise. If you do not really understand pointers in C, you will suffer untold pain and misery in subsequent labs, and then eventually come to understand them the hard way. Trust us; you don’t want to find out what “the hard way” is.