目录

6.S081 lab4 traps

RISC-V assembly

这是一个简单的RISC-V汇编热身关卡。

我们需要查看user/call.asm来回答一些问题,其主要内容如下:

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int g(int x) {
   0:	1141                	addi	sp,sp,-16
   2:	e422                	sd	s0,8(sp)
   4:	0800                	addi	s0,sp,16
  return x+3;
}
   6:	250d                	addiw	a0,a0,3
   8:	6422                	ld	s0,8(sp)
   a:	0141                	addi	sp,sp,16
   c:	8082                	ret

000000000000000e <f>:

int f(int x) {
   e:	1141                	addi	sp,sp,-16
  10:	e422                	sd	s0,8(sp)
  12:	0800                	addi	s0,sp,16
  return g(x);
}
  14:	250d                	addiw	a0,a0,3
  16:	6422                	ld	s0,8(sp)
  18:	0141                	addi	sp,sp,16
  1a:	8082                	ret

000000000000001c <main>:

void main(void) {
  1c:	1141                	addi	sp,sp,-16
  1e:	e406                	sd	ra,8(sp)
  20:	e022                	sd	s0,0(sp)
  22:	0800                	addi	s0,sp,16
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	7b850513          	addi	a0,a0,1976 # 7e0 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0
  34:	608080e7          	jalr	1544(ra) # 638 <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	276080e7          	jalr	630(ra) # 2b0 <exit>

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

根据RISC-V的 calling convention,a0-a7,fa0-fa7包含了函数的参数。调用printf时,a0为格式化字符串,a1是 12,a2是 13。

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.)

由于fg函数都是简单的常数计算,传递的参数也是常数 8,所以函数调用被编译器优化掉了,在0x26位置,直接将函数调用结果立即数 12 载入寄存器a1

At what address is the function printf located?

从代码中看,很显然,在0x638得位置。

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

jalr指令是链接并跳转,将返回地址保存到ra寄存器,所以应为0x38

1
2
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

运行以上代码,输出HE110 World。数字 57616 的 16 进制表示为 0xE110;RISC-V采用小端法表示,16 进制的 72、6c、64、00 表示字符串“rld\0”,如果改为大端法,则应反过来,变为i=0x726c6400

1
printf("x=%d y=%d", 3);

printf调用少了一个参数,根据 calling convention,对y=%d会取a2的值进行输出。

Backtrace

该步骤需要实现一个backtrace函数,打印出调用轨迹,即每次调用的返回地址。

xv6 运行时的 stack 结构如下图:

/6-S081-lab4-traps/image-20210225005912570.png

s0/fp中存储着当前的 frame pointer,fp-8指向返回地址,fp-16指向上一个fp地址。

所以我们只需要不断打印当前fp的返回地址并向前追溯,直到 stack 顶部。

首先在kernel/riscv.h添加内联汇编函数以获取fp值:

1
2
3
4
5
6
7
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

然后在kernel/printf.c实现backtrace

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void
backtrace(void) {
  uint64 fp, top;
  fp = r_fp();
  top = PGROUNDUP(fp);
  while(1) {
    if (fp == top) break;
    printf("%p\n", *(uint64*)(fp-8));
    fp = *(uint64*)(fp-16);
  }
}

之后在sys_sleeppanic中加入对backtrace的调用即可。

Alarm

本关需要实现一个sigalarm(interval, handler)系统调用,cpu 每消耗 interval 个 ticks 后,调用一次 handler 函数。

首先要在user/user.h添加对新系统调用的用户接口:

1
2
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

sigreturn是一个被设计用来帮助我们实现sigalarm的函数,每个handler执行结束后,都调用sigreturn

首先要在proc中添加新的字段,记录intervalhandler以及所需的辅助变量,在allocpoc中对它们进行初始化,在系统调用执行时,保存相应的值到proc中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
uint64
sys_sigalarm(void)
{
  struct proc *p = myproc();
  if (argint(0, &(p->alarm_interval)) < 0)
    return -1;
  if (argaddr(1, &(p->alarm_hanlder)) < 0)
    return -1;
  return 0;
}

之后,我们需要在usertrap识别到 timer interrupt 时,进行处理,hints 告诉我们,是which_dev == 2

1
2
3
4
5
6
7
8
9
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
  p->passed++;
  if (p->passed == p->alarm_interval) {
    p->passed = 0;
    p->trapframe->epc = p->alarm_hanlder;
  }
  yield();
}

此时只需让sigreturn直接返回 0,这样简单地添加代码,可以让test0打印出 alarm,但是随后,程序便逻辑崩溃,无法通过测试。这是因为当 kernle 处理完 time interru,回到用户模式,pc 指向 handler 的位置,之后开始执行 handler 函数,在 handler 函数尾部,调用sigreturn陷入 kernel,并无操作,再次返回用户态,执行 handler 尾部的ret。此时用于ret指令的返回地址寄存器ra所存储的值,是在 time interrupt 之时,test 函数执行中产生的ra的值,并非是 time interrupt 发生时,正在执行的代码地址,所以程序不能返回正确位置,并且 handler 执行过程中,修改了的部分寄存器也需要恢复。

于是,我们需要在 handler 执行前保存tramframe,在执行后的sigreturn中恢复tramframe,让代码返回到正确的位置执行,并使寄存器的数值复原。同时,根据 hints,为了防止 handler 执行过程中被重复调用,添加permission字段来进行控制,此外,interval==0时,意味着取消 alarm。

1
2
3
4
5
6
7
8
9
uint64
sys_sigreturn(void)
{
  struct proc* p = myproc();
  p->permission = 1;
  *p->trapframe = p->alarm_frame;
  printf("ra:%p\n", p->trapframe->ra);
  return 0;
}

usertrap中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if(which_dev == 2) {
  p->passed++;
  if (p->permission &&
      p->alarm_interval &&
      p->passed == p->alarm_interval) {
    p->passed = 0;
    p->permission = 0;
    p->alarm_frame = *p->trapframe;
    p->trapframe->epc = p->alarm_hanlder;
  }
  yield();
}

到此,便完成了 lab4 traps。

此外,还有一点,笔者曾尝试只保存tramframe中的caller save寄存器,但是无法通过测试。最终查看asm文件发现:callee save寄存器是在被调用函数尾部的 ret 指令前进行恢复的,但是在sigreturn中通过恢复epc的方式,将pc直接指向了被 time interrupt 打断执行的代码位置,所以callee save寄存器在被修改后并未被复原,我们必须保存trapframe中的所有寄存器。