MIT 6.828 Lab1 第一部分
Part 1:PC Bootstrap
第一个练习的目的是向你介绍 x86 汇编语言和 PC 启动过程,并让你开始使用 QEMU 和 QEMU/GDB 调试。在这部分实验中,你不必编写任何代码,但为了加深理解,你还是应该做一遍,并准备好回答下面的问题。
x86汇编入门
如果您还不熟悉 x86 汇编语言,那么在本课程中您将很快熟悉它!个人电脑汇编语言》一书是一个很好的起点。希望这本书能为你提供新旧资料的结合。
警告:遗憾的是,书中的示例是为 NASM 汇编语言编写的,而我们将使用 GNU 汇编语言。NASM 使用所谓的 Intel 语法,而 GNU 使用 AT&T 语法。虽然在语义上是等同的,但汇编文件至少在表面上会有很大的不同,这取决于使用的是哪种语法。幸运的是,这两种语法之间的转换非常简单,Brennan 的《内联汇编指南》中对此有所介绍。
练习 1.熟悉 6.828 参考页面上的汇编语言资料。你不必现在就阅读它们,但在阅读和编写 x86 汇编程序时,你几乎肯定要参考其中的一些材料。
我们建议您阅读 Brennan's Guide to Inline Assembly 中的 "语法 "部分。它对我们将在 JOS 中与 GNU 汇编程序一起使用的 AT&T 汇编语法作了很好(但相当简短)的描述。
当然,x86 汇编语言编程的权威参考资料是英特尔的指令集体系结构参考资料,您可以在 6.828 参考资料页面上找到它的两种版本:旧版 80386 程序员参考手册的 HTML 版,它比最新的手册更简短、更易于浏览,但描述了我们将在 6.828 中使用的所有 x86 处理器功能;以及英特尔完整、最新、最棒的 IA-32 英特尔体系结构软件开发人员手册,它涵盖了我们在课堂上用不上、但您可能感兴趣的最新处理器的所有功能。828;以及英特尔公司提供的完整的、最新的和最好的 IA-32 英特尔体系结构软件开发人员手册,涵盖了最新处理器的所有功能,这些功能我们在课堂上用不上,但你可能有兴趣了解。AMD 也提供了相应的手册(通常更友好)。请将英特尔/AMD 架构手册留作以后使用,或在需要查找特定处理器功能或指令的权威解释时用作参考。
模拟x86
我们不在真实的个人电脑(PC)上开发操作系统,而是使用一个能忠实模拟完整个人电脑的程序:你为模拟器编写的代码也能在真实的个人电脑上启动。使用仿真器可以简化调试;例如,你可以在仿真的 x86 内设置断点,而这在硅版本的 x86 上是很难做到的。
在 6.828 中,我们将使用 QEMU 仿真器,这是一个现代且相对快速的仿真器。虽然 QEMU 的内置监视器只能提供有限的调试支持,但 QEMU 可以充当 GNU 调试器 (GDB) 的远程调试目标,我们将在本实验中使用 GDB 来逐步完成早期启动过程。
要开始实验,请按照上文 "软件设置 "中的说明,将实验 1 文件解压缩到雅典娜上自己的目录中,然后在实验目录中键入 make(或 BSD 系统中的 gmake)来构建最小的 6.828 引导加载器和内核。(把我们在这里运行的代码称为 "内核 "有点夸张,但我们会在整个学期中充实它)。
若我们是自己是在虚拟机中clone了实验的代码,只需要进入源代码的目录,输入make就可以了,输出如下:
(如果出现类似 "undefined reference to `__udivdi3'"的错误,可能是因为没有 32 位 gcc multilib。如果运行的是 Debian 或 Ubuntu,请尝试安装 gcc-multilib 软件包)。
现在,你可以运行 QEMU,将上面创建的 obj/kern/kernel.img 文件作为仿真电脑 "虚拟硬盘 "的内容。该硬盘映像包含引导加载器(obj/boot/boot)和内核(obj/kernel)。
在实验的目录下输入make qemu:
从硬盘启动...... "之后的所有内容都是由我们的骨架 JOS 内核打印的;K> 是由我们内核中的小型监视器或交互式控制程序打印的提示。如果使用 make qemu,内核打印的这些行将同时出现在运行 QEMU 的常规 shell 窗口和 QEMU 显示窗口中。这是因为出于测试和实验室评分的目的,我们设置 JOS 内核不仅将控制台输出写入虚拟 VGA 显示屏(如 QEMU 窗口所示),而且写入模拟 PC 的虚拟串行端口,QEMU 再将其输出到自己的标准输出。同样,JOS 内核会接受键盘和串行端口的输入,因此你可以在 VGA 显示窗口或运行 QEMU 的终端上向它下达命令。另外,你也可以通过运行 make qemu-nox 来使用串行控制台,而不使用虚拟 VGA。如果你是通过 SSH 接入雅典娜拨号系统,这可能会很方便。要退出 qemu,请键入 Ctrl+a x。
内核监视器只有两个命令:help(帮助)和 kerninfo(内核信息)。
help 命令显而易见,我们稍后将讨论 kerninfo 命令所打印内容的含义。内核监视器虽然简单,但需要注意的是,它是在模拟 PC 的 "原始(虚拟)硬件 "上 "直接 "运行的。这意味着你可以将 obj/kern/kernel.img 的内容复制到真实硬盘的前几个扇区,然后将硬盘插入真实电脑,打开电脑,在电脑的真实屏幕上看到与 QEMU 窗口中完全相同的内容。(不过,我们不建议你在硬盘上有有用信息的真实计算机上这样做,因为将 kernel.img 复制到硬盘的开头会破坏主引导记录和第一个分区的开头,从而导致硬盘上的所有信息丢失!)。
PC内存地址空间
现在,我们将深入探讨有关 PC 启动方式的更多细节。个人电脑的物理地址空间布局如下:
第一代 PC 基于 16 位英特尔 8088 处理器,只能寻址 1MB 的物理内存。因此,早期 PC 的物理地址空间以 0x00000000 开始,但以 0x000FFFFF 结束,而不是 0xFFFFFFFF。标有 "低内存 "的 640KB 区域是早期 PC 可以使用的唯一随机存取内存(RAM);事实上,最早的 PC 只能配置 16KB、32KB 或 64KB 的 RAM!
从 0x000A0000 到 0x000FFFFF 的 384KB 区域由硬件保留,用于视频显示缓冲区和非易失性内存中的固件等特殊用途。保留区域中最重要的部分是基本输入/输出系统(BIOS),它占据了从 0x000F0000 到 0x000FFFFF 的 64KB 区域。在早期的个人电脑中,BIOS 保存在真正的只读存储器 (ROM) 中,但目前的个人电脑将 BIOS 保存在可更新的闪存中。BIOS 负责执行基本的系统初始化,如激活显卡和检查内存的安装量。完成初始化后,BIOS 会从软盘、硬盘、光盘或网络等适当位置加载操作系统,并将机器的控制权交给操作系统。
当英特尔公司最终以分别支持 16MB 和 4GB 物理地址空间的 80286 和 80386 处理器 "打破了一百万字节的障碍 "时,个人电脑架构师仍然保留了低 1MB 物理地址空间的原始布局,以确保与现有软件的向后兼容性。因此,现代 PC 的物理内存空间从 0x000A0000 到 0x00100000之间有一个 "洞",将 RAM 划分为 "低 "或 "常规内存"(前 640KB)和 "扩展内存"(其他)。此外,电脑 32 位物理地址空间最顶端的一些空间,即所有物理 RAM 的上方,现在通常被 BIOS 保留给 32 位 PCI 设备使用。
最新的 x86 处理器可支持超过 4GB 的物理 RAM,因此 RAM 可以进一步扩展到 0xFFFFFFFF 以上。在这种情况下,BIOS 必须在 32 位可寻址区域的顶部为系统 RAM 留出第二个孔,以便为这些 32 位设备留出映射空间。由于设计上的限制,JOS 只能使用个人电脑物理内存的前 256MB,所以现在我们就假设所有个人电脑都 "只有 "一个 32 位物理地址空间。但是,处理复杂的物理地址空间和多年来硬件组织的其他方面,是操作系统开发的重要实际挑战之一。
The ROM BIOS
在本部分实验中,您将使用 QEMU 的调试工具来研究 IA-32 兼容计算机的启动过程。
打开两个终端窗口,将两个 shell 都 cd 到实验室目录。在其中一个窗口中输入 make qemu-gdb(或 make qemu-nox-gdb)。这将启动 QEMU,但 QEMU 会在处理器执行第一条指令前停止,等待来自 GDB 的调试连接。在第二个终端,从运行 make 的同一目录,运行 make gdb。你应该会看到类似下面的内容:
第一个终端输入:make qemu-gdb,其将会停留在第一步
第二个终端输入:make gdb
第一行:[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
是 GDB 对要执行的第一条指令的反汇编。从输出结果中可以得出一些结论:
- IBM PC 从物理地址 0x000ffff0 开始执行,该地址位于为 ROM BIOS 预留的 64KB 区域的最顶端。
- PC 以 CS = 0xf000 和 IP = 0xfff0 开始执行。
- 执行的第一条指令是 jmp 指令,它跳转到 CS = 0xf000 和 IP = 0xe05b 的分段地址。
QEMU 为什么这样启动?英特尔就是这样设计 8088 处理器的,IBM 在最初的 PC 中也使用了这种处理器。因为 PC 中的 BIOS 是 "硬连接 "到物理地址范围 0x000f0000-0x000fffff 的,这种设计确保了 BIOS 在开机或任何系统重启后总是能首先获得机器的控制权,这一点至关重要,因为开机时,机器的 RAM 中没有其他软件可供处理器执行。QEMU 模拟器自带 BIOS,并将其置于处理器模拟物理地址空间中的该位置。处理器复位时,(模拟)处理器会进入实模式,并将 CS 设置为 0xf000,将 IP 设置为 0xfff0,这样执行就从该(CS:IP)段地址开始。分段地址 0xf000:fff0 如何变成物理地址?
$$
16*0xf000+0xfff0=0xffff0
$$
exercise 2
使用 GDB 的 si(步骤指令)命令跟踪 ROM BIOS 中的其他指令,并尝试猜测它可能在做什么。您可能需要查看 Phil Storrs 的 I/O 端口说明,以及 6.828 参考资料页面上的其他资料。无需弄清所有细节,只需先了解 BIOS 正在做什么的总体思路。
工具:gdb
gdb常用调试命令:
list(显示程序源代码)
break:在程序中设置断点,使用格式:break 要设置断点的行数
next/nexti:继续执行语句,跳过子程序的调用
step/stepi:与next一致,但是会跟踪到子程序的内部
continue:使程序暂停在断点之后继续运行
该实验中,输入si,逐句执行,查看反汇编代码。
反汇编代码:
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
[f000:e05b] 0xfe05b: cmpl $0x0,%cs:0x6ac8 ;0x0与f000:0x6ac8的值比较
[f000:e062] 0xfe062: jne 0xfd2e1 ;若不相等,则跳转至0xfd2e1
[f000:e066] 0xfe066: xor %dx,%dx ;对dx寄存器本身进行异或运算,结果为0
[f000:e068] 0xfe068: mov %dx,%ss ;将dx的值赋值给ss寄存器
[f000:e06a] 0xfe06a: mov $0x7000,%esp ;将0x7000赋值给esp寄存器
[f000:e070] 0xfe070: mov $0xf34c2,%edx ;将0xf34c2赋值给edx寄存器
[f000:e076] 0xfe076: jmp 0xfd15c ;cs=0xf000,ip=0xd15c
[f000:d15c] 0xfd15c: mov %eax,%ecx ;将eax寄存器的值赋值ecx
[f000:d15f] 0xfd15f: cli ;禁止中断
[f000:d160] 0xfd160: cld ;将DF设置为0,si和di递增
[f000:d161] 0xfd161: mov $0x8f,%eax ;将0x8f赋值给eax寄存器
[f000:d167] 0xfd167: out %al,$0x70 ;将al寄存器的值写给端口地址0x70
[f000:d169] 0xfd169: in $0x71,%al ;从端口地址0x71
[f000:d16b] 0xfd16b: in $0x92,%al ;从端口地址8f读取
[f000:d16d] 0xfd16d: or $0x2,%al ;
[f000:d16f] 0xfd16f: out %al,$0x92
[f000:d171] 0xfd171: lidtw %cs:0x6ab8
[f000:d177] 0xfd177: lgdtw %cs:0x6a74
[f000:d17d] 0xfd17d: mov %cr0,%eax
[f000:d180] 0xfd180: or $0x1,%eax
[f000:d184] 0xfd184: mov %eax,%cr0
[f000:d187] 0xfd187: ljmpl $0x8,$0xfd18f
ROM BIOS启动流程(日后再说)
BIOS 运行时,会设置中断描述符表,并初始化 VGA 显示屏等各种设备。这就是你在 QEMU 窗口中看到的 "Starting SeaBIOS "信息的来源。
初始化 PCI 总线和 BIOS 所知道的所有重要设备后,BIOS 会搜索软盘、硬盘或 CD-ROM 等可启动设备。最终,当找到可启动磁盘时,BIOS 会从磁盘读取启动加载程序并将控制权转移给它。