MIT 6.S081 lab4:traps | 陷入
lab4 - traps
前置准备
主要内容为探索陷阱处理机制。
根据课程官网的要求,需要阅读完教材的第四章\(Page \space
tables\)。并且读懂下列源代码:kernel/memlayout.h
,
kernel/vm.c
, kernel/kalloc.c
,
kernel/riscv.h
, user/exec.c
。
相关笔记参考: lecture 4 Notes
将pgtbl
分支所有内容都上传仓库后,执行下列命令来切换分支
1
2
3git fetch
git checkout traps
make clean
此外,新建一个traps_dev
分支来进行实际实验。
1 |
|
在traps_dev
中每通过一个作业的测试,提交(git
commit)你的代码,并将所做的修改合并(git merge)到util中,然后提交(git
push)到github。
1 |
|
RISC-V assembly
在本部分中将给出一段 RISC-V
汇编代码,通过阅读代码我们要回答几个问题,并把答案存储在主目录下的
answers-traps.txt
下。
运行 make fs.img
后会编译user/call.c
,
并生成user/call.asm
。我们需要观察call.asm
下的g
、f
、main
函数。
RISC-V 参考文档:RISC-V unprivileged instructions RISC-V privileged instructions
哪些寄存器包含函数的参数?例如,在
main
调用printf
时,哪个寄存器保存 \(13\)?
查阅Calling conventions
手册,可以发现 \(a_0 \rightarrow
a_7\)为函数参数和返回值寄存器。

13属于第三个参数(第一个为format
string,第二个为f(8) + 1
)。所以存储在\(a_2\)寄存器中。
在
main
的汇编代码中,函数f
的调用在哪里?对g
的调用在哪里? 提示:编译器可能会内联函数)。
我们先分析一下g()
的汇编代码。 1
2
3
4
5
6
7
8
9
10
11
12int g(int x) {
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
return x + 3;
}
8: 250d addiw a0,a0,3
a: 60a2 ld ra,8(sp)
c: 6402 ld s0,0(sp)
e: 0141 addi sp,sp,16
10: 8082 retsp
往下移动16字节,等价与要入栈两个元素。将ra
,即caller进程的pc
值,存入栈的第一个位置。将s0
,即caller进程的其他寄存器保存地址,存放到第二个位置。
然后将a0
的值加3,存储到a0
寄存器中。然后从栈中恢复ra
和s0
的地址,此时CPU能返回原进程继续执行。然后ret
指令将a0
复制给原进程,即返回值。
f()
函数和g()
大同小异,只是编译器将return g(x)
直接展开为x + 3
了。
main
函数中可以看到,直接将12写入a1
,直接将13写入a2
。所以推测直接将f(8) + 1
计算在编译器计算出来,当常数写入了。
printf 函数位于哪个地址?
可以看到jalr
跳转到了ra + 1544
的地址,也就是0x640
的地方。所以printf应该在这个位置。
在 jalr 跳转至 main 函数的 printf 时,寄存器 ra 中有什么值?
当程序进行跳转时,我们需要将 ra 寄存器存储的返回地址指向 printf 执行结束后返回到主程序的位置,也就是当前位置 PC 加 4,也就是 0x38
Backtrace (moderate)
Statement
在调试过程中,回溯通常很有用:在发生错误时,堆栈上的函数调用列表。
在
kernel/printf.c
中实现一个backtrace()
函数。在sys_sleep
中插入对该函数的调用,然后运行bttest
,调用sys_sleep
。输出结果如下
1 |
|
bttest
之后退出qemu
。在终端中:地址可能略有不同,但如果运行addr2line -e kernel/kernel
(或riscv64-unknown-elf-addr2line -e kernel/kernel
)并剪切粘贴上述地址,则如下所示:应该查阅类似下面的内容:
1
2
3
4
5$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-Dkernel/sysproc.c:74
、kernel/syscall.c:224
、kernel/trap.c:85
。
Hints
- 在
kernel/defs.h
中添加backtrace
原型,以便在sys_sleep
中调用backtrace
。 - GCC 编译器会将当前执行函数的帧指针存储在寄存器 s0
中。将以下函数添加到
kernel/riscv.h
, 并在回溯中调用该函数来读取当前帧指针。该函数使用内联汇编读取 s0。1
2
3
4
5
6
7static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
} - 这些讲义中有一张堆栈帧布局的图片。返回地址位于堆栈帧的帧指针的固定偏移量(-8)处,而保存的帧指针位于帧指针的固定偏移量(-16)处。
- Xv6 为 xv6 内核中的每个堆栈分配一个 PAGE 对齐地址的页面。您可以使用
PGROUNDDOWN(fp)
和PGROUNDUP(fp)
计算堆栈页面的顶部和底部地址(参见kernel/riscv.h
)。
Analysis & Solution
前两个提示说的很明白了,这里就跳过。
首先读出s0
寄存器的值,即当前函数的栈指针。然后用类似链表遍历的方式,每次输出return address
的值,然后移动到prev frame
继续遍历即可。
1 |
|
然后在sys_sleep()
和panic()
中调用backtrace
。
运行结果。
Alarm (Hard)
咕咕咕先。说实话,题目我都没看懂().
大概就是实现 \(CPU\) 计时器,当一个进程使用 \(CPU\) 资源的时候,周期性的发出一个警告,类似于时间片轮转算法的简化版本。感觉没十几个小时弄不完,等有生之年吧。