Stanford 的 CS144 计网完成后让我们继续挑战一项更难的课程项目:UCB 操作系统 CS162 的 Pintos,这个也是多个 CS 顶校都在用的项目。老规矩讲课部分因为本科基本都学过就略过了。
继续安利 CS自学指南,和博主同届甚至就住在楼下的大佬的自学网站,汇总了很多国内外高校 CS 相关的高质量公开课。
项目内容为理解一个微型操作系统 Pintos 的原理并为其添加几方面的重要功能,有自动化测试样例。可以跟着北大操作系统实验班整理的文档做:PintosBook
我的实现(更新至Lab 2):Altair-Alpha/pintos
跟着实验手册的 Environment Setup 做即可,博主用的是 Docker 部署,未出现问题。开发环境 VSCode + 开两个 PoweShell 窗口一个运行一个调试就很舒服了。
成功启动 Pintos 即可。下面游戏正式开始。
- What is the first instruction that gets executed?
- At which physical address is this instruction located?
按照 Debugging 部分说明运行 GDB 绑定 Pintos 后得到以下输出,即机器启动运行的第一条指令:
[f000:fff0] 0xffff0: ljmp $0x3630,$0xf000e05b
该指令为 ljmp(长转移),位于 0xffff0,属于 BIOS 区内,是硬编码的第一条指令位置。
在 0x7c00 处设置断点并运行到该位置,此时控制权已由 BIOS 移交给 Bootloader,运行的指令与 loader.S 文件相对应。
- How does the bootloader read disk sectors? In particular, what BIOS interrupt is used?
第 55 行,指令为 call read_sector,调用的函数位于 230 行:

读取硬盘扇区需要借助 BIOS 提供的功能,具体来说就如题面所述是触发一个 BIOS 中断,该指令位于 242 行(图中红框),维基百科 BIOS interrupt call 条目下有完整的中断表可供查询:

结合 240 行对 AH = 0x42 的设置可知使用的是 Extended Read Sectors 功能。
- How does the bootloader decide whether it successfully finds the Pintos kernel?
继续向下看,目前我们已经读取了第一个磁盘扇区的内容,该扇区应该为主引导记录(Master Boot Record, MBR)扇区,包含了磁盘的分区信息,其特征是以 0x55AA 标志位结束(位于 0x01FE - 0x01FF,即 510,511 字节处)。67 行进行该检查,如果不相等,说明当前磁盘未正常分区,跳过并读取下一个磁盘。接下来跳转到 MBR 中第一个分区记录的位置(offset=446),如果读取结果为 0,说明当前分区未使用,使 si+=16 读取下一个分区记录,如果达到结束位置(510)仍未找到有效分区,则跳转至下一个磁盘。然后根据注释使用了值 0x20 来检查是否为 Pintos Kernel,这里我没有在文档中找到相应说明。最后,检查分区记录中第一个字节的值,该值如果为 0x80 则标识着当前分区是 Bootable 的。


至此,如果所有检查均通过,则可以确信已经找到了 Pintos Kernel。
- What happens when the bootloader could not find the Pintos kernel?
接续上一个问题的分析,如果所有分区、所有磁盘均尝试读取后都没有找到 Pintos Kernel 并执行 86 行跳转到 load_kernel 函数,则最终会落入:

输出 Not found 后,会触发另一个 BIOS 中断 0x18,该中断的作用即 Bootloader 向 BIOS 报告因未找到可引导磁盘导致启动失败。

- At what point and how exactly does the bootloader transfer control to the Pintos kernel?
找到 Pintos Kernel 后,Bootloader 从磁盘逐个读取扇区内容,并放在从 0x20000 开始的内存空间。Kernel 以 ELF 格式存储,如文档所述,Kernel 的入口位置非固定编码,而是被保存在其 ELF Header 中的一个指针。该指针位于 0x18,于是第 165 行指令读取该指针放在寄存器 dx 中,又在 166 行将其转存在一个内存位置 start 中(注释中解释了这样做的原因),最终在 168 行执行 ljmp 实际跳转。


至此,Bootloader 的使命完成,控制权移交给 Pintos Kernel 的入口。该入口就是 start.S 文件中的汇编代码。这部分代码完成了内存识别、页表建立、GDT 处理以及从 16-bit real mode 到 32-bit protected mode 的转换等工作,对应文档中 Core Guide / Loading / Low-Level Kernel Initialization 部分,这里就不再展开了。最终在 180 行执行 call pintos_init,该函数位于 init.c 中,进入 C 语言代码。
此部分主要练习 GDB 使用,为后续 Lab 打基础。追踪的目标为 init.c 中的一个函数 palloc_get_page() 和一个全局变量 uint32_t *init_page_dir。
- At the entry of pintos_init(), what is the value of the expression
init_page_dir[pd_no(ptov(0))]in hexadecimal format?
在 pintos_init 起始处设置断点,并计算该表达式,结果为 0。

- When palloc_get_page() is called for the first time,
- what does the call stack look like?
- what is the return value in hexadecimal format?
- what is the value of expression
init_page_dir[pd_no(ptov(0))]in hexadecimal format?
b 在 palloc_get_page 处设置断点,c 运行至该位置,然后使用 bt 查看调用栈。使用 finish/fin 运行至函数结束并查看返回值是一个 void * 指针。使用 p/x 打印表达式 16 进制计算结果,仍为 0。

- When palloc_get_page() is called for the third time,
- what does the call stack look like?
- what is the return value in hexadecimal format?
- what is the value of expression init_page_dir[pd_no(ptov(0))] in hexadecimal format?
继续 c 两次,重复上一步操作得到:

可以观察到,本次调用与第一次不同,不是在 paging_init 中而是在 thread_start 中被调用。返回的 void * 指针与第一次相差 0x2000。表达式的值为 0x102027。
在 Lab 0 的最后,我们可以上手在 Pintos 中写入一些自己的代码了。目前,如果在 Pintos 启动的命令行中没有参数,则启动后会自动结束。本节的任务是在这种情况下添加一个可交互的终端,要求如下:
Enhance threads/init.c to implement a tiny kernel monitor in Pintos.
Requirments:
- It starts with a prompt PKUOS> and waits for user input.
- As the user types in a printable character, display the character.
- When a newline is entered, it parses the input and checks if it is whoami. If it is whoami, print your student id.
- Afterward, the monitor will print the command prompt PKUOS> again in the next line and repeat.
- If the user input is exit, the monitor will quit to allow the kernel to finish. For the other input, print invalid command. Handling special input such as backspace is not required.
- If you implement such an enhancement, mention this in your design document.
这里首先要注意,我们是在 Kernel 层级编写代码,所以标准 C 库函数是 不能使用 的,不过课程为我们预先写好了一些与 C 同名的库函数,位于 lib 文件夹下,并且配置好了头文件搜索路径。
代码实现(包含 Backspace 键处理):
int
pintos_init (void)
{
...
if (*argv != NULL) {
/* Run actions specified on kernel command line. */
run_actions (argv);
} else {
size_t cmd_maxlen = 10;
char *buf = (char *)malloc(cmd_maxlen); // command line input buffer
while (true) {
printf("PKUOS>");
memset(buf, '\0', cmd_maxlen);
size_t index = 0;
while (1) {
char c = input_getc();
if (c == 13) { // newline
printf("\n");
break;
} else if (c == 127) { // backspace
if (index > 0) {
buf[--index] = '\0';
printf("\b \b");
}
continue;
}
if (index >= cmd_maxlen) {
continue;
}
buf[index++] = c;
if (c > 31) { // printable characters
printf("%c", c);
}
}
printf("cmd: %s\n", buf);
if (!strcmp(buf, "whoami")) {
printf("123456789\n");
} else if (!strcmp(buf, "exit")) {
break;
} else {
printf("invalid command\n");
}
}
free(buf);
printf("shell terminated.\n");
}
...
}
Lab 0 至此结束。