Merge remote-tracking branch 'LCTT/master'

This commit is contained in:
Xingyu.Wang 2018-11-25 13:17:36 +08:00
commit 00a659ea59
3 changed files with 236 additions and 214 deletions

View File

@ -0,0 +1,222 @@
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。这个命令将关注的线程切换到 nn 是从 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

View File

@ -1,27 +1,27 @@
让系统崩溃的黑天鹅分类
======
在严重的故障发生之前,找到引起问题的异常事件,并修复它。
> 在严重的故障发生之前,找到引起问题的异常事件,并修复它。
![](https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/black-swan-pair_0.png?itok=MkshwqVg)
黑天鹅用来比喻造成严重影响的小概率事件(比如 2008 年的金融危机)。在生产环境的系统中,黑天鹅是指这样的事情:它引发了你不知道的问题,造成了重大影响,不能快速修复或回滚,也不能用值班说明书上的其他标准响应来解决。它是事发几年后你还在给新人说起的事件。
<ruby>黑天鹅<rt>Black swan</rt></ruby>用来比喻造成严重影响的小概率事件(比如 2008 年的金融危机)。在生产环境的系统中,黑天鹅是指这样的事情:它引发了你不知道的问题,造成了重大影响,不能快速修复或回滚,也不能用值班说明书上的其他标准响应来解决。它是事发几年后你还在给新人说起的事件。
从定义上看,黑天鹅是不可预测的,不过有时候我们能找到其中的一些模式,针对有关联的某一类问题准备防御措施。
例如,大部分故障的直接原因是变更(代码、环境或配置)。虽然这种方式触发的 bug 是独特的不可预测的,但是常见的金丝雀发布对避免这类问题有一定的作用,而且自动回滚已经成了一种标准止损策略。
例如,大部分故障的直接原因是变更(代码、环境或配置)。虽然这种方式触发的 bug 是独特的不可预测的,但是常见的金丝雀发布对避免这类问题有一定的作用,而且自动回滚已经成了一种标准止损策略。
随着我们的专业性不断成熟,一些其他的问题也正逐渐变得容易理解,被归类到某种风险并有普适的预防策略。
### 公布出来的黑天鹅事件
所有科技公司都有生产环境的故障,只不过并不是所有公司都会分享他们的事故分析。那些公开讨论事故的公司帮了我们的忙。下列事故都描述了某一类问题,但它们绝对不是只属于一个类别。我们的系统中都有黑天鹅在潜伏着,只是有些人还不知道而已。
所有科技公司都有生产环境的故障,只不过并不是所有公司都会分享他们的事故分析。那些公开讨论事故的公司帮了我们的忙。下列事故都描述了某一类问题,但它们绝对不是只一个孤例。我们的系统中都有黑天鹅在潜伏着,只是有些人还不知道而已。
#### 达到上限
达到任何类型的限制都会引发严重事故。这类问题的一个典型例子是 2017 年 2 月 [Instapaper 的一次服务中断][1]。我把这份事故报告给任何一个运维工作者看他们读完都会脊背发凉。Instapaper 生产环境的数据库所在的文件系统有 2 TB 的大小限制,但是数据库服务团队并不知情。在没有任何报错的情况下,数据库不再接受任何写入了。完全恢复需要好几天,而且还得迁移数据库。
资源限制有各式各样的触发场景。Sentry 遇到了 [Postgres 的最大事务 ID 限制][2]。Platform.sh 遇到了[管道缓冲区大小限制][3]。SparkPost [触发了 AWS 的 DDos 保护][4]。Foursquare 在他们的一个 [MongoDB 耗尽内存][5]时遭遇了性能骤降。
资源限制有各式各样的触发场景。Sentry 遇到了 [Postgres 的最大事务 ID 限制][2]。Platform.sh 遇到了[管道缓冲区大小限制][3]。SparkPost [触发了 AWS 的 DDoS 保护][4]。Foursquare 在他们的一个 [MongoDB 耗尽内存][5]时遭遇了性能骤降。
提前了解系统限制的一个办法是定期做测试。好的压力测试(在生产环境的副本上做)应该包含写入事务,并且应该把每一种数据存储都写到超过当前生产环境的容量。压力测试时很容易忽略的是次要存储(比如 Zookeeper。如果你是在测试时遇到了资源限制那么你还有时间去解决问题。鉴于这种资源限制问题的解决方案可能涉及重大的变更比如数据存储拆分所以时间是非常宝贵的。
@ -32,7 +32,7 @@
#### 扩散的慢请求
> “这个世界的关联性远比我们想象中更大。所以我们看到了更多 Nassim Taleb 所说的‘黑天鹅事件’ —— 即罕见事件以更高的频率离谱地发生了,因为世界是相互关联的”
> — [Richard Thaler][6]
> — [Richard Thaler][6]
HostedGraphite 的负载均衡器并没有托管在 AWS 上,却[被 AWS 的服务中断给搞垮了][7],他们关于这次事故原因的分析报告很好地诠释了分布式计算系统之间存在多么大的关联。在这个事件里,负载均衡器的连接池被来自 AWS 上的客户访问占满了,因为这些连接很耗时。同样的现象还会发生在应用的线程、锁、数据库连接上 —— 任何能被慢操作占满的资源。
@ -40,7 +40,7 @@ HostedGraphite 的负载均衡器并没有托管在 AWS 上,却[被 AWS 的服
重试的间隔应该用指数退避来限制一下并加入一些时间抖动。Square 有一次服务中断是 [Redis 存储的过载][9],原因是有一段代码对失败的事务重试了 500 次,没有任何重试退避的方案,也说明了过度重试的潜在风险。另外,针对这种情况,[断路器][10]设计模式也是有用的。
应该设计出监控仪表盘来清晰地展示所有资源的[使用率饱和度和报错][11],这样才能快速发现问题。
应该设计出监控仪表盘来清晰地展示所有资源的[使用率饱和度和报错][11],这样才能快速发现问题。
#### 突发的高负载
@ -48,7 +48,7 @@ HostedGraphite 的负载均衡器并没有托管在 AWS 上,却[被 AWS 的服
在预定时刻同时发生的事件并不是突发大流量的唯一原因。Slack 经历过一次短时间内的[多次服务中断][12],原因是非常多的客户端断开连接后立即重连,造成了突发的大负载。 CircleCI 也经历过一次[严重的服务中断][13],当时 Gitlab 从故障中恢复了,所以数据库里积累了大量的构建任务队列,服务变得饱和而且缓慢。
几乎所有的服务都会受突发的高负载所影响。所以对这类可能出现的事情做应急预案——并测试一下预案能否正常工作——是必须的。客户端退避和[减载][14]通常是这些方案的核心。
几乎所有的服务都会受突发的高负载所影响。所以对这类可能出现的事情做应急预案 —— 并测试一下预案能否正常工作 —— 是必须的。客户端退避和[减载][14]通常是这些方案的核心。
如果你的系统必须不间断地接收数据,并且数据不能被丢掉,关键是用可伸缩的方式把数据缓冲到队列中,后续再处理。
@ -57,7 +57,7 @@ HostedGraphite 的负载均衡器并没有托管在 AWS 上,却[被 AWS 的服
> “复杂的系统本身就是有风险的系统”
> —— [Richard Cook, MD][15]
过去几年里软件的运维操作趋势是更加自动化。任何可能降低系统容量的自动化操作(比如擦除磁盘,退役设备,关闭服务)都应该谨慎操作。这类自动化操作的故障(由于系统有 bug 或者有不正确的调用)能很快地搞垮你的系统,而且可能很难恢复。
过去几年里软件的运维操作趋势是更加自动化。任何可能降低系统容量的自动化操作(比如擦除磁盘、退役设备、关闭服务)都应该谨慎操作。这类自动化操作的故障(由于系统有 bug 或者有不正确的调用)能很快地搞垮你的系统,而且可能很难恢复。
谷歌的 Christina Schulman 和 Etienne Perot 在[用安全规约协助保护你的数据中心][16]的演讲中给了一些例子。其中一次事故是将谷歌整个内部的内容分发网络CDN提交给了擦除磁盘的自动化系统。
@ -69,11 +69,11 @@ Schulman 和 Perot 建议使用一个中心服务来管理规约,限制破坏
### 防止黑天鹅事件
可能在等着击垮系统的黑天鹅可不止上面这些。有很多其他的严重问题是能通过一些技术来避免的,像金丝雀发布,压力测试,混沌工程,灾难测试和模糊测试——当然还有冗余性和弹性的设计。但是即使用了这些技术,有时候你的系统还是会有故障。
可能在等着击垮系统的黑天鹅可不止上面这些。有很多其他的严重问题是能通过一些技术来避免的,像金丝雀发布、压力测试、混沌工程、灾难测试和模糊测试 —— 当然还有冗余性和弹性的设计。但是即使用了这些技术,有时候你的系统还是会有故障。
为了确保你的组织能有效地响应,在服务中断期间,请保证关键技术人员和领导层有办法沟通协调。例如,有一种你可能需要处理的烦人的事情,那就是网络完全中断。拥有故障时仍然可用的通信通道非常重要,这个通信通道要完全独立于你们自己的基础设施和基础设施的依赖。举个例子,假如你使用 AWS那么把故障时可用的通信服务部署在 AWS 上就不明智了。在和你的主系统无关的地方,运行电话网桥或 IRC 服务器是比较好的方案。确保每个人都知道这个通信平台,并练习使用它。
为了确保你的组织能有效地响应,在服务中断期间,请保证关键技术人员和领导层有办法沟通协调。例如,有一种你可能需要处理的烦人的事情,那就是网络完全中断。拥有故障时仍然可用的通信通道非常重要,这个通信通道要完全独立于你们自己的基础设施及对其的依赖。举个例子,假如你使用 AWS那么把故障时可用的通信服务部署在 AWS 上就不明智了。在和你的主系统无关的地方,运行电话网桥或 IRC 服务器是比较好的方案。确保每个人都知道这个通信平台,并练习使用它。
另一个原则是,确保监控和运维工具对生产环境系统的依赖尽可能的少。将控制平面和数据平面分开,你才能在系统不健康的时候做变更。不要让数据处理和配置变更或监控使用同一个消息队列,比如——应该使用不同的消息队列实例。在 [SparkPost: DNS 挂掉的那一天][4] 这个演讲中Jeremy Blosser 讲了一个这类例子,很关键的工具依赖了生产环境的 DNS 配置,但是生产环境的 DNS 出了问题。
另一个原则是,确保监控和运维工具对生产环境系统的依赖尽可能的少。将控制平面和数据平面分开,你才能在系统不健康的时候做变更。不要让数据处理和配置变更或监控使用同一个消息队列,比如应该使用不同的消息队列实例。在 [SparkPost: DNS 挂掉的那一天][4] 这个演讲中Jeremy Blosser 讲了一个这类例子,很关键的工具依赖了生产环境的 DNS 配置,但是生产环境的 DNS 出了问题。
### 对抗黑天鹅的心理学
@ -83,7 +83,7 @@ Schulman 和 Perot 建议使用一个中心服务来管理规约,限制破坏
### 了解更多
关于黑天鹅或者以前的黑天鹅事件以及应对策略还有很多其他的事情可以说。如果你想了解更多我强烈推荐你去看这两本书它们是关于生产环境中的弹性和稳定性的Susan Fowler 写的[生产微服务][19],还有 Michael T. Nygard 的 [Release It!][20]。
关于黑天鹅或者以前的黑天鹅事件以及应对策略还有很多其他的事情可以说。如果你想了解更多我强烈推荐你去看这两本书它们是关于生产环境中的弹性和稳定性的Susan Fowler 写的[生产微服务][19],还有 Michael T. Nygard 的 [Release It!][20]
--------------------------------------------------------------------------------
@ -92,7 +92,7 @@ via: https://opensource.com/article/18/10/taxonomy-black-swans
作者:[Laura Nolan][a]
选题:[lujun9972][b]
译者:[BeliteX](https://github.com/belitex)
校对:[校对者ID](https://github.com/校对者ID)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出

View File

@ -1,200 +0,0 @@
6.828 实验工具指南
======
### 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。这个命令将关注的线程切换到 nn 是从 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)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [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