xv6-labs-2020.lab3.traps

lab3 traps

1. 作业链接

  • https://pdos.csail.mit.edu/6.828/2020/labs/traps.html

2. 实习内容

RISC-V 汇编代码

(1)函数传参使用哪些寄存器?printf() 函数调用中 13 放在那个寄存器里?

  • 函数传递参数使用寄存器:a0-a7
  • 放置在 a2 寄存器里(第 3 个参数)

(2)哪一部分的代码体现了对 f(), g() 的调用?

  • f():编译器优化的太厉害了,直接把 f(8)+1 算出来了,结果是 12
1
2
3
# f(int)
# Line 46
26: 45b1 li a1,12
  • g():编译器直接把 g() 函数优化成 inline 形式的,在调用 g() 的地方,直接使用 a0 = a0 + 3 代替
1
2
3
# g(int)
# Line 32
14: 250d addiw a0,a0,3

(3)printf() 函数的地址

  • 0000000000000630
1
2
# Line 1092
0000000000000630 <printf>:

(4)在调用 printf() 之后,ra 寄存器中保存的值是多少?

  • 0x38
    • 1536 = 0x600
    • 0x630 - 0x600 + 0x8 = 0x38

(5)下列代码输出是什么?

1
2
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
  • 输出为:HE110 World
    • 57616 = 0xe110
    • RISC-V 是小端的,因此 i 在内存中的分布为 72 6c 64 00(低地址到高地址),对应的字符串为 rld
  • 如果 RISC-V 是大端的,为了保证输出输出不变,需要把 i 的值修改为 0x726c6400,不需要修改 57616

(6)下列代码的输出是什么?

1
printf("x=%d y=%d", 3);
  • 输出结果为:x=3 y=5221
    • 其中 5221 为一个不确定的数字,寄存器传参的时候少传了一个参数,所以这个值我们是未知的

Backtrace

  • 要求
    • The compiler puts in each stack frame a frame pointer that holds the address of the caller's frame pointer. Your backtrace should use these frame pointers to walk up the stack and print the saved return address in each stack frame.
  • 在 sys_sleep() 中调用 backtrace()

kernel/defs.h

  • 添加函数原型,从而能够让 sleep() 调用 backtrace()
1
void backtrace(void);

kernel/riscv.h

  • 在这里添加读取寄存器 s0 的指令
1
2
3
4
5
static inline uint64 r_fp() {
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}

kernel/sysproc.c

  • 在 sys_sleep() 函数中添加对 backtrace() 的调用
  • 我直接加在开头了

kernel/printf.c

  • 添加函数实现
  • 注意栈帧结构
    • return address: a fixed offset (-8) from the frame pointer of a stackframe
    • the saved frame pointer: fixed offset (-16) from the frame pointer
1
2
3
4
5
6
7
8
9
10
11
12
13
// backtrace
void backtrace() {
// 获取当前的帧指针 frame pointer
uint64 fp = r_fp();
uint64 down = PGROUNDDOWN(fp);
uint64 up = PGROUNDUP(fp);
// 栈向下增长, 因此循环的时候 fp 应该是越来越大
while(fp < up && fp > down) {
printptr(*(uint64 *)(fp - 8));
consputc('\n');
fp = *(uint64 *)(fp - 16);
}
}

backtrace 用途

  • Once your backtrace is working, call it from panic in kernel/printf.c so that you see the kernel's backtrace when it panics.

Alarm

问题描述

  • In this exercise you'll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you'll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.
  • 添加两个系统调用

准备工作

Makefile
  • $U/_alarmtest 添加到 UPROGS
user/user.h
  • 添加函数原型
1
2
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
user/user.pl
  • 添加入口,辅助生成汇编代码
1
2
entry("sigalarm");
entry("sigreturn");
kernel/syscall.h
  • 添加系统调用号
1
2
#define SYS_sigalarm  22
#define SYS_sigreturn 23
kernel/syscall.c
  • 添加新系统调用的定义和入口
1
2
3
4
5
6
7
8
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

static uint64 (*syscalls[])(void) = {
// ...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};
kernel/sysproc.c
  • 添加具体的实现(测试)
1
2
3
4
5
6
uint64 sys_sigalarm(void){
return 0;
}
uint64 sys_sigreturn(void){
return 0;
}
  • 做到这里,程序能够编译成功了,但是具体功能还没有实现

test0: invoke handler

  • 实现逻辑
    • 在调用 sysalarm 的时候,在进程的数据结构中保存参数
      • handler、多久调用一次 handler、现在已经运行了多少个 ticks(初始化为 0)
    • 在收到时钟中断信号的时候,判断是否需要调用 handler,如果需要,设置 PC 为 handler
  • 这个是先破坏了栈帧,因此是有问题的
    • test1/test2 部分解决这个问题
kernel/proc.h
  • 添加一些中间变量辅助实现 sys_alarm() 功能
1
2
3
4
5
6
7
struct proc {
// ...
// 添加几个变量辅助实现 sysalarm()
int ticks_for_alarm; // 多久时间调用一次 handler
void(*)(alarm_handler); // handler
int ticks_used; // 已经使用了多少 ticks(对 ticks_for_alarm 取模)
}
kernel/proc.c
  • 对于上面那些变量的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static struct proc* allocproc(void) {
// 最后的部分添加如下修改

p->ticks_for_alarm = 0;
// 理论上修改上边一个变量就够了, 但是为了安全, 全都修改了
p->alarm_handler = 0;
p->ticks_used = 0;

return p;
}

// 安全起见也改了, 但是不该不影响结果
static void freeproc(struct proc *p) {
// ...
p->ticks_for_alarm = 0;
p->alarm_handler = 0;
p->ticks_used = 0;
}
kernel/sysproc.c
  • 设置 proc.h 中的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uint64 sys_sigalarm(void){
struct proc *p = myproc();
int n;
uint64 handler;
if(argint(0, &n) < 0)
return -1;
if(argaddr(1, &handler) < 0)
return -1;
p->alarm_handler = (void(*)()) handler;
p->ticks_for_alarm = n;
p->ticks_used = 0;
return 0;
}


uint64 sys_sigreturn(void) {
return 0;
}
kernel/usertrap.c(错误实现)
  • 每次接受到时钟中断的时候,计数器 ticks 自增
  • 在从内核态返回用户态的时候判断 ticks 记录是否超标,如果是,则调用 handler 进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void usertrap(void) {
//...
if(which_dev == 2){
if(p->ticks_for_alarm) {
++p->ticks_used;
}
yield();
}
// ...
}

void usertrapret(void) {
// 原来是 w_sepc(p->trapframe->epc);
// 现在加上一个判断, 将其设置为 handler 的地址
// 这显然是错误的, 栈帧被破坏了, 但是 test0 是 OK 的
// ...
if(p->ticks_for_alarm && p->ticks_used == p->ticks_for_alarm) {
p->ticks_used = 0;
w_sepc((uint64)p->alarm_handler);
} else {
w_sepc(p->trapframe->epc);
}
// ...
}
  • 另外一种简便的实现方式
    • 直接修改 epc
    • 但是也是破坏了栈帧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void usertrap(void) {
//...
if(which_dev == 2){
if(p->ticks_for_alarm) {
++p->ticks_used;
if(p->ticks_used == p->ticks_for_alarm) {
p->ticks_used = 0;
p->trapframe->epc = (uint64)p->alarm_handler;
}
yield();
}
}
// ...
}

test1: resume interrupted code

  • 这一部分的实现除了 usertrap.c,其他部分都是基于 test0 进行修改的
kernel/usertrap.c(正确实现)
  • 需要保存寄存器状态,通过 sysalarm 和 sigreturn 的配合实现
  • 要求每个 hander 的最后都需要调用 sigreturn 回到内核态
  • test0 的实现回到用户态之前需要保存当时的寄存器状态,当 sigreturn 回到了内核态是恢复寄存器状态,返回用户态正常执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void usertrap(void) {
// ...
if(which_dev == 2){
if(p->ticks_for_alarm) {
++p->ticks_used;
if(p->ticks_used == p->ticks_for_alarm) {
p->ticks_used = 0;
// 保存所有的寄存器状态
memmove(&(p->trapframe2), p->trapframe, sizeof(struct trapframe));
p->trapframe->epc = (uint64)p->alarm_handler;
}
}
yield();
}
// ...
}
kernel/proc.h
  • 需要在 proc 数据结构中保存寄存器
  • 这里为了方便直接使用 trapframe 的数据结构,实际上有些值是不需要保存恢复的
    • kernel 的相关值不用恢复:kernel_satp、kernel_sp、kernel_trap、kernel_hartid
1
2
3
4
5
struct proc {
// ...
// 保存 sysalarm 的寄存器状态, kernel 的 4 个不需要, 但是为了方便都写了
struct trapframe trapframe2;
};
kernel/sysproc.c
  • 当调用 sys_sigreturn 的时候恢复寄存器状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint64 sys_sigreturn(void) {
struct proc *p = myproc();
// 恢复寄存器状态(4 个 kernel 的不需要)
// 如果是 sigalarm(0, 0) 的话不需要恢复现场
if(p->ticks_for_alarm) {
memmove(
((uint64 *)p->trapframe) + 5,
((uint64 *)&(p->trapframe2)) + 5,
sizeof(struct trapframe) - 5*sizeof(uint64)
);
p->trapframe->epc = p->trapframe2.epc;
p->is_alarm = 0;
}
return 0;
}
  • 到这里为止,已经能够通过 test2,在不破坏栈帧的情况下实现 sysalarm 的效果

test2(): prevent re-entrant calls

  • 这一部分的实现基于 test1 进行修改的
实现逻辑
  • 不允许重复调用,当一个 sysalarm 已经存在的时候,没有调用 sigreturn 之前不允许调用新的 sigalarm
  • 这一部分的实现很简单,在 proc 数据结构中添加一个标记变量
    • 调用 sigalarm 的时候设置为 1
      • 如果调用 sigalarm 的时候已经为 1,则该调用不生效
    • 调用 sigreturn 的时候设置为 0
kernel/proc.h
  • 添加一个标记变量
1
2
3
4
struct proc {
// ...
int is_alarm; // 是否调用 sigalarm
};
kernel/proc.c
  • 标记变量的初始化
1
2
3
4
5
6
7
8
9
10
11
static struct proc* allocproc(void) {
// ...
p->is_alarm = 0;
return p;
}

// 安全起见也改了, 但是不该不影响结果
static void freeproc(struct proc *p) {
// ...
p->is_alarm = 0;
}
kernel/sysproc.c(错误实现)
  • 添加标记变量的判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint64 sys_sigalarm(void){
// ...
if(p->is_alarm) {
return 0;
} else if(handler){
// 注意如果调用的是 sigalarm(0, 0) 的话是不需要调用 sigreturn 的
p->is_alarm = 1;
}
// ...
}

uint64 sys_sigreturn(void) {
// ...
p->is_alarm = 0;
// ...
}
  • 这样子实现是错误的,只能阻止新的 sigalarm 的调用,但是不能够实现已经设置的 handler 在 sigreturn 之前的再次调用
  • 输出结果
1
2
3
4
test2 start
.....alarm!
alarm!
test2 failed: alarm handler called more than once
kernel/sysproc.c(正确实现)
  • 此时 is_alarm 的含义变成了是否调用 handler
  • 如果有 handler 调用,禁止新的 sigalarm 调用
  • 恢复当然还是在调用 sigreturn 的时候恢复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
uint64 sys_sigalarm(void){
// ...
// 如果已经调用了一个 handler, 而且没有返回的话
// 我们禁止这个新的调用(我们的设计不支持递归调用)
if(p->is_alarm) {
return 0;
}
// ...
}

uint64 sys_sigreturn(void) {
// ...
// 恢复寄存器状态(4 个 kernel 的不需要)
// 只有保存了寄存器才需要恢复现场
if(p->is_alarm) {
memmove(
((uint64 *)p->trapframe) + 5,
((uint64 *)&(p->trapframe2)) + 5,
sizeof(struct trapframe) - 5*sizeof(uint64)
);
p->trapframe->epc = p->trapframe2.epc;
}
p->is_alarm = 0;
// ...
}
kernel/trap.c
  • 正确实现应该是在保存寄存器的时候进行判断
    • 因为我们只有一块区域,不能够实现递归调用
    • 也能解决上面的问题
1
2
3
4
5
6
7
8
9
10
void usertrap(void) {
// ...
// 保存所有的寄存器状态
if(!(p->is_alarm)) {
p->is_alarm = 1;
memmove(&(p->trapframe2), p->trapframe, sizeof(struct trapframe));
p->trapframe->epc = (uint64)p->alarm_handler;
}
// ...
}

3. 实验结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
== Test answers-traps.txt == answers-traps.txt: OK
== Test backtrace test ==
$ make qemu-gdb
backtrace test: OK (14.9s)
== Test running alarmtest ==
$ make qemu-gdb
(7.0s)
== Test alarmtest: test0 ==
alarmtest: test0: OK
== Test alarmtest: test1 ==
alarmtest: test1: OK
== Test alarmtest: test2 ==
alarmtest: test2: OK
== Test usertests ==
$ make qemu-gdb
usertests: OK (231.3s)
(Old xv6.out.usertests failure log removed)
== Test time ==
time: OK
Score: 85/85

4. 遇到的困难以及收获

  • 做完整个 lab 感觉操作系统的设计很巧妙,这里的的 sysalarm 的实际就感觉很有趣,通过两次系统调用解决了一次系统调用很难解决的问题。
  • 具体的解决方法在上面都已经提到了,主要是怎么设置 PC,以及栈帧的保存与恢复问题。

一个小问题

  • 在这个 lab 的实现中,在 sigreturn() 未返回时,有两种实现方案
    • 不允许 sigalarm() 重新设置 handler
    • 允许 sigalarm() 重新设置 handler,但是不允许其有修改保存的寄存器(我们只有一块区域,不支持递归)
  • 这两种方法都可行,测试都能过
  • 但似乎又都有各自的问题
    • 第一种方法不能允许 sigalarm(0, 0) 的调用,这样的调用能够取消 handler()
    • 第二种方法会让最终的 handler() 不太可控,可能在 sigreturn() 未返回的时候设置了奇怪的 handler()
  • 我是按照方法 1 实现的

sys_alarm 保存的寄存器

  • epc(需要)
  • callee-saved(需要)(sig_return 之前可能没有 pop 栈)
  • caller-saved(不需要)
  • kernel_sp(不需要)
  • kernel_hartid(不能)
  • kernel_stap(不需要)

5. 对课程或 lab 的意见和建议

  • 一个建议是最后的 usertests.c 的运行时间太长了,感觉没有必要用那么多的测试,之前已按因为一行代码写反了一直超时,浪费了太多时间。

6. 参考文献

  • https://pdos.csail.mit.edu/6.828/2020/reference.html
  • https://github.com/riscv/riscv-isa-manual/releases/download/draft-20200727-8088ba4/riscv-spec.pdf
  • https://pdos.csail.mit.edu/6.828/2019/xv6/book-riscv-rev0.pdf