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
3
git fetch
git checkout traps
make clean

此外,新建一个traps_dev分支来进行实际实验。

1
git checkout -b traps_dev

traps_dev中每通过一个作业的测试,提交(git commit)你的代码,并将所做的修改合并(git merge)到util中,然后提交(git push)到github。

1
2
3
4
5
git add .
git commit -m "xxxxxxxx"
git checkout traps
git merge traps_dev
git push github traps:traps

RISC-V assembly

在本部分中将给出一段 RISC-V 汇编代码,通过阅读代码我们要回答几个问题,并把答案存储在主目录下的 answers-traps.txt下。

运行 make fs.img后会编译user/call.c, 并生成user/call.asm。我们需要观察call.asm下的gfmain函数。

RISC-V 参考文档:RISC-V unprivileged instructions RISC-V privileged instructions

哪些寄存器包含函数的参数?例如,在 main 调用 printf 时,哪个寄存器保存 \(13\)

查阅Calling conventions手册,可以发现 \(a_0 \rightarrow a_7\)为函数参数和返回值寄存器。

RISC-V常用寄存器及使用约定

13属于第三个参数(第一个为format string,第二个为f(8) + 1)。所以存储在\(a_2\)寄存器中。

main 的汇编代码中,函数 f 的调用在哪里?对 g 的调用在哪里? 提示:编译器可能会内联函数)。

我们先分析一下g()的汇编代码。

1
2
3
4
5
6
7
8
9
10
11
12
int 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 ret
首先将栈顶指针sp往下移动16字节,等价与要入栈两个元素。将ra,即caller进程的pc值,存入栈的第一个位置。将s0,即caller进程的其他寄存器保存地址,存放到第二个位置。

然后将a0的值加3,存储到a0寄存器中。然后从栈中恢复ras0的地址,此时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
2
3
4
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

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-D
应该查阅类似下面的内容:kernel/sysproc.c:74kernel/syscall.c:224kernel/trap.c:85

Hints

  • kernel/defs.h 中添加 backtrace 原型,以便在 sys_sleep 中调用 backtrace
  • GCC 编译器会将当前执行函数的帧指针存储在寄存器 s0 中。将以下函数添加到 kernel/riscv.h, 并在回溯中调用该函数来读取当前帧指针。该函数使用内联汇编读取 s0。
    1
    2
    3
    4
    5
    6
    7
    static 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
2
3
4
5
6
7
8
9
10
11
void backtrace(void) {
printf("backtrace:\n");
uint64 fp = r_fp();
while (fp != PGROUNDUP(fp)) { // until get to stack bottom
// get return addr in current stack frame
uint64 ra = *(uint64*)(fp - 8);
printf("%p\n", ra);
// go to prev stack frame
fp = *(uint64*)(fp - 16);
}
}

然后在sys_sleep()panic()中调用backtrace

运行结果。

Alarm (Hard)

咕咕咕先。说实话,题目我都没看懂().

大概就是实现 \(CPU\) 计时器,当一个进程使用 \(CPU\) 资源的时候,周期性的发出一个警告,类似于时间片轮转算法的简化版本。感觉没十几个小时弄不完,等有生之年吧。


MIT 6.S081 lab4:traps | 陷入
https://acmicpc.top/2024/02/29/MIT-6.S081-lab04/
作者
江欣婷
发布于
2024年2月29日
许可协议