Merge pull request #9117 from wxy/20140110-Caffeinated-6.828--Lab-1--Booting-a-PC

PRF&PUB:20140110 Caffeinated 6.828  Lab 1  Booting a PC
This commit is contained in:
Xingyu.Wang 2018-06-12 20:56:02 +08:00 committed by GitHub
commit 18d7528fbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -9,7 +9,8 @@ Caffeinated 6.828:实验 1PC 的引导过程
本课程中你需要的文件和接下来的实验任务所需要的文件都是通过使用 [Git][1] 版本控制系统来分发的。学习更多关于 Git 的知识,请查看 [Git 用户手册][2],或者,如果你熟悉其它的版本控制系统,这个 [面向 CS 的 Git 概述][3] 可能对你有帮助。
本课程在 Git 仓库中的地址是 `https://exokernel.scripts.mit.edu/joslab.git`。在你的 Athena 帐户中安装文件,你需要运行如下的命令去克隆课程仓库。你也可以使用 `ssh -X athena.dialup.mit.edu` 去登入到一个公共的 Athena 主机。
本课程在 Git 仓库中的地址是 https://exokernel.scripts.mit.edu/joslab.git 。在你的 Athena 帐户中安装文件,你需要运行如下的命令去克隆课程仓库。你也可以使用 `ssh -X athena.dialup.mit.edu` 去登入到一个公共的 Athena 主机。
```
athena% mkdir ~/6.828
athena% cd ~/6.828
@ -18,16 +19,15 @@ athena% git clone https://exokernel.scripts.mit.edu/joslab.git lab
Cloning into lab...
athena% cd lab
athena%
```
Git 可以帮你跟踪代码中的变化。比如,如果你完成了一个练习,想在你的进度中打一个检查点,你可以运行如下的命令去提交你的变更:
```
athena% git commit -am 'my solution for lab1 exercise 9'
Created commit 60d2135: my solution for lab1 exercise 9
1 files changed, 1 insertions(+), 0 deletions(-)
athena%
```
你可以使用 `git diff` 命令跟踪你的变更。运行 `git diff` 将显示你的代码自最后一次提交之后的变更,而 `git diff origin/lab1` 将显示这个实验相对于初始代码的变更。在这里,`origin/lab1` 是为了完成这个作业,从我们的服务器上下载的初始代码在 Git 分支上的名字。
@ -40,9 +40,10 @@ athena%
我们为了你便于做实验,为你使用了不同的 Git 仓库。做实验用的仓库位于一个 SSH 服务器后面。你可以拥有你自己的实验仓库,其他的任何同学都不可访问你的这个仓库。为了通过 SSH 服务器的认证,你必须有一对 RSA 密钥,并让服务器知道你的公钥。
实验代码同时还带有一个脚本,它可以帮你设置如何访问你的实验仓库。在运行这个脚本之前,你必须在我们的 [submission web 界面][5] 上有一个帐户。在登陆页面上,输入你的 Athena 用户名,然后点击 "Mail me my password"。在你的邮箱中将马上接收到一封包含有你的 `6.828` 课程密码的邮件。注意,每次你点击这个按钮的时候,系统将随机给你分配一个新密码。
实验代码同时还带有一个脚本,它可以帮你设置如何访问你的实验仓库。在运行这个脚本之前,你必须在我们的 [submission web 界面][5] 上有一个帐户。在登陆页面上,输入你的 Athena 用户名,然后点击 “Mail me my password”。在你的邮箱中将马上接收到一封包含有你的 `6.828` 课程密码的邮件。注意,每次你点击这个按钮的时候,系统将随机给你分配一个新密码。
现在,你已经有了你的 `6.828` 密码,在 `lab` 目录下,运行如下的命令去配置实践仓库:
```
athena% make handin-prep
Using public key from ~/.ssh/id_rsa:
@ -59,10 +60,10 @@ Setting up hand-in Git repository...
Adding remote repository ssh://josgit@exokernel.mit.edu/joslab.git as 'handin'.
Done! Use 'make handin' to submit your lab code.
athena%
```
如果你没有 RSA 密钥对,这个脚本可能会询问你是否生成一个新的密钥对:
```
athena% make handin-prep
SSH key file ~/.ssh/id_rsa does not exists, generate one? [Y/n] Y
@ -85,6 +86,7 @@ athena%
```
当你开始动手做实验时,在 `lab` 目录下,输入 `make handin` 去使用 git 做第一次提交。后面将运行 `git push handin HEAD`,它将推送当前分支到远程 `handin` 仓库的同名分支上。
```
athena% git commit -am "ready to submit my lab"
[lab1 c2e3c8b] ready to submit my lab
@ -136,6 +138,7 @@ athena%
在 6.828 中,我们将使用 [QEMU 仿真器][12],它是一个现代化的并且速度非常快的仿真器。虽然 QEMU 内置的监视功能提供了有限的调试支持但是QEMU 也可以做为 [GNU 调试器][13] (GDB) 的远程调试目标,我们在这个实验中将使用它来一步一步完成引导过程。
在开始之前,按照前面 “软件安装“ 中在 Athena 主机上描述的步骤,提取实验 1 的文件到你自己的目录中,然后,在 `lab` 目录中输入 `make`(如果是 BSD 的系统,是输入 `gmake` )来构建最小的 6.828 引导加载器和用于启动的内核。(把在这里我们运行的这些代码称为 ”内核“ 有点夸大,但是,通过这个学期的课程,我们将把这些代码充实起来,成为真正的 ”内核“)
```
athena% cd lab
athena% make
@ -156,14 +159,16 @@ boot block is 414 bytes (max 510)
```
(如果你看到有类似 ”undefined reference to `__udivdi3'" 这样的错误,可能是因为你的电脑上没有 32 位的 ”gcc multilib“。如果你运行在 Debian 或者 Ubuntu你可以尝试去安装 ”gcc-multilib“ 包。)
(如果你看到有类似 ”undefined reference to `__udivdi3'” 这样的错误,可能是因为你的电脑上没有 32 位的 “gcc multilib”。如果你运行在 Debian 或者 Ubuntu你可以尝试去安装 “gcc-multilib” 包。)
现在,你可以去运行 QEMU 了,并将上面创建的 `obj/kern/kernel.img` 文件提供给它,以作为仿真 PC 的 “虚拟硬盘”,这个虚拟硬盘中包含了我们的引导加载器(`obj/boot/boot`) 和我们的内核(`obj/kernel`)。
```
athena% make qemu
```
运行 QEMU 时需要使用选项去设置硬盘,以及指示串行端口输出到终端。在 QEMU 窗口中将出现一些文本内容:
```
Booting from Hard Disk...
6828 decimal is XXX octal!
@ -184,9 +189,10 @@ Type 'help' for a list of commands.
K>
```
'`Booting from Hard Disk...`' 之后的内容,就是由我们的基本 JOS 内核输出的:`K>` 是包含在我们的内核中的小型监听器或者交互式控制程序的提示符。内核输出的这些行也会出现在你运行 QEMU 的普通 shell 窗口中。这是因为测试和实验分级的原因,我们配置了 JOS 的内核,使它将控制台输出不仅写入到虚拟的 VGA 显示器(就是 QEMU 窗口),也写入到仿真 PC 的虚拟串口上QEMU 会将虚拟串口上的信息转发到它的标准输出上。同样JOS 内核也将接收来自键盘和串口的输入,因此,你既可以从 VGA 显示窗口中输入命令,也可以从运行 QEMU 的终端窗口中输入命令。或者,你可以通过运行 `make qemu-nox` 来取消虚拟 VGA 的输出,只使用串行控制台来输出。如果你是通过 SSH 拨号连接到 Athena 主机,这样可能更方便。
`Booting from Hard Disk...` 之后的内容,就是由我们的基本 JOS 内核输出的:`K>` 是包含在我们的内核中的小型监听器或者交互式控制程序的提示符。内核输出的这些行也会出现在你运行 QEMU 的普通 shell 窗口中。这是因为测试和实验分级的原因,我们配置了 JOS 的内核,使它将控制台输出不仅写入到虚拟的 VGA 显示器(就是 QEMU 窗口),也写入到仿真 PC 的虚拟串口上QEMU 会将虚拟串口上的信息转发到它的标准输出上。同样JOS 内核也将接收来自键盘和串口的输入,因此,你既可以从 VGA 显示窗口中输入命令,也可以从运行 QEMU 的终端窗口中输入命令。或者,你可以通过运行 `make qemu-nox` 来取消虚拟 VGA 的输出,只使用串行控制台来输出。如果你是通过 SSH 拨号连接到 Athena 主机,这样可能更方便。
在这里有两个可以用来监视内核的命令,它们是 `help``kerninfo`
```
K> help
help - display this list of commands
@ -206,7 +212,8 @@ K>
#### PC 的物理地址空间
我们现在将更深入去了解 ”关于 PC 是如何启动“ 的更多细节。一台 PC 的物理地址空间是硬编码为如下的布局:
我们现在将更深入去了解 “关于 PC 是如何启动” 的更多细节。一台 PC 的物理地址空间是硬编码为如下的布局:
```
+------------------+ <- 0xFFFFFFFF (4GB)
| 32-bit |
@ -244,15 +251,16 @@ K>
`0x000A0000``0x000FFFFF` 的 384 KB 的区域是为特定硬件保留的区域,比如,视频显示缓冲和保存在非易失存储中的固件。这个保留区域中最重要的部分是基本输入/输出系统BIOS它位于从 `0x000F0000``0x000FFFFF` 之间的 64KB 大小的区域。在早期的 PC 中BIOS 在真正的只读存储ROM但是现在的 PC 的 BIOS 都保存在可更新的 FLASH 存储中。BIOS 负责执行基本系统初始化工作比如激活视频卡和检查已安装的内存数量。这个初始化工作完成之后BIOS 从相关位置加载操作系统比如从软盘、硬盘、CD-ROM、或者网络然后将机器的控制权传递给操作系统。
当 Intel 最终在 80286 和 80386 处理器上 “打破了 1MB 限制” 之后,这两个处理器各自支持 16MB 和 4GB 物理地址空间尽管如此为了确保向下兼容现存软件PC 架构还是保留着 1 MB 以内物理地址空间的原始布局。因此,现代 PC 的物理内存,在 `0x000A0000``0x00100000` 之间有一个 "黑洞区域“,将内存分割为 ”低位“ 或者 ”传统内存“ 区域(前 640 KB和 ”扩展内存“(其它的部分)。除此之外,在 PC 的 32 位物理地址空间顶部之上的一些空间,在全部的物理内存上面,现在一般都由 BIOS 保留给 32 位的 PCI 设备使用。
当 Intel 最终在 80286 和 80386 处理器上 “打破了 1MB 限制” 之后,这两个处理器各自支持 16MB 和 4GB 物理地址空间尽管如此为了确保向下兼容现存软件PC 架构还是保留着 1 MB 以内物理地址空间的原始布局。因此,现代 PC 的物理内存,在 `0x000A0000``0x00100000` 之间有一个 “黑洞区域”,将内存分割为 “低位” 或者 “传统内存” 区域(前 640 KB和 “扩展内存”(其它的部分)。除此之外,在 PC 的 32 位物理地址空间顶部之上的一些空间,在全部的物理内存上面,现在一般都由 BIOS 保留给 32 位的 PCI 设备使用。
最新的 x86 处理器可以支持超过 4GB 的物理地址空间因此RAM 可以进一步扩展到 `0xFFFFFFFF` 之上。在这种情况下BIOS 必须在 32 位可寻址空间顶部之上的系统 RAM 上,设置第二个 ”黑洞区域“,以便于为这些 32 位的设备映射留下空间。因为 JOS 设计的限制,它仅可以使用 PC 物理内存的前 256 MB因此我们将假设所有的 PC ”仅仅“ 拥有 32 位物理地址空间。但是处理复杂的物理地址空间和其它部分的硬件系统,将涉及到许多年前操作系统开发所遇到的实际挑战之一。
最新的 x86 处理器可以支持超过 4GB 的物理地址空间因此RAM 可以进一步扩展到 `0xFFFFFFFF` 之上。在这种情况下BIOS 必须在 32 位可寻址空间顶部之上的系统 RAM 上,设置第二个 “黑洞区域”,以便于为这些 32 位的设备映射留下空间。因为 JOS 设计的限制,它仅可以使用 PC 物理内存的前 256 MB因此我们将假设所有的 PC “仅仅” 拥有 32 位物理地址空间。但是处理复杂的物理地址空间和其它部分的硬件系统,将涉及到许多年前操作系统开发所遇到的实际挑战之一。
#### ROM BIOS
在实验的这一部分中,你将使用 QEMU 的调试功能去研究 IA-32 相关的计算机是如何引导的。
打开两个终端窗口,在其中一个中,输入 `make qemu-gdb`(或者 `make qemu-nox-gdb`),这将启动 QEMU但是处理器在运行第一个指令之前将停止 QEMU以等待来自 GDB 的调试连接。在第二个终端窗口中,从相同的目录中运行 `make`,以及运行 `make gdb`。你将看到如下的输出。
```
athena% make gdb
GNU gdb (GDB) 6.8-debian
@ -274,6 +282,7 @@ The target architecture is assumed to be i8086
`make gdb` 的运行目标是一个称为 `.gdbrc` 的脚本,它设置了 GDB 在早期引导期间调试所用到的 16 位代码,并且将它指向到正在监听的 QEMU 上。
下列行:
```
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
```
@ -284,11 +293,10 @@ The target architecture is assumed to be i8086
* PC 使用 `CS = 0xf000``IP = 0xfff0` 开始运行。
* 运行的第一个指令是一个 `jmp` 指令,它跳转段地址 `CS = 0xf000``IP = 0xe05b`
为什么 QEMU 是这样开始的呢?这是因为 Intel 设计的 8088 处理器是这样做的,这个处理器是 IBM 最早用在他们的 PC 上的处理器。因为在一台 PC 中BIOS 是硬编码在物理地址范围 `0x000f0000-0x000fffff` 中的这样的设计确保了在机器接通电源或者任何系统重启之后BIOS 总是能够首先控制机器 —— 这是至关重要的因为机器接通电源之后在机器的内存中没有处理器可以运行的任何软件。QEMU 仿真器有它自己的 BIOS它的位置在处理器的模拟地址空间中。在处理器复位之后模拟的处理器进入了实模式然后设置 `CS``0xf000` 、`IP` 为 `0xfff0`,所以,运行开始于那个(`CS:IP`)段地址。那么,段地址 `0xf000:fff0` 是如何转到物理地址的呢?
在回答这个问题之前我们需要了解有关实模式地址的知识。在实模式PC 启动之后就处于实模式)中,物理地址是根据这个公式去转换的:物理地址 = 16 * 段地址 + 偏移。因此,当 PC 设置 `CS``0xf000` 、`IP` 为 `0xfff0` 之后,物理地址指向到:
为什么 QEMU 是这样开始的呢?这是因为 Intel 设计的 8088 处理器是这样做的,这个处理器是 IBM 最早用在他们的 PC 上的处理器。因为在一台 PC 中BIOS 是硬编码在物理地址范围 `0x000f0000-0x000fffff` 中的这样的设计确保了在机器接通电源或者任何系统重启之后BIOS 总是能够首先控制机器 —— 这是至关重要的因为机器接通电源之后在机器的内存中没有处理器可以运行的任何软件。QEMU 仿真器有它自己的 BIOS它的位置在处理器的模拟地址空间中。在处理器复位之后模拟的处理器进入了实模式然后设置 CS 为 `0xf000` 、IP 为 `0xfff0`所以运行开始于那个CS:IP段地址。那么段地址 0xf000:fff0 是如何转到物理地址的呢?
在回答这个问题之前我们需要了解有关实模式地址的知识。在实模式PC 启动之后就处于实模式)中,物理地址是根据这个公式去转换的:物理地址 = 16 * 段地址 + 偏移。因此,当 PC 设置 CS 为 `0xf000` 、IP 为 `0xfff0` 之后,物理地址指向到:
```
16 * 0xf000 + 0xfff0 # in hex multiplication by 16 is
= 0xf0000 + 0xfff0 # easy--just append a 0.
@ -302,22 +310,20 @@ The target architecture is assumed to be i8086
>
> 使用 GDB 的 `si`(步进指令)指令去跟踪进入到 ROM BIOS 的更多指令,然后尝试猜测它可能会做什么。你可能需要去查看 [Phil Storrs I/O 端口描述][14],以及在 [6.828 参考资料页面][8] 上的其它资料。不需要了解所有的细节 —— 只要搞明白 BIOS 首先要做什么就可以了。
当 BIOS 运行后,它将设置一个中断描述符表和初始化各种设备,比如, VGA 显示。在这时,你在 QEMU 窗口中将出现 "`Starting SeaBIOS`" 的信息。
当 BIOS 运行后,它将设置一个中断描述符表和初始化各种设备,比如, VGA 显示。在这时,你在 QEMU 窗口中将出现 `Starting SeaBIOS` 的信息。
在初始化 PCI 产品线和 BIOS 知道的所有重要设备之后,它将搜索可引导设备,比如,一个软盘、硬盘、或者 CD-ROM。最后当它找到可引导磁盘之后BIOS 从可引导硬盘上读取引导加载器,然后将控制权交给它。
### 第二部分:引导加载器
在 PC 的软盘和硬盘中,将它们分割成 512 字节大小的区域,每个区域称为一个扇区。一个扇区就是磁盘的最小转存单元:每个读或写操作都必须是一个或多个扇区大小,并且按扇区边界进行对齐。如果磁盘是可引导盘,第一个扇区则为引导扇区,因为,第一个扇区中驻留有引导加载器的代码。当 BIOS 找到一个可引导软盘或者硬盘时,它将 512 字节的引导扇区加载进物理地址为 0x7c00 到 `0x7dff` 的内存中,然后使用一个 `jmp` 指令设置 CS:IP 为 `0000:7c00`,并传递控制权到引导加载器。与 BIOS 加载地址一样,这些地址是任意的 —— 但是它们对于 PC 来说是固定的,并且是标准化的。
在 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 格式规范][15]。
后来,随着 PC 的技术进步,它们可以从 CD-ROM 中引导因此PC 架构师趁机对引导过程进行轻微的调整。最后的结果使现代的 BIOS 从 CD-ROM 中引导的过程更复杂并且功能更强大。CD-ROM 使用 2048 字节大小的扇区,而不是 512 字节的扇区并且BIOS 在传递控制权之前,可以从磁盘上加载更大的(不止是一个扇区)引导镜像到内存中。更多内容,请查看 [“El Torito” 可引导 CD-ROM 格式规范][15]。
不过对于 6.828,我们将使用传统的硬盘引导机制,意味着我们的引导加载器必须小于 512 字节。引导加载器是由一个汇编源文件 `boot/boot.S` 和一个 C 源文件 `boot/main.c` 构成,仔细研究这些源文件可以让你彻底理解引导加载器都做了些什么。引导加载器必须要做两件主要的事情:
1. 第一,引导加载器将处理器从实模式切换到 32 位保护模式,因为只有在 32 位保护模式中,软件才能够访问处理器中 1 MB 以上的物理地址空间。关于保护模式将在 [PC 汇编语言][6] 的 1.2.7 和 1.2.8 节中详细描述,更详细的内容请参阅 Intel 架构手册。在这里,你只要理解在保护模式中段地址(段基地址:偏移量)与物理地址转换的差别就可以了,并且转换后的偏移是 32 位而不是 16 位。
2. 第二,引导加载器通过 x86 的专用 I/O 指令直接访问 IDE 磁盘设备寄存器,从硬盘中读取内核。如果你想去更好地了解在这里说的专用 I/O 指令,请查看 [6.828 参考页面][8] 上的 ”IDE 硬盘控制器“ 章节。你不用学习太多的专用设备编程方面的内容:在实践中,写设备驱动程序是操作系统开发中的非常重要的部分,但是,从概念或者架构的角度看,它也是最让人乏味的部分。
1. 第一、引导加载器将处理器从实模式切换到 32 位保护模式,因为只有在 32 位保护模式中,软件才能够访问处理器中 1 MB 以上的物理地址空间。关于保护模式将在 [PC 汇编语言][6] 的 1.2.7 和 1.2.8 节中详细描述,更详细的内容请参阅 Intel 架构手册。在这里,你只要理解在保护模式中段地址(段基地址:偏移量)与物理地址转换的差别就可以了,并且转换后的偏移是 32 位而不是 16 位。
2. 第二、引导加载器通过 x86 的专用 I/O 指令直接访问 IDE 磁盘设备寄存器,从硬盘中读取内核。如果你想去更好地了解在这里说的专用 I/O 指令,请查看 [6.828 参考页面][8] 上的 “IDE 硬盘控制器” 章节。你不用学习太多的专用设备编程方面的内容:在实践中,写设备驱动程序是操作系统开发中的非常重要的部分,但是,从概念或者架构的角度看,它也是最让人乏味的部分。
理解了引导加载器源代码之后,我们来看一下 `obj/boot/boot.asm` 文件。这个文件是在引导加载器编译过程中,由我们的 GNUmakefile 创建的引导加载器的反汇编文件。这个反汇编文件让我们可以更容易地看到引导加载器代码所处的物理内存位置,并且也可以更容易地跟踪在 GDB 中步进的引导加载器发生了什么事情。同样的,`obj/kern/kernel.asm` 文件中包含了 JOS 内核的一个反汇编,它也经常被用于内核调试。
@ -340,8 +346,6 @@ The target architecture is assumed to be i8086
* 内核的第一个指令在哪里?
* 为从硬盘上获取完整的内核,引导加载器如何决定有多少扇区必须被读入?在哪里能找到这些信息?
#### 加载内核
我们现在来进一步查看引导加载器在 `boot/main.c` 中的 C 语言部分的详细细节。在继续之前,我们先停下来回顾一下 C 语言编程的基础知识。
@ -350,7 +354,7 @@ The target architecture is assumed to be i8086
>
> 下载 [pointers.c][17] 的源代码,运行它,然后确保你理解了输出值的来源的所有内容。尤其是,确保你理解了第 1 行和第 6 行的指针地址的来源、第 2 行到第 4 行的值是如何得到的、以及为什么第 5 行指向的值表面上看像是错误的。
>
> 如果你对指针的使用不熟悉Brian Kernighan 和 Dennis Ritchie就是大家知道的 'K&R'写的《C Programming Language》是一个非常好的参考书。同学们可以去买这本书这里是 [Amazon 购买链接][18]),或者在 [MIT 的图书馆有 7 个拷贝][19] 中找到其中一个。在 [SIPB Office][20] 也有三个拷贝可以细读。
> 如果你对指针的使用不熟悉Brian Kernighan 和 Dennis Ritchie就是大家知道的 “K&R”写的《C Programming Language》是一个非常好的参考书。同学们可以去买这本书这里是 [Amazon 购买链接][18]),或者在 [MIT 的图书馆的 7 个副本][19] 中找到其中一个。在 [SIPB Office][20] 也有三个副本可以细读。
>
> 在课程阅读中,[Ted Jensen 写的教程][21] 可以使用,它大量引用了 K&R 的内容。
>
@ -368,11 +372,10 @@ The target architecture is assumed to be i8086
* `.rodata`:只读数据,比如,由 C 编译器生成的 ASCII 字符串常量。(然而我们并不需要操心设置硬件去禁止写入它)
* `.data`:保持在程序的初始化数据中的数据节,比如,初始化声明所需要的全局变量,比如,像 `int x = 5;`
当链接器计算程序的内存布局的时候,它为未初始化的全局变量保留一些空间,比如,`int x;`,在内存中的被称为 `.bss` 的节后面会马上跟着一个 `.data`。C 规定 "未初始化的" 全局变量以一个 0 值开始。因此,在 ELF 二进制中 `.bss` 中并不存储内容;而是,链接器只记录地址和`.bss` 节的大小。加载器或者程序自身必须在 `.bss` 节中写入 0。
通过输入如下的命令来检查在内核中可运行的所有节的名字、大小、以及链接地址的列表:
```
athena% i386-jos-elf-objdump -h obj/kern/kernel
```
@ -381,7 +384,7 @@ athena% i386-jos-elf-objdump -h obj/kern/kernel
你将看到更多的节,而不仅是上面列出的那几个,但是,其它的那些节对于我们的实验目标来说并不重要。其它的那些节中大多数都是为了保留调试信息,它们一般包含在程序的可执行文件中,但是,这些节并不会被程序加载器加载到内存中。
我们需要特别注意 `.text` 节中的 "VMA"(或者链接地址)和 "LMA"(或者加载地址)。一个节的加载地址是那个节加载到内存中的地址。在 ELF 对象中,它保存在 `ph->p_pa` 域(在本案例中,它实际上是物理地址,不过 ELF 规范在这个域的意义方面规定的很模糊)。
我们需要特别注意 `.text` 节中的 VMA或者链接地址和 LMA或者加载地址)。一个节的加载地址是那个节加载到内存中的地址。在 ELF 对象中,它保存在 `ph->p_pa` 域(在本案例中,它实际上是物理地址,不过 ELF 规范在这个域的意义方面规定的很模糊)。
一个节的链接地址是这个节打算在内存中运行时的地址。链接器在二进制代码中以变量的方式去编码这个链接地址,比如,当代码需要全局变量的地址时,如果二进制代码从一个未链接的地址去运行,结果将是无法运行。(它一般是去生成一个不包含任何一个绝对地址的、与位置无关的代码。现在的共享库大量使用的就是这种方法,但这是以性能和复杂性为代价的,所以,我们在 6.828 中不使用这种方法。)
@ -394,7 +397,7 @@ BIOS 加载引导扇区到内存中的 0x7c00 地址,因此,这就是引导
> **练习 5**
>
> 如果你得到一个错误的引导加载器链接地址,通过再次跟踪引导加载器的前几个指令,你将会发现第一个指令会 ”中断“ 或者出错。然后在 `boot/Makefrag` 修改链接地址来修复错误,运行 `make clean`,使用 `make` 重新编译,然后再次跟踪引导加载器去查看会发生什么事情。不要忘了改回正确的链接地址,然后再次 `make clean`
> 如果你得到一个错误的引导加载器链接地址,通过再次跟踪引导加载器的前几个指令,你将会发现第一个指令会 “中断” 或者出错。然后在 `boot/Makefrag` 修改链接地址来修复错误,运行 `make clean`,使用 `make` 重新编译,然后再次跟踪引导加载器去查看会发生什么事情。不要忘了改回正确的链接地址,然后再次 `make clean`
我们继续来看内核的加载和链接地址。与引导加载器不同,这里有两个不同的地址:内核告诉引导加载器加载它到内存的低位地址(小于 1 MB 的地址),但是它期望在一个高位地址来运行。我们将在下一节中深入研究它是如何实现的。
@ -407,9 +410,9 @@ athena% i386-jos-elf-objdump -f obj/kern/kernel
> **练习 6**
>
> 我们可以使用 GDB 的 `x` 命令去检查内存。[GDB 手册][23] 上讲的非常详细,但是现在,我们知道命令 `x/Nx ADDR` 是输出地址 `ADDR``N`word~~致校对word 有“单词”、“命令”、“消息”、“结论” 等意思,到底哪个更适合,我个人认为根据上下文,”单词“ 更适合一些,这里推送的 ”词“ 可能是指令,也可能是数据~~)就够了。(注意在命令中所有的 `x` 都是小写。警告word的多少并没有一个普遍的标准。在 GNU 汇编中一个词word是两个字节(在 xorw 中的 'w',它在这个词中就是 2 个字节)。
> 我们可以使用 GDB 的 `x` 命令去检查内存。[GDB 手册][23] 上讲的非常详细,但是现在,我们知道命令 `x/Nx ADDR` 是输出地址 `ADDR``N`<ruby><rt>word</rt></ruby>就够了。(注意在命令中所有的 `x` 都是小写。)警告:<ruby><rt>word</rt></ruby>的多少并没有一个普遍的标准。在 GNU 汇编中,一个<ruby><rt>word</rt></ruby>是两个字节(在 xorw 中的 'w',它在这个词中就是 2 个字节)。
重置机器(退出 QEMU/GDB 然后再次启动它们)。检查内存中在 `0x00100000` 地址上的 8 个词word,输出 BIOS 上的引导加载器入口,然后再次找出引导载器上的内核的入口。为什么它们不一样?在第二个断点上有什么内容?(你并不用真的在 QEMU 上去回答这个问题,只需要思考就可以。)
重置机器(退出 QEMU/GDB 然后再次启动它们)。检查内存中在 `0x00100000` 地址上的 8 个词,输出 BIOS 上的引导加载器入口,然后再次找出引导载器上的内核的入口。为什么它们不一样?在第二个断点上有什么内容?(你并不用真的在 QEMU 上去回答这个问题,只需要思考就可以。)
### 第三部分:内核
@ -435,7 +438,7 @@ athena% i386-jos-elf-objdump -f obj/kern/kernel
#### 格式化控制台的输出
大多数人认为像 `printf()` 这样的函数是天生就有的,有时甚至认为这是 C 语言的 ”原语“。但是在操作系统的内核中,我们需要自己去实现所有的 I/O。
大多数人认为像 `printf()` 这样的函数是天生就有的,有时甚至认为这是 C 语言的 “原语”。但是在操作系统的内核中,我们需要自己去实现所有的 I/O。
通过阅读 `kern/printf.c`、`lib/printfmt.c`、以及 `kern/console.c`,确保你理解了它们之间的关系。在后面的实验中,你将会明白为什么 `printfmt.c` 是位于单独的 `lib` 目录中。
@ -473,15 +476,15 @@ athena% i386-jos-elf-objdump -f obj/kern/kernel
>
> 4. 运行下列代码:
>
> ```
> ```
> unsigned int i = 0x00646c72;
> cprintf("H%x Wo%s", 57616, &i);
> ```
> 输出是什么?解释如何在前面的练习中一步一步实现这个输出。这是一个 [ASCII 表][24],它是一个字节到字符串的映射表。
> 输出是什么?解释如何在前面的练习中一步一步实现这个输出。这是一个 [ASCII 表][24],它是一个字节到字符串的映射表。
>
> 这个输出取决于 x86 是小端法这一事实。如果这个 x86 采用大端法格式,你怎么去设置 `i`,以产生相同的输出?你需要将 `57616` 改变为一个不同值吗?
> 这个输出取决于 x86 是小端法这一事实。如果这个 x86 采用大端法格式,你怎么去设置 `i`,以产生相同的输出?你需要将 `57616` 改变为一个不同值吗?
>
> [这是小端法和大端法的描述][25] 和 [一个更古怪的描述][26]。
> [这是小端法和大端法的描述][25] 和 [一个更古怪的描述][26]。
>
> 5. 在下列代码中,`y=` 会输出什么?(注意:这个问题没有确切值)为什么会发生这种情况?
> `cprintf("x=%d y=%d", 3);`
@ -518,7 +521,7 @@ Stack backtrace:
输出的第一行列出了当前运行的函数,名字为 `mon_backtrace`,就是它自己,第二行列出了被 `mon_backtrace` 调用的函数,第三行列出了另一个被调用的函数,依次类推。你可以输出所有未完成的栈帧。通过研究 `kern/entry.S`,你可以发现,有一个很容易的方法告诉你何时停止。
在每一行中,`ebp` 表示了那个函数进入栈的基指针:即,栈指针的位置,它就是函数进入之后,函数的前序代码设置的基指针。`eip` 值列出的是函数的返回指令指针:当函数返回时,指令地址将控制返回。返回指令指针一般指向 `call` 指令之后的指令(想一想为什么?)。在 `args` 之后列出的五个十六进制值是在问题中传递给函数的前五个参数。当然,如果函数调用时传递的参数少于五个,那么,在这里就不会列出全部五个值了。(为什么跟踪回溯代码不能检测到调用时实际上传递了多少个参数?如何去修复这个 ”缺陷“
在每一行中,`ebp` 表示了那个函数进入栈的基指针:即,栈指针的位置,它就是函数进入之后,函数的前序代码设置的基指针。`eip` 值列出的是函数的返回指令指针:当函数返回时,指令地址将控制返回。返回指令指针一般指向 `call` 指令之后的指令(想一想为什么?)。在 `args` 之后列出的五个十六进制值是在问题中传递给函数的前五个参数。当然,如果函数调用时传递的参数少于五个,那么,在这里就不会列出全部五个值了。(为什么跟踪回溯代码不能检测到调用时实际上传递了多少个参数?如何去修复这个 “缺陷”
下面是在阅读 K&R 的书中的第 5 章中的一些关键点,为了接下来的练习和将来的实验,你应该记住它们。
@ -526,8 +529,6 @@ Stack backtrace:
* `p[i]` 的定义与 `*(p+i)` 定义是相同的,都反映了在内存中由 `p` 指向的第 `i` 个对象。当对象大于一个字节时,上面的加法规则可以使这个定义正常工作。
* `&p[i]``(p+i)` 是相同的,获取在内存中由 p 指向的第 `i` 个对象的地址。
虽然大多数 C 程序不需要在指针和整数之间转换,但是操作系统经常做这种转换。不论何时,当你看到一个涉及内存地址的加法时,你要问你自己,你到底是要做一个整数加法还是一个指针加法,以确保做完加法后的值是正确的,而不是相乘后的结果。
> **练 11**
@ -545,16 +546,16 @@ Stack backtrace:
`debuginfo_eip` 中,`__STAB_*` 来自哪里?这个问题的答案很长;为帮助你找到答案,下面是你需要做的一些事情:
* 在 `kern/kernel.ld` 文件中查找 `__STAB_*`
* 运行 i386-jos-elf-objdump -h obj/kern/kernel
* 运行 i386-jos-elf-objdump -G obj/kern/kernel
* 运行 i386-jos-elf-gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s。
* 运行 `i386-jos-elf-objdump -h obj/kern/kernel`
* 运行 `i386-jos-elf-objdump -G obj/kern/kernel`
* 运行 `i386-jos-elf-gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s`
* 如果引导加载器在加载二进制内核时,将符号表作为内核的一部分加载进内存中,那么,去查看它。
通过在 `stab_binsearch` 中插入调用,可以完成在 `debuginfo_eip` 中通过地址找到行号的功能。
在内核监视中添加一个 `backtrace` 命令,扩展你实现的 `mon_backtrace` 的功能,通过调用 `debuginfo_eip`,然后以下面的格式来输出每个栈帧行:
```
K> backtrace
Stack backtrace:
@ -571,7 +572,7 @@ K>
为防止评级脚本引起混乱,应该将文件和函数名输出在单独的行上。
提示:`printf` 格式的字符串提供一个易用(尽管有些难理解)的方式去输出非空终止non-null-terminated字符串,就像在 STABS 表中的这些一样。`printf("%.*s", length, string)` 输出 `string` 中的最多 `length` 个字符。查阅 `printf` 的 man 页面去搞清楚为什么这样工作。
提示:`printf` 格式的字符串提供一个易用(尽管有些难理解)的方式去输出<ruby>非空终止<rt>non-null-terminated</rt></ruby>字符串,就像在 STABS 表中的这些一样。`printf("%.*s", length, string)` 输出 `string` 中的最多 `length` 个字符。查阅 `printf` 的 man 页面去搞清楚为什么这样工作。
你可以从 `backtrace` 中找到那些没有的功能。比如,你或者可能看到一个到 `monitor()` 的调用,但是没有到 `runcmd()` 中。这是因为编译器的行内in-lines函数调用。其它的优化可能导致你看到一些意外的行号。如果你从 `GNUMakefile` 删除 `-O2` 参数,`backtraces` 可能会更有意义(但是你的内核将运行的更慢)。
@ -583,7 +584,7 @@ via: https://sipb.mit.edu/iap/6.828/lab/lab1/
作者:[mit][a]
译者:[qhwdw](https://github.com/qhwdw)
校对:[校对者ID](https://github.com/校对者ID)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出