本文为 CS 162 Project Pregame(proj-pregame)官方指南的中文译本。
原文:https://cs162.org/static/proj/proj-pregame/
归档参考:https://web.archive.org/web/20251215163710/https://cs162.org/static/
- 代码:各 project 共用仓库根目录下的一份
../src/,本目录只有指南。 - 文档译本:
../pintos-docs/ - 构建 / 测试本作业(在仓库根目录下):
cd src/userprog
make
make check
# 或:pintos-test do-nothing
下文路径已按本仓库改写(官方原文里的 proj-pregame/src/...、~/code/personal/... 均对应这里的 src/...)。书面题原要求交 Gradescope;自学可自行记录答案。
来源:https://cs162.org/static/proj/proj-pregame/
CS 162 的各个 project 将使用 Pintos——一个教学用操作系统。它们旨在让你在开发真实、可运行的 kernel 的情境中,获得对操作系统核心思想的实践经验,同时又不至于过于复杂。Pintos 的 skeleton code 在其 file system、thread scheduler 以及对 user programs 的支持上存在若干局限。在这些 project 的过程中,你将在上述各个方面大幅改进 Pintos。
本仓库已包含 Pintos 源码(src/),无需再从 staff repository 拉取。
本作业的具体内容见下方 Tasks。不过,你可能会发现先通读部分 Pintos documentation 中文译本 会有帮助。这通常有助于你理解所需完成的任务。
我们要求通读 pintos-docs 的以下章节:
在本次练习中,你将进一步了解 Pintos 的一些基础内容(例如 debugging),并有机会修复 Pintos 中的一个 bug。与后续 project 不同,本次为个人作业,所有工作(包括 code 与书面回答)必须由你独立完成。 本 project 旨在让你感受 Pintos 的结构,并学会使用一些通用的 debugging 工具。
来源:https://cs162.org/static/proj/proj-pregame/docs/tasks/
为了给后续 Pintos project 设计出好的解决方案,你首先需要对现有 codebase 有充分理解。本次练习的目标,是帮助你开始熟悉 Pintos 的代码。
来源:https://cs162.org/static/proj/proj-pregame/docs/tasks/documentation/
我们要求通读 pintos-docs 的以下章节:
官方课程会在后续的 design documents 与 oral exams 中就这些文档章节提问。自学时也建议能回答诸如 “What address is the top of a user programs stack?” 这类问题。
来源:https://cs162.org/static/proj/proj-pregame/docs/tasks/faulting_instruction/
首先,在 src/userprog 目录下运行 make 和 make check,并观察当前没有任何测试通过。
在本次作业中,我们将在 GDB 中逐步跟踪 do-nothing 测试的执行,以学习如何修改 Pintos 使该测试通过,并理解 Pintos 现有对 user programs 的支持是如何实现的。
do-nothing 是检验 Pintos user program 支持的最简单测试。请查看 src/tests/userprog/do-nothing.c;它是一个什么都不做的 Pintos user application。main 函数中唯一的 return 162 语句会向操作系统返回 exit code 162。exit code 的具体数值对本测试并不重要;我们选择了一个非 0 的值,以便更容易通过 GDB 跟踪 Pintos kernel 如何处理该值(注意 162 = 0xa2)。
当你运行 make 时,do-nothing.c 被编译成可执行程序 do-nothing,你可以在 src/userprog/build/tests/userprog/do-nothing 找到它。do-nothing 测试只不过是在 Pintos 中运行该 do-nothing 可执行文件。
查看文件 src/userprog/build/tests/userprog/do-nothing.result。(或者,你也可以在 src/userprog 下运行 pintos-test do-nothing。)该文件展示了 Pintos testing framework 在运行 do-nothing 测试时的输出。testing framework 期望 Pintos 输出 do-nothing: exit(162)。这是 Pintos 在 process 退出时打印的标准消息(你将在 Project Userprog 中再次遇到它)。然而,如 diff 所示,Pintos 并未输出这条消息;相反,do-nothing 程序因 memory access violation(segmentation fault)在 userspace 中崩溃了。
Hint: 如果查找某样东西时卡住了,试试 find 或 grep!例如,若要按文件名查找文件,可从 src 运行 find -name <file_name>;若要在任意文件中搜索字符串匹配,可从 src 运行 grep -r <string> .。
现在我们将引导你调试该测试。根据 do-nothing 测试的输出,请回答下列问题(自学可记在自己的笔记里;课上原交 Gradescope):
- 该程序从 userspace 试图访问哪个 virtual address,从而导致崩溃?为什么此时不允许程序访问该 memory address?(请具体说明,并提到 Pintos codebase 中的特定 macros。)
- 导致崩溃的那条 instruction 的 virtual address 是什么?
- 为了调查,使用
i386-objdump反汇编do-nothingbinary。程序崩溃时所在的 function 名称是什么?将该 function 的反汇编代码抄下来,并指出程序崩溃时所在的那条 instruction。 - 找到你在上面识别出的那个 function 的 C 代码(Hint: 它在 userspace 中执行,因此要么在
do-nothing.c中,要么在src/lib或src/lib/user的某个文件中),并将其抄下来。 - 对于 #3 中反汇编 function 里的每条 instruction,试着用几句话向自己说明它为何必要和/或它在试图做什么。Hint: 阅读 80x86 calling convention(见 02-userprog.md)。你在 #3 中识别出的那条 instruction,为什么会试图访问你在 #1 中识别出的那个 virtual address 处的 memory?请给出高层解释,而不要仅仅提及 register 的值。
如果你已经做到这一步,干得好!你现在对测试在何处失败以及为何失败已有较好的认识。这将让我们能更高效地使用 GDB。
来源:https://cs162.org/static/proj/proj-pregame/docs/tasks/step_through/
既然我们已经理解了 do-nothing 程序为何崩溃,我们将使用 GDB,从 kernel 启动开始,逐步跟踪 Pintos 中 do-nothing 测试的执行。我们的目标是弄清如何修改 Pintos 的 user program loader,使 do-nothing 不再崩溃,同时熟悉 Pintos 如何支持 user programs。为此,将工作目录切换到 src/userprog/ 并运行
FORCE_SIMULATOR=--bochs PINTOS_DEBUG=1 pintos-test do-nothing
补充说明:你可以通过将其加入 bashrc,为其设置别名 pintos-debug。
echo "alias pintos-debug='FORCE_SIMULATOR=--bochs PINTOS_DEBUG=1 pintos-test'" >> ~/.bashrc
此时 GDB 应当已经打开。在高层上,Pintos 要启动 do-nothing process,必须先发生以下事情:
- BIOS 从磁盘的第一个 sector 读取 Pintos bootloader(
src/threads/loader.S),并将其载入地址0x7c00处的 memory。 - bootloader 从磁盘读取 kernel code 到地址
0x20000处的 memory,然后跳转到 kernel entrypoint(src/threads/start.S)。 - kernel entrypoint 处的代码切换到 32-bit protected mode,然后调用
main(src/threads/init.c)。 main函数通过初始化 scheduler、memory subsystem、interrupt vector、hardware devices 以及 file system 来启动 Pintos。
欢迎你阅读代码以进一步了解这一启动过程,但对于 Pintos projects 而言,你并不需要理解其工作原理。
在 run_task 处设置 breakpoint,并在 GDB 中 continue,以跳过上述 setup。正如你在 run_task 的代码中所见,Pintos 通过调用
process_wait(process_execute("do-nothing"));
来执行(在 Pintos command line 上指定的)do-nothing 程序,该调用位于 run_task 中。process_wait 与 process_execute 都在 src/userprog/process.c 中。
现在,回答下列问题:
- Step into
process_execute函数。正在运行该函数的 thread 的名称与地址是什么?此时 Pintos 中还存在哪些其他 threads?复制它们的struct thread。(Hint: 对于最后一部分,dumplist &all_list thread allelem可能会有用。) - 当前 thread 的 backtrace 是什么?将 GDB 中的 backtrace 复制为你的答案。
- 在
start_process处设置 breakpoint 并 continue 到该处。正在运行该函数的 thread 的名称与地址是什么?此时 Pintos 中还存在哪些其他 threads?复制它们的struct thread。 - 逐步跟踪
start_process函数,直到你已经 stepped over 对load的调用。注意load会设置if_structure 中的eip与esp字段。打印出if_structure 的值,并以十六进制显示(hint:print/x if_)。 asm volatile语句中的第一条 instruction 将 stack pointer 设置为if_structure 的底部。第二条跳转到intr_exit。代码中的注释解释了这里发生的事情。Step into 该asm volatile语句,然后逐步跟踪这些 instructions。当你 step throughiretinstruction 时,观察该函数“返回”到了 userspace。为什么 processor 在执行该函数时会切换模式?你可以从执行iret时 memory 和/或 registers 中的值,以及iretinstruction 的功能来解释。- 执行完
iret后,输入info registers以打印 registers 的内容。将此命令的输出抄下来。这些值与你打印if_时的值相比如何? - 注意,若你试图用
backtrace获取当前位置,将只会得到一个十六进制地址。这是因为 debugger 只加载了来自 kernel 的 symbols。现在我们已进入 userspace,必须加载我们正在运行的那个 Pintos 可执行文件——即do-nothing——的 symbols。为此,使用loadusersymbols tests/userprog/do-nothing。现在,使用backtrace,你会看到当前位于_start函数中。使用disassemble与stepi命令,在 userspace 中逐条 instruction 执行,直到发生 page fault。此时,processor 已立即进入 kernel mode 以处理 page fault,因此backtrace将显示 kernel mode 下的当前 stack,而不是 page fault 发生时的 user stack。不过,你可以使用btpagefault来找到 page fault 发生时的 user stack。将btpagefault的输出抄下来。
来源:https://cs162.org/static/proj/proj-pregame/docs/tasks/debug/
你在 GDB 中观察到的 faulting instruction 应当与你在 #3 中找到的相一致。既然你已经确定了 faulting instruction、理解了该 instruction 的用途,并走通了 kernel 如何初始化一个 user process,你现在可以修改 kernel,使 do-nothing 能够正确运行。
- (Coding test 1) 修改 Pintos kernel,使
do-nothing不再崩溃。你的改动应当在 Pintos kernel 中,而不是在 userspace 程序(do-nothing.c)或src/lib中的 libraries。这不应涉及对 Pintos 源代码的大幅修改。官方 staff solution 仅对process.c做了一行改动就解决了该问题。完成此改动后,do-nothing测试应当通过,但其他测试很可能会失败。Note: 即便你的改动看起来像 hack 也没关系。你将在 user programs project 中实现更好的修复。 - (Coding test 2) 既然你已理解如何修复
do-nothing,请查看do-not-much测试。该测试与do-nothing的不同之处在于:它返回argc,而不是简单地返回162。目前你可以假定do-not-much不接收 command-line arguments。(这与argc有何关系?)实现一处能让do-not-much通过的改动。同样,该解决方案可以是 hack。Hint: 回顾你在 Find the faulting instruction 第 5 题中的回答。 - (Coding test 3) 你的修复有可能对
stack-align-v测试也有效,但对do-nothing与do-not-much也存在并不适用于该测试的解决方案。请查看stack-align-v测试。它的行为与do-not-much类似,但应返回esp % 16的值。写下该程序应当返回什么(Hint: 可在src/tests/userprog/stack-align-v.ck中找到),以及为何如此。 - 如有必要,修改你的修复,使
do-nothing、do-not-much与stack-align-v三者全部通过。请记下你通过stack-align-v的解决方案,并明确引用 calling convention 的各个步骤以及相应的 offset 值来证明其正确性。可用pintos-test <name>逐个验证。 - 像之前一样,在
do-nothing上重新运行 GDB。执行loadusersymbols命令,在_start处设置 breakpoint,然后 continue,以直接跳到 userspace 执行的开始。使用disassemble与stepi命令,逐条 instruction 执行do-nothing程序,直到到达src/lib/user/syscall.c中的int $0x30instruction。此时,通过检查 memory 打印 stack 顶部的两个 words(Hint:x/2xw $esp),并复制输出。 int $0x30instruction 会切换到 kernel mode,并在该 process 的 kernel stack 上压入一个 interrupt stack frame。继续逐条 instruction 跟踪,直到到达syscall_handler。args[0]与args[1]的值分别是什么,它们与你对上一题的回答有何关系?
现在,你可以继续逐步跟踪 Pintos。在 do-nothing 运行完成后,由于我们在 kernel command line 上提供了 -q 选项,Pintos 将着手关机。若你好奇 Pintos 如何关机,可以在 GDB 中继续跟踪。
恭喜!你已经在 GDB 中走通了 Pintos 的启动、运行一个 user program 直至完成,以及关机的全过程。自学完成标准:上述书面题已自行作答,且 do-nothing、do-not-much、stack-align-v 三个测试通过。
来源:https://cs162.org/static/proj/proj-pregame/docs/deliverables/
- Written answers
- Code
- Documentation
上文 Tasks 各节中编号的书面问题(课上共约 17 题,原交 Gradescope)。自学请自行作答并保存笔记。
在 src/ 中做出必要的小修复之后,下列测试应全部通过:do-nothing、do-not-much、stack-align-v(在 src/userprog 下用 pintos-test 或 make check 验证相关结果)。
你应当已经通读文档中的必读章节;后续做 Userprog / Threads 时这些概念会反复用到。