Caffeinated 6.828：实验工具指南
======

熟悉你的环境对高效率的开发和调试来说是至关重要的。本文将为你简单概述一下 JOS 环境和非常有用的 GDB 和 QEMU 命令。话虽如此，但你仍然应该去阅读 GDB 和 QEMU 手册，来理解这些强大的工具如何使用。

### 调试小贴士

#### 内核

GDB 是你的朋友。使用 `qemu-gdb target`（或它的变体 `qemu-gdb-nox`）使 QEMU 等待 GDB 去绑定。下面在调试内核时用到的一些命令，可以去查看 GDB 的资料。

如果你遭遇意外的中断、异常、或三重故障，你可以使用 `-d` 参数要求 QEMU 去产生一个详细的中断日志。

调试虚拟内存问题时，尝试 QEMU 的监视命令 `info mem`（提供内存高级概述）或 `info pg`（提供更多细节内容）。注意，这些命令仅显示**当前**页表。

（在实验 4 以后）去调试多个 CPU 时，使用 GDB 的线程相关命令，比如 `thread` 和 `info threads`。

#### 用户环境（在实验 3 以后）

GDB 也可以去调试用户环境，但是有些事情需要注意，因为 GDB 无法区分开多个用户环境或区分开用户环境与内核环境。

你可以使用 `make run-name`（或编辑 `kern/init.c` 目录）来指定 JOS 启动的用户环境，为使 QEMU 等待 GDB 去绑定，使用 `run-name-gdb` 的变体。

你可以符号化调试用户代码，就像调试内核代码一样，但是你要告诉 GDB，哪个符号表用到符号文件命令上，因为它一次仅能够使用一个符号表。提供的 `.gdbinit` 用于加载内核符号表 `obj/kern/kernel`。对于一个用户环境，这个符号表在它的 ELF 二进制文件中，因此你可以使用 `symbol-file obj/user/name` 去加载它。不要从任何 `.o` 文件中加载符号，因为它们不会被链接器迁移进去（库是静态链接进 JOS 用户二进制文件中的，因此这些符号已经包含在每个用户二进制文件中了）。确保你得到了正确的用户二进制文件；在不同的二进制文件中，库函数被链接为不同的 EIP，而 GDB 并不知道更多的内容！

（在实验 4 以后）因为 GDB 绑定了整个虚拟机，所以它可以将时钟中断看作为一种控制转移。这使得从底层上不可能实现步进用户代码，因为一个时钟中断无形中保证了片刻之后虚拟机可以再次运行。因此可以使用 `stepi` 命令，因为它阻止了中断，但它仅可以步进一个汇编指令。断点一般来说可以正常工作，但要注意，因为你可能在不同的环境（完全不同的一个二进制文件）上遇到同一个 EIP。

### 参考

#### JOS makefile

JOS 的 GNUmakefile 包含了在各种方式中运行的 JOS 的许多假目标。所有这些目标都配置 QEMU 去监听 GDB 连接（`*-gdb` 目标也等待这个连接）。要在运行中的 QEMU 上启动它，只需要在你的实验目录中简单地运行 `gdb ` 即可。我们提供了一个 `.gdbinit` 文件，它可以在 QEMU 中自动指向到 GDB、加载内核符号文件、以及在 16 位和 32 位模式之间切换。退出 GDB 将关闭 QEMU。

  * `make qemu`
    
    在一个新窗口中构建所有的东西并使用 VGA 控制台和你的终端中的串行控制台启动 QEMU。想退出时，既可以关闭 VGA 窗口，也可以在你的终端中按 `Ctrl-c` 或 `Ctrl-a x`。
  * `make qemu-nox`
    
    和 `make qemu` 一样，但仅使用串行控制台来运行。想退出时，按下 `Ctrl-a x`。这种方式在通过 SSH 拨号连接到 Athena 上时非常有用，因为 VGA 窗口会占用许多带宽。
  * `make qemu-gdb`
    
    和 `make qemu` 一样，但它与任意时间被动接受 GDB 不同，而是暂停第一个机器指令并等待一个 GDB 连接。
  * `make qemu-nox-gdb`
    
    它是 `qemu-nox` 和 `qemu-gdb` 目标的组合。
  * `make run-nam`
    
    （在实验 3 以后）运行用户程序 _name_。例如，`make run-hello` 运行 `user/hello.c`。
  * `make run-name-nox`,`run-name-gdb`, `run-name-gdb-nox`
    
    （在实验 3 以后）与 `qemu` 目标变量对应的 `run-name` 的变体。

makefile 也接受几个非常有用的变量：

  * `make V=1 …`
    
    详细模式。输出正在运行的每个命令，包括参数。
  * `make V=1 grade`
    
    在评级测试失败后停止，并将 QEMU 的输出放入 `jos.out` 文件中以备检查。
  * `make QEMUEXTRA=' _args_ ' …`
    
    指定传递给 QEMU 的额外参数。

#### JOS obj/

在构建 JOS 时，makefile 也产生一些额外的输出文件，这些文件在调试时非常有用：

  * `obj/boot/boot.asm`、`obj/kern/kernel.asm`、`obj/user/hello.asm`、等等。
    
    引导加载器、内核、和用户程序的汇编代码列表。
  * `obj/kern/kernel.sym`、`obj/user/hello.sym`、等等。
    
    内核和用户程序的符号表。
  * `obj/boot/boot.out`、`obj/kern/kernel`、`obj/user/hello`、等等。
    
    内核和用户程序链接的 ELF 镜像。它们包含了 GDB 用到的符号信息。

#### GDB

完整的 GDB 命令指南请查看 [GDB 手册][1]。下面是一些在 6.828 课程中非常有用的命令，它们中的一些在操作系统开发之外的领域几乎用不到。

  * `Ctrl-c`
     
     在当前指令处停止机器并打断进入到 GDB。如果 QEMU 有多个虚拟的 CPU，所有的 CPU 都会停止。
  * `c`（或 `continue`）
    
    继续运行，直到下一个断点或 `Ctrl-c`。
  * `si`（或 `stepi`）
    
    运行一个机器指令。
  * `b function` 或 `b file:line`（或 `breakpoint`）
    
    在给定的函数或行上设置一个断点。
  * `b * addr`（或 `breakpoint`）
    
    在 EIP 的 addr 处设置一个断点。
  * `set print pretty`
    
    启用数组和结构的美化输出。
  * `info registers`
    
    输出通用寄存器 `eip`、`eflags`、和段选择器。更多更全的机器寄存器状态转储，查看 QEMU 自己的 `info registers` 命令。
  * `x/ N x addr`
    
    以十六进制显示虚拟地址 addr 处开始的 N 个词的转储。如果 N 省略，默认为 1。addr 可以是任何表达式。
  * `x/ N i addr`
    
    显示从 addr 处开始的 N 个汇编指令。使用 `$eip` 作为 addr 将显示当前指令指针寄存器中的指令。
  * `symbol-file file`
    
    （在实验 3 以后）切换到符号文件 file 上。当 GDB 绑定到 QEMU 后，它并不是虚拟机中进程边界内的一部分，因此我们要去告诉它去使用哪个符号。默认情况下，我们配置 GDB 去使用内核符号文件 `obj/kern/kernel`。如果机器正在运行用户代码，比如是 `hello.c`，你就需要使用 `symbol-file obj/user/hello` 去切换到 hello 的符号文件。

QEMU 将每个虚拟 CPU 表示为 GDB 中的一个线程，因此你可以使用 GDB 中所有的线程相关的命令去查看或维护 QEMU 的虚拟 CPU。

  * `thread n`
    
    GDB 在一个时刻只关注于一个线程（即：CPU）。这个命令将关注的线程切换到 n，n 是从 0 开始编号的。
  * `info threads`
    
    列出所有的线程（即：CPU），包括它们的状态（活动还是停止）和它们在什么函数中。


#### QEMU

QEMU 包含一个内置的监视器，它能够有效地检查和修改机器状态。想进入到监视器中，在运行 QEMU 的终端中按入 `Ctrl-a c` 即可。再次按下 `Ctrl-a c` 将切换回串行控制台。

监视器命令的完整参考资料，请查看 [QEMU 手册][2]。下面是 6.828 课程中用到的一些有用的命令：

  * `xp/ N x paddr`
    
    显示从物理地址 paddr 处开始的 N 个词的十六进制转储。如果 N 省略，默认为 1。这是 GDB 的 `x` 命令模拟的物理内存。
  * `info registers`
    
    显示机器内部寄存器状态的一个完整转储。实践中，对于段选择器，这将包含机器的 _隐藏_ 段状态和局部、全局、和中断描述符表加任务状态寄存器。隐藏状态是在加载段选择器后，虚拟的 CPU 从 GDT/LDT 中读取的信息。下面是实验 1 中 JOS 内核处于运行中时的 CS 信息和每个字段的含义：
    
    ```c
    CS =0008 10000000 ffffffff 10cf9a00 DPL=0 CS32 [-R-]
    ```

  * `CS =0008`

    代码选择器可见部分。我们使用段 0x8。这也告诉我们参考全局描述符表（0x8&4=0），并且我们的 CPL（当前权限级别）是 0x8&3=0。
  * `10000000`
    
    这是段基址。线性地址 = 逻辑地址 + 0x10000000。
  * `ffffffff`
    
    这是段限制。访问线性地址 0xffffffff 以上将返回段违规异常。
  * `10cf9a00`
    
    段的原始标志，QEMU 将在接下来的几个字段中解码这些对我们有用的标志。
  * `DPL=0`
    
    段的权限级别。一旦代码以权限 0 运行，它将就能够加载这个段。
  * `CS32`
    
    这是一个 32 位代码段。对于数据段（不要与 DS 寄存器混淆了），另外的值还包括 `DS`，而对于本地描述符表是  `LDT`。
  * `[-R-]`
    
    这个段是只读的。

  * `info mem`
    
    （在实验 2 以后）显示映射的虚拟内存和权限。比如：

    ```
    ef7c0000-ef800000 00040000 urw
    efbf8000-efc00000 00008000 -rw
    ```

    这告诉我们从 0xef7c0000 到 0xef800000 的 0x00040000 字节的内存被映射为读取/写入/用户可访问，而映射在 0xefbf8000 到 0xefc00000 之间的内存权限是读取/写入，但是仅限于内核可访问。

  * `info pg`
    
    （在实验 2 以后）显示当前页表结构。它的输出类似于 `info mem`，但与页目录条目和页表条目是有区别的，并且为每个条目给了单独的权限。重复的 PTE 和整个页表被折叠为一个单行。例如：

    ```
VPN range     Entry         Flags        Physical page
[00000-003ff]  PDE[000]     -------UWP
  [00200-00233]  PTE[200-233] -------U-P 00380 0037e 0037d 0037c 0037b 0037a ..
[00800-00bff]  PDE[002]     ----A--UWP
  [00800-00801]  PTE[000-001] ----A--U-P 0034b 00349
  [00802-00802]  PTE[002]     -------U-P 00348
    ```

    这里各自显示了两个页目录条目、虚拟地址范围 0x00000000 到 0x003fffff 以及 0x00800000 到 0x00bfffff。 所有的 PDE 都存在于内存中、可写入、并且用户可访问，而第二个 PDE 也是可访问的。这些页表中的第二个映射了三个页、虚拟地址范围 0x00800000 到 0x00802fff，其中前两个页是存在于内存中的、可写入、并且用户可访问的，而第三个仅存在于内存中，并且用户可访问。这些 PTE 的第一个条目映射在物理页 0x34b 处。

QEMU 也有一些非常有用的命令行参数，使用 `QEMUEXTRA` 变量可以将参数传递给 JOS 的 makefile。

  * `make QEMUEXTRA='-d int' ...`
    
    记录所有的中断和一个完整的寄存器转储到 `qemu.log` 文件中。你可以忽略前两个日志条目、“SMM: enter” 和 “SMM: after RMS”，因为这些是在进入引导加载器之前生成的。在这之后的日志条目看起来像下面这样：
    
    ```
     4: v=30 e=0000 i=1 cpl=3 IP=001b:00800e2e pc=00800e2e SP=0023:eebfdf28 EAX=00000005
EAX=00000005 EBX=00001002 ECX=00200000 EDX=00000000
ESI=00000805 EDI=00200000 EBP=eebfdf60 ESP=eebfdf28
...
```

    第一行描述了中断。`4:` 只是一个日志记录计数器。`v` 提供了十六进程的向量号。`e` 提供了错误代码。`i=1` 表示它是由一个 `int` 指令（相对一个硬件产生的中断而言）产生的。剩下的行的意思很明显。对于一个寄存器转储而言，接下来看到的就是寄存器信息。

    注意：如果你运行的是一个 0.15 版本之前的 QEMU，日志将写入到 `/tmp` 目录，而不是当前目录下。



--------------------------------------------------------------------------------

via: https://pdos.csail.mit.edu/6.828/2018/labguide.html

作者：[csail.mit][a]
选题：[lujun9972][b]
译者：[qhwdw](https://github.com/qhwdw)
校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出

[a]: https://pdos.csail.mit.edu
[b]: https://github.com/lujun9972
[1]: http://sourceware.org/gdb/current/onlinedocs/gdb/
[2]: http://wiki.qemu.org/download/qemu-doc.html#pcsys_005fmonitor
