Lab: traps

本实验主要关注 OS 如何由用户态陷入内核态,在 Alarm 中试图逆向模拟这个过程。整个实验的关键在于特权级转换时如何保存及恢复现场,使得整个过程对单个特权级是透明的。

RISC-V assembly

这部分主要涉及 RISC-V 汇编、数据存储方式、反汇编、编译过程中的函数内联优化…..这些老生常谈的知识点。故不再赘述,只放上我的答案:

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
26
27
28
1. Which registers contain arguments to functions?
a0 ~ a7
For example, which register holds 13 in main's call to printf?
a2
2. 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.)
26: 45b1 li a1,12
12 = 11 + 1 = g(8) + 1 = f(8) + 1
3. At what address is the function printf located?
30: 00000097 auipc ra,0x0
34: 612080e7 jalr 1554(ra)
thus the address should be 0x30 + 1554(0x612) = 0x642
4. What value is in the register ra just after the jalr to printf in main?
0x38(0x34 + 4)
5. Run the following code.
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
What is the output?
He110 World
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?
unsigned int i = 0x726c6400;
Would you need to change 57616 to a different value?
No
6. In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.)
Why does this happen?
printf("x=%d y=%d", 3);
a rubbish value(actually that is left in register a2 at the time printf is called.)

Backtrace

Implement a backtrace() function in kernel/printf.c. Insert a call to this function in sys_sleep, and then run bttest, which calls sys_sleep.

链接文档中已详细给出 RISC-V 的调用规范及栈帧布局,以及常用寄存器及其作用。循环终止条件为栈帧指针是页对齐的(xv6 为每个进程只分配了一页长度的栈空间),代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Print the backtrace information
void backtrace(void)
{
printf("backtrace:\n");

uint64 s0, ra;
s0 = r_fp();

while(s0 != PGROUNDDOWN(s0))
{
ra = *(uint64*)(s0 - 8);
printf("%p\n", ra);

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

这里有两点需要思考:

  • 为什么采用 while 循环而不是递归形式实现函数 backtrace?
  • *(uint64*)(s0 - A) 与 *((uint64*)s0 - A) 的区别是什么?

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 -q’

这部分是该实验的重头戏,需要对特权级转换时的机制足够了解。首先扩充结构体 proc 的定义:

1
2
3
4
5
6
  ...
int ticks; // Ticks since the last call
int interval; // Interval for SIGALARM
uint64 handler; // Handler for SIGALARM
struct trapframe *trapframe_dup; // copy of the trapframe
uint8 uie; // user interrupt enable

扩充结构体定义后需调整进程初始化、分支(fork)、释放过程中的操作。

随后,实现 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
26
27
28
// SIGALARM system call
uint64 sys_sigalarm(void)
{
int interval;
uint64 handler;
struct proc *p = myproc();

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

if (interval < 0 || handler > MAXVA) {
return -1;
}

p->interval = interval;
p->handler = handler;
p->ticks = 0; // reset ticks
return 0;
}

// SIGRETURN system call
uint64 sys_sigreturn(void)
{
struct proc *p = myproc();
*(p->trapframe) = *(p->trapframe_dup); // restore the trapframe
p->uie = 1; // enable user interrupt
return p->trapframe->a0;
}

为保证 SIGRETURN 后返回用户态的寄存器与初始值完全相同,sys_sigreturn 返回 p->trapframe->a0 的值(原因参照 syscall 函数的实现)。

在 usertrap 函数中实现核心处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if(which_dev == 2) {
if (p->interval) {
p->ticks++;
if (p->ticks >= p->interval) {
if (p->uie) {
// If the user interrupt is enabled
// reset ticks, copy the trapframe and trap to the handler
// finally set uie to 0(disable user interrupt)
p->ticks = 0;
*(p->trapframe_dup) = *(p->trapframe); // save the trapframe
p->trapframe->epc = p->handler;
p->uie = 0;
} else {
// If the user interrupt is not enabled
// don't da anything(waiting for the user call sigreturn)
// actually it return to the user program
}
}
}
yield();
}

在 ticks 满足跳转条件时,将用户态的现场保存在 p->trapframe_dup 中,并更新 p->trapframe->epc 为 p->handler 以跳转至目标处理函数。p->uie 用于防止执行处理函数时被频繁打断重启,若 uie 为 0 时即使 ticks 满足跳转条件也不执行任何操作,仿佛其在用户态执行处理函数时关闭中断直到执行系统调用 SIGRETURN 主动陷入内核。

PS:起初我希望真正的关闭用户态的时钟中断(虽然这样既不优雅也不合理),在阅读了 RISC-V 手册后尝试操作相应寄存器,但最终并没能成功。