1. 实验内容
lab1 中包含一个 bootloader 和一个 OS。这个 bootloader 可以切换到 X86 保护模式,能够读磁盘并加载 ELF 执行文件格式,并显示字符。而这 lab1 中的 OS 只是一个可以处理时钟中断和显示字符的幼儿园级别 OS。
一:练习
为了实现 lab1 的目标,lab1 提供了 6 个基本练习和 1 个扩展练习,要求完成实验报告。
对实验报告的要求:
- 基于 markdown 格式来完成,以文本方式为主。
- 填写各个基本练习中要求完成的报告内容
- 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点)
- 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点
1. lab1练习1
练习 1:理解通过 make 生成执行文件的过程。(要求在报告中写出对下述问题的回答)
列出本实验各练习中对应的 OS 原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。
在此练习中,大家需要通过静态分析代码来了解:
- 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
补充材料:
如何调试 Makefile
当执行 make 时,一般只会显示输出,不会显示 make 到底执行了哪些命令。
如想了解 make 执行了哪些命令,可以执行:
$ make "V="
要获取更多有关 make 的信息,可上网查询,并请执行
$ man make
2. lab1练习2
练习 2:使用 qemu 执行并调试 lab1 中的软件。(要求在报告中简要写出练习过程)
为了熟悉使用 qemu 和 gdb 进行的调试工作,我们进行如下的小练习:
- 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
- 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
- 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S 和 bootblock.asm 进行比较。
- 自己找一个 bootloader 或内核中的代码位置,设置断点并进行测试。
提示:参考附录“启动后第一条执行的指令”,可了解更详细的解释,以及如何单步调试和查看 BIOS 代码。提示:查看 labcodes_answer/lab1_result/tools/lab1init 文件,用如下命令试试如何调试 bootloader 第一条指令:
$ cd labcodes_answer/lab1_result/ $ make lab1-mon
补充材料: 我们主要通过硬件模拟器 qemu 来进行各种实验。在实验的过程中我们可能会遇上各种各样的问题,调试是必要的。qemu 支持使用 gdb 进行的强大而方便的调试。所以用好 qemu 和 gdb 是完成各种实验的基本要素。
默认的 gdb 需要进行一些额外的配置才进行 qemu 的调试任务。qemu 和 gdb 之间使用网络端口 1234 进行通讯。在打开 qemu 进行模拟之后,执行 gdb 并输入
target remote localhost:1234
即可连接 qemu,此时 qemu 会进入停止状态,听从 gdb 的命令。
另外,我们可能需要 qemu 在一开始便进入等待模式,则我们不再使用 make qemu 开始系统的运行,而使用 make debug 来完成这项工作。这样 qemu 便不会在 gdb 尚未连接的时候擅自运行了。
gdb 的地址断点
在 gdb 命令行中,使用 b *[地址]便可以在指定内存地址设置断点,当 qemu 中的 cpu 执行到指定地址时,便会将控制权交给 gdb。
关于代码的反汇编
有可能 gdb 无法正确获取当前 qemu 执行的汇编指令,通过如下配置可以在每次 gdb 命令行前强制反汇编当前的指令,在 gdb 命令行或配置文件中添加:
define hook-stop x/i $pc end
即可
gdb 的单步命令
在 gdb 中,有 next, nexti, step, stepi 等指令来单步调试程序,他们功能各不相同,区别在于单步的“跨度”上。
next 单步到程序源代码的下一行,不进入函数。 nexti 单步一条机器指令,不进入函数。 step 单步到下一个不同的源代码行(包括进入函数)。 stepi 单步一条机器指令。
3. lab1练习3
练习 3:分析 bootloader 进入保护模式的过程。(要求在报告中写出分析)
BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。请分析 bootloader 是如何完成从实模式进入保护模式的。
提示:需要阅读小节“保护模式和分段机制”和 lab1/boot/bootasm.S 源码,了解如何从实模式切换到保护模式,需要了解:
- 为何开启 A20,以及如何开启 A20
- 如何初始化 GDT 表
- 如何使能和进入保护模式
4. lab1练习4
练习 4:分析 bootloader 加载 ELF 格式的 OS 的过程。(要求在报告中写出分析)
通过阅读 bootmain.c,了解 bootloader 如何加载 ELF 文件。通过分析源代码和通过 qemu 来运行并调试 bootloader&OS,
- bootloader 如何读取硬盘扇区的?
- bootloader 是如何加载 ELF 格式的 OS?
提示:可阅读“硬盘访问概述”,“ELF 执行文件格式概述”这两小节。
5. lab1练习5
练习 5:实现函数调用堆栈跟踪函数 (需要编程)
我们需要在 lab1 中完成 kdebug.c 中函数 print_stackframe 的实现,可以通过函数 print_stackframe 来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在 lab1 中执行 “make qemu”后,在 qemu 模拟器中得到类似如下的输出:
…… ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096 kern/debug/kdebug.c:305: print_stackframe+22 ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8 kern/debug/kmonitor.c:125: mon_backtrace+10 ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84 kern/init/init.c:48: grade_backtrace2+33 ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029 kern/init/init.c:53: grade_backtrace1+38 ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d kern/init/init.c:58: grade_backtrace0+23 ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000 kern/init/init.c:63: grade_backtrace+34 ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53 kern/init/init.c:28: kern_init+88 ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 <unknow>: -- 0x00007d72 – ……
请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。
提示:可阅读小节“函数堆栈”,了解编译器如何建立函数调用关系的。在完成 lab1 编译后,查看 lab1/obj/bootblock.asm,了解 bootloader 源码与机器码的语句和地址等的对应关系;查看 lab1/obj/kernel.asm,了解 ucore OS 源码与机器码的语句和地址等的对应关系。
要求完成函数 kern/debug/kdebug.c::print_stackframe 的实现,提交改进后源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对上述问题的回答。
补充材料:
由于显示完整的栈结构需要解析内核文件中的调试符号,较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用 print_debuginfo 函数完成查找对应函数名并打印至屏幕的功能。具体可以参见 kdebug.c 代码中的注释。
6. lab1练习6
练习 6:完善中断初始化和处理 (需要编程)
请完成编码工作和回答如下问题:
- 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
- 请编程完善 kern/trap/trap.c 中对中断向量表进行初始化的函数 idt_init。在 idt_init 函数中,依次对所有中断入口进行初始化。使用 mmu.h 中的 SETGATE 宏,填充 idt 数组内容。每个中断的入口由 tools/vectors.c 生成,使用 trap.c 中声明的 vectors 数组即可。
- 请编程完善 trap.c 中的中断处理函数 trap,在对时钟中断进行处理的部分填写 trap 函数中处理时钟中断的部分,使操作系统每遇到 100 次时钟中断后,调用 print_ticks 子程序,向屏幕上打印一行文字”100 ticks”。
【注意】除了系统调用中断(T_SYSCALL)使用陷阱门描述符且权限为用户态权限以外,其它中断均使用特权级(DPL)为0的中断门描述符,权限为内核态权限;而 ucore 的应用程序处于特权级3,需要采用` int 0x80`指令操作(这种方式称为软中断,软件中断,Tra 中断,在 lab5 会碰到)来发出系统调用请求,并要能实现从特权级3到特权级0的转换,所以系统调用中断(T_SYSCALL)所对应的中断门描述符中的特权级(DPL)需要设置为3。
要求完成问题 2 和问题 3 提出的相关函数实现,提交改进后的源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对问题 1 的回答。完成这问题 2 和 3 要求的部分代码后,运行整个系统,可以看到大约每 1 秒会输出一次”100 ticks”,而按下的键也会在屏幕上显示。
提示:可阅读小节“中断与异常”。
7. lab1扩展练习
扩展练习 Challenge 1(需要编程)
扩展 proj4,增加 syscall 功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务(通过网络查询所需信息,可找老师咨询。如果完成,且有兴趣做代替考试的实验,可找老师商量)。需写出详细的设计和分析报告。
提示: 规范一下 challenge 的流程。
kern_init 调用 switch_test,该函数如下:
static void switch_test(void) { print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态 cprintf("+++ switch to user mode +++\n"); switch_to_user(); // switch to user mode print_cur_status(); cprintf("+++ switch to kernel mode +++\n"); switch_to_kernel(); // switch to kernel mode print_cur_status(); }
switchto* 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO* 中断,并设置好返回的状态。
在 lab1 里面完成代码以后,执行 make grade 应该能够评测结果是否正确。
扩展练习 Challenge 2(需要编程)
用键盘实现用户模式内核模式切换。具体目标是:“键盘输入 3 时切换到用户模式,键盘输入 0 时切换到内核模式”。 基本思路是借鉴软中断(syscall 功能)的代码,并且把 trap.c 中软中断处理的设置语句拿过来。
注意:
1.关于调试工具,不建议用 lab1_print_cur_status()来显示,要注意到寄存器的值要在中断完成后 tranentry.S 里面 iret 结束的时候才写回,所以再 trap.c 里面不好观察,建议用 print_trapframe(tf)
2.关于内联汇编,最开始调试的时候,参数容易出现错误,可能的错误代码如下
asm volatile ( "sub $0x8, %%esp \n" "int %0 \n" "movl %%ebp, %%esp" : )
要去掉参数 int %0 \n 这一行
3.软中断是利用了临时栈来处理的,所以有压栈和出栈的汇编语句。硬件中断本身就在内核态了,直接处理就可以了。
- 参考答案在 mooc_os_lab 中的 mooc_os_2014 branch 中的 labcodes_answer/lab1_result 目录下
二:lab1项目组成
lab1 的整体目录结构如下所示:
. ├── boot │ ├── asm.h │ ├── bootasm.S │ └── bootmain.c ├── kern │ ├── debug │ │ ├── assert.h │ │ ├── kdebug.c │ │ ├── kdebug.h │ │ ├── kmonitor.c │ │ ├── kmonitor.h │ │ ├── panic.c │ │ └── stab.h │ ├── driver │ │ ├── clock.c │ │ ├── clock.h │ │ ├── console.c │ │ ├── console.h │ │ ├── intr.c │ │ ├── intr.h │ │ ├── kbdreg.h │ │ ├── picirq.c │ │ └── picirq.h │ ├── init │ │ └── init.c │ ├── libs │ │ ├── readline.c │ │ └── stdio.c │ ├── mm │ │ ├── memlayout.h │ │ ├── mmu.h │ │ ├── pmm.c │ │ └── pmm.h │ └── trap │ ├── trap.c │ ├── trapentry.S │ ├── trap.h │ └── vectors.S ├── libs │ ├── defs.h │ ├── elf.h │ ├── error.h │ ├── printfmt.c │ ├── stdarg.h │ ├── stdio.h │ ├── string.c │ ├── string.h │ └── x86.h ├── Makefile └── tools ├── function.mk ├── gdbinit ├── grade.sh ├── kernel.ld ├── sign.c └── vector.c 10 directories, 48 files
其中一些比较重要的文件说明如下:
bootloader 部分
- boot/bootasm.S :定义并实现了 bootloader 最先执行的函数 start,此函数进行了一定的初始化,完成了从实模式到保护模式的转换,并调用 bootmain.c 中的 bootmain 函数。
- boot/bootmain.c:定义并实现了 bootmain 函数实现了通过屏幕、串口和并口显示字符串。bootmain 函数加载 ucore 操作系统到内存,然后跳转到 ucore 的入口处执行。
- boot/asm.h:是 bootasm.S 汇编文件所需要的头文件,主要是一些与 X86 保护模式的段访问方式相关的宏定义。
ucore 操作系统部分
系统初始化部分:
- kern/init/init.c:ucore 操作系统的初始化启动代码
内存管理部分:
- kern/mm/memlayout.h:ucore 操作系统有关段管理(段描述符编号、段号等)的一些宏定义
- kern/mm/mmu.h:ucore 操作系统有关 X86 MMU 等硬件相关的定义,包括 EFLAGS 寄存器中各位的含义,应用/系统段类型,中断门描述符定义,段描述符定义,任务状态段定义,NULL 段声明的宏 SEG_NULL, 特定段声明的宏 SEG,设置中 断门描述符的宏 SETGATE(在练习 6 中会用到)
- kern/mm/pmm.[ch]:设定了 ucore 操作系统在段机制中要用到的全局变量:任务状态段 ts,全局描述符表 gdt[],加载全局描述符表寄存器的函数 lgdt,临时的内核栈 stack0;以及对全局描述符表和任务状态段的初始化函数 gdt_init
外设驱动部分:
- kern/driver/intr.[ch]:实现了通过设置 CPU 的 eflags 来屏蔽和使能中断的函数;
- kern/driver/picirq.[ch]:实现了对中断控制器 8259A 的初始化和使能操作;
- kern/driver/clock.[ch]:实现了对时钟控制器 8253 的初始化操作;- kern/driver/console.[ch]:实现了对串口和键盘的中断方式的处理操作;
中断处理部分:
- kern/trap/vectors.S:包括 256 个中断服务例程的入口地址和第一步初步处理实现。注意,此文件是由 tools/vector.c 在编译 ucore 期间动态生成的;
- kern/trap/trapentry.S:紧接着第一步初步处理后,进一步完成第二步初步处理;并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作;
- kern/trap/trap.[ch]:紧接着第二步初步处理后,继续完成具体的各种中断处理操作;
内核调试部分:
- kern/debug/kdebug.[ch]:提供源码和二进制对应关系的查询功能,用于显示调用栈关系。其中补全 print_stackframe 函数是需要完成的练习。其他实现部分不必深究。
- kern/debug/kmonitor.[ch]:实现提供动态分析命令的 kernel monitor,便于在 ucore 出现 bug 或问题后,能够进入 kernel monitor 中,查看当前调用关系。实现部分不必深究。
- kern/debug/panic.c | assert.h:提供了 panic 函数和 assert 宏,便于在发现错误后,调用 kernel monitor。大家可在编程实验中充分利用 assert 宏和 panic 函数,提高查找错误的效率。
公共库部分
- libs/defs.h:包含一些无符号整型的缩写定义。
- Libs/x86.h:一些用 GNU C 嵌入式汇编实现的 C 函数(由于使用了 inline 关键字,所以可以理解为宏)。
工具部分
- Makefile 和 function.mk:指导 make 完成整个软件项目的编译,清除等工作。
- sign.c:一个 C 语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区。
- tools/vector.c:生成 vectors.S,此文件包含了中断向量处理的统一实现。
编译方法
首先下载 lab1.tar.bz2,然后解压 lab1.tar.bz2。在 lab1 目录下执行 make,可以生成 ucore.img(生成于 bin 目录下)。ucore.img 是一个包含了 bootloader 或 OS 的硬盘镜像,通过执行如下命令可在硬件虚拟环境 qemu 中运行 bootloader 或 OS:
$ make qemu
则可以得到如下显示界面(仅供参考)
(THU.CST) os is loading ... Special kernel symbols: entry 0x00100000 (phys) etext 0x00103468 (phys) edata 0x0010ea18 (phys) end 0x0010fd80 (phys) Kernel executable memory footprint: 64KB ebp:0x00007b38 eip:0x00100a55 args:0x00010094 0x00010094 0x00007b68 0x00100084 kern/debug/kdebug.c:305: print_stackframe+21 ebp:0x00007b48 eip:0x00100d3a args:0x00000000 0x00000000 0x00000000 0x00007bb8 kern/debug/kmonitor.c:125: mon_backtrace+10 ebp:0x00007b68 eip:0x00100084 args:0x00000000 0x00007b90 0xffff0000 0x00007b94 kern/init/init.c:48: grade_backtrace2+19 ebp:0x00007b88 eip:0x001000a5 args:0x00000000 0xffff0000 0x00007bb4 0x00000029 kern/init/init.c:53: grade_backtrace1+27 ebp:0x00007ba8 eip:0x001000c1 args:0x00000000 0x00100000 0xffff0000 0x00100043 kern/init/init.c:58: grade_backtrace0+19 ebp:0x00007bc8 eip:0x001000e1 args:0x00000000 0x00000000 0x00000000 0x00103480 kern/init/init.c:63: grade_backtrace+26 ebp:0x00007be8 eip:0x00100050 args:0x00000000 0x00000000 0x00000000 0x00007c4f kern/init/init.c:28: kern_init+79 ebp:0x00007bf8 eip:0x00007d61 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 <unknow>: -- 0x00007d60 -- ++ setup timer interrupts 0: @ring 0 0: cs = 8 0: ds = 10 0: es = 10 0: ss = 10 +++ switch to user mode +++ 1: @ring 3 1: cs = 1b 1: ds = 23 1: es = 23 1: ss = 23 +++ switch to kernel mode +++ 2: @ring 0 2: cs = 8 2: ds = 10 2: es = 10 2: ss = 10 100 ticks 100 ticks 100 ticks 100 ticks