xv6 Lab4: Traps
Lapin Gris Lv3

Lab4 实现用户态 trap 处理,集中在返回用户态的中断上下文保存,很类似 signal 处理。

RISC-V assembly

主要是一些基础知识。

Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?

根据 xv6 传参规范, a1,a2 分别存放第一个参数和第二个参数。故 13 在 a2,另外通过 call.asm 也能看出。

printf("%d %d\n", f(8) + 1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12

Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)

000000000000000e
0000000000000000

At what address is the function printf located?

0000000000000630

What value is in the register ra just after the jalr to printf in main?

0x38
讲义里介绍,ra 存储的是下一条指令的地址。所以就是,

exit(0);
38: 4501 li a0,0

Ref: https://pdos.csail.mit.edu/6.1810/2023/lec/l-riscv.txt#:~:text=ra <- address of next instruction

Run the following code.
unsigned int i = 0x00646c72;
printf(“H%x Wo%s”, 57616, &i);
What is the output? Here’s an ASCII table that maps bytes to characters.
The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

输出是 “HE110 World”
如果是大段序,i = 0x726c64

下面解释输出为什么是 E110 和 rld?
为什么是 rld?
16 进制字符是 2 组 2 组一读,根据链接里的对照表,我们将 0x00646c72 翻译为 dlr,
因为是小端,所以时间上 CPU 从小端往大端读,也就是 rld.
00 -> null
64 -> ‘d’
6c -> ‘l’
72 -> ‘r’
为什么是 E110?

# python3
>>> hex(57616)
'0xe110'

In the following code, what is going to be printed after ‘y=’? (note: the answer is not a specific value.) Why does this happen?

打印的是 a2 寄存器的值

Backtrace

做这个任务前需要先理解 RISC-V 的 Calling conventions (调用约定) ,理解了 calling conventions 实现 backtrace 就显得很简单~

函数调用通常都是压栈,栈是从高处往低处增长,图中展示每一个函数的压栈情况,依次压栈的是”函数返回地址”,”fp”,”寄存器”,”本地变量”等
然后就是被调用函数以同样的次序压栈,函数调用会依次压栈下去。
$fp 寄存器指向的当前函数的栈帧
$sp 寄存器指向的是栈顶,在 backtrace 里,我们不需要它。

下面这张图很重要,代表 RISC-V 函数调用过程中的压栈操作,也就是函数的栈帧(fucntion frame)

function call stack frame

https://pdos.csail.mit.edu/6.828/2023/lec/l-riscv.txt

关键实现

1、kernel/riscv.h: 实现一个获取当前 frame pointer 的方法:

static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}

2、实现栈回溯

kernel/memlayout.h 里描述了 xv6 内核的 stack 的大小,KSTACK(p) 的参数 p 代表是一个 proc,一个 进程,xv6 里,每个进程的 KSTACK 占用 2 个 PGSIZE 大小,其中一个内核栈,另一个是 GUARD PAGE,用于检测栈溢出的,防止“爆栈”后污染别的进程地址空间。
所以,xv6 每个进程的的内核栈大小都是一个 PAGE(4K)。

// map kernel stacks beneath the trampoline,
// each surrounded by invalid guard pages.
#define KSTACK(p) (TRAMPOLINE - ((p) + 1) * 2 * PGSIZE)

在了解内核栈后。实现对一个进程的 backtrace,就显得很简单。
首先获取当前 fp,这个 fp 指向内核栈的某个地址。使用 PGROUNDUP 获得页的上界(因为栈是从高往低处增长,所以向上取基址,最上面的是栈底),通过 fp 之间的指向进行回溯。

  • 当前 fp 减 8 就是返回地址所在栈帧(frame)地址,解引用地址就获得 return address。
  • 将 fp 减 16 地址的内容解引用获得就是 caller fp。

看上图,return address 的位置就在 fp 的下面,通过 fp-8 访问。previous fp 在 return address 的下面,通过 fp - 16 访问,还有函数参数等均可以通过 fp 相对位置计算出来。

void
backtrace()
{
uint64 retaddr;
uint64 fp = r_fp();
uint64 top = PGROUNDUP(fp);

printf("backtrace:\n");
while (fp < top) {
retaddr = *(uint64 *)(fp - 8);
printf("%p\n", retaddr);

fp = *(uint64 *)(fp - 16);
}
}

Show me the code

diff --git a/kernel/defs.h b/kernel/defs.h
index f3f5fef..3dface5 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -147,6 +147,7 @@ void trapinit(void);
void trapinithart(void);
extern struct spinlock tickslock;
void usertrapret(void);
+void backtrace();

// uart.c
void uartinit(void);
diff --git a/kernel/printf.c b/kernel/printf.c
index 9171d09..0f901d1 100644
--- a/kernel/printf.c
+++ b/kernel/printf.c
@@ -122,6 +122,7 @@ panic(char *s)
printf("panic: ");
printf(s);
printf("\n");
+ backtrace();
panicked = 1; // freeze uart output from other CPUs
for (;;)
;
diff --git a/kernel/riscv.h b/kernel/riscv.h
index 4bb2f3a..16ca148 100644
--- a/kernel/riscv.h
+++ b/kernel/riscv.h
@@ -319,6 +319,14 @@ r_ra()
return x;
}

+static inline uint64
+r_fp()
+{
+ uint64 x;
+ asm volatile("mv %0, s0" : "=r" (x) );
+ return x;
+}
+
// flush the TLB.
static inline void
sfence_vma()
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 7990c27..0cd201d 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -54,6 +54,8 @@ sys_sleep(void)
int n;
uint ticks0;

+ backtrace();
+
argint(0, &n);
if (n < 0)
n = 0;
diff --git a/kernel/trap.c b/kernel/trap.c
index f30d2e3..0dcd358 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c
@@ -223,3 +223,19 @@ devintr()
return 0;
}
}
+
+void
+backtrace()
+{
+ uint64 retaddr;
+ uint64 fp = r_fp();
+ uint64 top = PGROUNDUP(fp);
+
+ printf("backtrace:\n");
+ while (fp < top) {
+ retaddr = *(uint64 *)(fp - 8);
+ printf("%p\n", retaddr);
+
+ fp = *(uint64 *)(fp - 16);
+ }
+}

Alarm

关键实现

1、kernel/proc.h:在 struct proc 添加必要的结构体成员(passed ticks、ticks 阈值、handler 地址、trapframe 备份)
优化:这些结构体可以通过单独放到一个 struct alarm 的结构体里,这样就可以达到在不添加 alarm 前缀的情况下区分不同成员的命名。

struct proc {
...
// traps alarm
int alarm_passed;
int alarm_threshold;
uint64 alarm_handler;
struct trapframe *alarm_prev_trapframe;
};

2、解析用户态调用的参数

uint64
sys_sigalarm(void)
{
int period;
uint64 handler;
struct proc *p = myproc();

argint(0, &period);
argaddr(1, &handler);

p->alarm_threshold = period;
p->alarm_handler = handler;

return 0;
}

3、每次时钟中断都会调用 usertrap。因此,我们在 usertrap 里实现 alarm feature。每次发生时钟中断时候,递增 alarm_passed,当 alarm_passed 等于设置的阈值时,此时需要返回用户态,执行用户态的 handler。
注:是返回用户态执行 handler,不是直接在内核态执行 handler。

让流程返回用户态,只需要将 epc 设置为 handler 的地址即可,当返回用户态时候就会从 epc 指向的地方开始执行。在返回之前,我们需要保存现场,将 trapframe 结构体保存到 alarm_prev_trapframe 里,赋值语法是 C 语言的结构体赋值。

void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if (which_dev == 2) {
p->alarm_passed++;
if (p->alarm_passed == p->alarm_threshold) {
*(p->alarm_prev_trapframe) = *(p->trapframe);
p->trapframe->epc = p->alarm_handler;
}
yield();
}
...
}

4、中断返回,返回需要先恢复现场,恢复到发生中断前。通过将 alarm_prev_trapframe 中保存的现场恢复到 trapframe 即可。重置 alarm_passed。 题目要求,返回值是 a0 寄存的值,不能直接写 return 0.

uint64
sys_sigreturn(void)
{
struct proc *p = myproc();

*(p->trapframe) = *(p->alarm_prev_trapframe);
p->alarm_passed = 0;

return p->trapframe->a0;
}

Show me the code

diff --git a/Makefile b/Makefile
index 46997f8..b5eaf29 100644
--- a/Makefile
+++ b/Makefile
@@ -188,6 +188,7 @@ UPROGS=\
$U/_grind\
$U/_wc\
$U/_zombie\
+ $U/_alarmtest\



diff --git a/kernel/proc.c b/kernel/proc.c
index 4315d58..ae8d6ed 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -133,6 +133,13 @@ allocproc(void)
return 0;
}

+ // Allocate a trapframe page for alarm
+ if ((p->alarm_prev_trapframe = (struct trapframe *)kalloc()) == 0) {
+ freeproc(p);
+ release(&p->lock);
+ return 0;
+ }
+
// An empty user page table.
p->pagetable = proc_pagetable(p);
if (p->pagetable == 0) {
@@ -147,6 +154,10 @@ allocproc(void)
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;

+ p->alarm_passed = 0;
+ p->alarm_threshold = 0;
+ p->alarm_handler = 0;
+
return p;
}

@@ -159,6 +170,9 @@ freeproc(struct proc *p)
if (p->trapframe)
kfree((void *)p->trapframe);
p->trapframe = 0;
+ if (p->alarm_prev_trapframe)
+ kfree((void *)p->alarm_prev_trapframe);
+ p->alarm_prev_trapframe = 0;
if (p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
@@ -170,6 +184,10 @@ freeproc(struct proc *p)
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
+
+ p->alarm_passed = 0;
+ p->alarm_threshold = 0;
+ p->alarm_handler = 0;
}

// Create a user page table for a given process, with no user memory,
diff --git a/kernel/proc.h b/kernel/proc.h
index a74496c..fe3d174 100644
--- a/kernel/proc.h
+++ b/kernel/proc.h
@@ -104,4 +104,10 @@ struct proc {
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
+
+ // traps alarm
+ int alarm_passed;
+ int alarm_threshold;
+ uint64 alarm_handler;
+ struct trapframe *alarm_prev_trapframe;
};
diff --git a/kernel/syscall.c b/kernel/syscall.c
index b1f6d3d..4e19877 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -102,6 +102,8 @@ extern uint64 sys_unlink(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_close(void);
+extern uint64 sys_sigalarm(void);
+extern uint64 sys_sigreturn(void);

// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
@@ -113,6 +115,7 @@ static uint64 (*syscalls[])(void) = {
[SYS_sleep] sys_sleep, [SYS_uptime] sys_uptime, [SYS_open] sys_open,
[SYS_write] sys_write, [SYS_mknod] sys_mknod, [SYS_unlink] sys_unlink,
[SYS_link] sys_link, [SYS_mkdir] sys_mkdir, [SYS_close] sys_close,
+ [SYS_sigalarm] sys_sigalarm, [SYS_sigreturn] sys_sigreturn,
};

void
diff --git a/kernel/syscall.h b/kernel/syscall.h
index 1e1f4ba..2133a24 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -20,3 +20,5 @@
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
+#define SYS_sigalarm 22
+#define SYS_sigreturn 23
\ No newline at end of file
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 0cd201d..0c61942 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -93,3 +93,30 @@ sys_uptime(void)
release(&tickslock);
return xticks;
}
+
+uint64
+sys_sigalarm(void)
+{
+ int period;
+ uint64 handler;
+ struct proc *p = myproc();
+
+ argint(0, &period);
+ argaddr(1, &handler);
+
+ p->alarm_threshold = period;
+ p->alarm_handler = handler;
+
+ return 0;
+}
+
+uint64
+sys_sigreturn(void)
+{
+ struct proc *p = myproc();
+
+ *(p->trapframe) = *(p->alarm_prev_trapframe);
+ p->alarm_passed = 0;
+
+ return p->trapframe->a0;
+}
diff --git a/kernel/trap.c b/kernel/trap.c
index 0dcd358..58de139 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c
@@ -79,8 +79,19 @@ usertrap(void)
exit(-1);

// give up the CPU if this is a timer interrupt.
- if (which_dev == 2)
+ if (which_dev == 2) {
+
+ // stop generating periodic alarm calls if threshold is 0
+ if (p->alarm_threshold != 0) {
+ p->alarm_passed++;
+
+ if (p->alarm_passed == p->alarm_threshold) {
+ *(p->alarm_prev_trapframe) = *(p->trapframe);
+ p->trapframe->epc = p->alarm_handler;
+ }
+ }
yield();
+ }

usertrapret();
}
diff --git a/user/alarmtest.c b/user/alarmtest.c
index 2337221..9abd157 100644
--- a/user/alarmtest.c
+++ b/user/alarmtest.c
@@ -106,6 +106,7 @@ test1()
// occurred; another is that that registers may not be
// restored correctly, causing i or j or the address ofj
// to get an incorrect value.
+ printf("\n test1: i: %d, j:%d\n", i, j);
printf("\ntest1 failed: foo() executed fewer times than it was called\n");
}
else {
diff --git a/user/user.h b/user/user.h
index 5837a96..fdcc993 100644
--- a/user/user.h
+++ b/user/user.h
@@ -22,6 +22,8 @@ int getpid(void);
char *sbrk(int);
int sleep(int);
int uptime(void);
+int sigalarm(int ticks, void (*handler)());
+int sigreturn(void);

// ulib.c
int stat(const char *, struct stat *);
diff --git a/user/usys.pl b/user/usys.pl
index 01e426e..3c258dc 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -36,3 +36,5 @@ sub entry {
entry("sbrk");
entry("sleep");
entry("uptime");
+entry("sigalarm");
+entry("sigreturn");
\ No newline at end of file

测试结果

最后,执行 make grade 查看结果,

== Test answers-traps.txt ==
answers-traps.txt: OK
== Test backtrace test == backtrace test: OK (3.9s)
== Test running alarmtest == (5.6s)
== Test alarmtest: test0 ==
alarmtest: test0: OK
== Test alarmtest: test1 ==
alarmtest: test1: OK
== Test alarmtest: test2 ==
alarmtest: test2: OK
== Test alarmtest: test3 ==
alarmtest: test3: OK
== Test usertests ==
usertests: OK (149.0s)
== Test time ==
time: OK
Score: 95/95