文章

MIT 6.S081 | 0x04 Traps

MIT 6.S081 | 0x04 Traps

Before you start coding, read Chapter 4 of the xv6 book, and related source files:

  • kernel/trampoline.S: the assembly involved in changing from user space to kernel space and back
  • kernel/trap.c: code handling all interrupts

:green_square: RISC-V assembly

这道题让我们通过阅读一个文件 user/call.casm 代码来学习 RISC-V 汇编的一些基础知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
  return x+3;
}

int f(int x) {
  return g(x);
}

void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}

这个文件很简单,main 使用 printf 打印两个数字,并在其中通过 f 调用 g。下面就来看看实验提了哪些问题吧

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

直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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+0xe6>
  30: 00000097             auipc ra,0x0
  34: 612080e7             jalr 1554(ra) # 642 <printf>
  exit(0);
  38: 4501                 li a0,0
  3a: 00000097             auipc ra,0x0
  3e: 28e080e7             jalr 654(ra) # 2c8 <exit>

在 24 和 26 这两行分别向 a2a1 两个寄存器存了 13、12 这两个值。所以可以回答这个问题,函数的寄存器存在 a1~a7 这些寄存器中(查查 RISC-V 寄存器也能知道)

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

还是上面那段代码,a1a2 存了两个立即数,这个 12 其实就是 f(8)+1 的结果。所以说编译器在编译的过程中直接将 fg 的调用进行内联,直接用它们的结果进行编译了

At what address is the function printf located?

1
0000000000000642 <printf>:

所以 printf 这个函数的地址位于 0x642。这个问题一出来,就会很自然地想到下面的问题:

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

main 中有一句 jalr 1554(ra),这应该是在 ra 的基础上加 1554,而且这是一个十进制立即数,那么 0x642 减去这个立即数得到的结果是 48,转换为十六进制就是 0x30,这正好是 jalr 跳转的上一句地址

我还查了一下 auipc ra, 0x0 的含义:

这是一个 RISC-V 指令,用于将一个 20 位的立即数(偏移量)加到当前指令的地址的高 20 位上,结果存储在目标寄存器 ra

具体来说,auipc 指令将当前指令的地址的高 20 位(即 PC[31:12])与一个 20 位的立即数相加,得到一个 32 位的地址,然后将这个地址存储在目标寄存器 ra 中。这个指令通常用于计算全局地址,例如用于加载全局变量的地址

例如,如果当前指令的地址是 0x1000,那么执行 auipc ra, 0x0 指令后,目标寄存器 ra 将被设置为 0x1000

Run the following code.

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

What is the output?

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?

这个问题考察我们对大端小端的理解,下面先来进行一个分析。首先是 "H%x",这个 %x 打印的是 57616 的 16 进制值,跟大小端没关系,所以打印的第一个值必定是 HE110。然后是这个 "Wo%s",将一个无符号整数转换为字符串,因为 RISC-V 是小端存储,所以低地址在前,高地址在后,所以打印的字符应该分别为 726c64 以及 00(字符串结束),ASCII 转码后打印得到的是 rld。现在可以对这个问题进行一个解答:

  1. 因为 RISC-V 是小端存储,所以全部的输出为:HE110 World
  2. 如果是大端存储,因为 57616 直接打印值,所以不需要改变;而 i 需要改变,应改为 0x726c6400

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

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

y= 后的数字不确定,其数值取决于调用 printf 时第二个寄存器 a2 上的值。发生这种情况的原因是 printf 内有两个变量需要输出,但在后面的列表中只给了一个值,第二个值未给定,导致了输出结果的不确定

:blue_square: 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. Your output should be as follows:

1
2
3
4
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

After bttest exit qemu. In your terminal: the addresses may be slightly different but if you run addr2line -e kernel/kernel (or riscv64-unknown-elf-addr2line -e kernel/kernel) and cut-and-paste the above addresses as follows:

1
2
3
4
5
$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D

You should see something like this:

kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85

分析

实现一个 backtrace() 方便调试。GCC 编译器将当前调用函数的 frame pointer 存到寄存器 s0 中,下图是函数栈的结构,返回地址存到 fp - 8 的位置,fp - 16 是上一个 fp 指针的位置

frame pointer

代码

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
diff --git a/kernel/defs.h b/kernel/defs.h
index a3c962b..4b82fc2 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -80,6 +80,7 @@ int             pipewrite(struct pipe*, uint64, int);
 void            printf(char*, ...);
 void            panic(char*) __attribute__((noreturn));
 void            printfinit(void);
+void            backtrace(void);
 
 // proc.c
 int             cpuid(void);
diff --git a/kernel/printf.c b/kernel/printf.c
index 1a50203..1fffaa4 100644
--- a/kernel/printf.c
+++ b/kernel/printf.c
@@ -133,3 +133,19 @@ printfinit(void)
   initlock(&pr.lock, "pr");
   pr.locking = 1;
 }
+
+void
+backtrace(void)
+{
+  uint64 fp, up, ra;
+
+  fp = r_fp();
+  up = PGROUNDUP(fp);
+
+  printf("backtrace:\n");
+  while (fp != up) {
+    ra = *(uint64*)(fp - 8);
+    printf("%p\n", ra);
+    fp = *(uint64*)(fp - 16);
+  }
+}
diff --git a/kernel/riscv.h b/kernel/riscv.h
index 20a01db..85a08cc 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 3b4d5bd..4ef466f 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -51,6 +51,7 @@ sys_sbrk(void)
 uint64
 sys_sleep(void)
 {
+  backtrace();
   int n;
   uint ticks0;

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

分析

这个任务要我们实现 sigalarmsigreturn。前者每隔若干个 ticks 执行一个 handler 函数,后者结束这个 sigalarm 分配的任务。在 RISC-V 中,用户空间中的 pc 存在 p->trapframe->epc 中,我们在发生时钟中断时更新 alarm_tick_count,如果达到相应的 ticks 数则执行 alarm_handler(即 p->trapframe->epc = p->alarm_handler

大体的思路不是很复杂,有一些细节需要注意:

  1. 在执行 handler 函数之前要保持一些必要的寄存器,我这里是直接将 trapframe 进行复制。在 sigreturn 的时候还需要将这些寄存器的值进行复原
  2. 为了防止重复对 handler 进行赋值并调用,我在 proc 中设置了一个开关变量 alarm_running,在执行 handler 后无法再次对 handler 复制,直到调用 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
diff --git a/Makefile b/Makefile
index ded5bc2..d89ddfe 100644
--- a/Makefile
+++ b/Makefile
@@ -199,6 +199,7 @@ endif
 
 ifeq ($(LAB),traps)
 UPROGS += \
+ $U/_alarmtest\
  $U/_call\
  $U/_bttest
 endif
diff --git a/kernel/proc.c b/kernel/proc.c
index 959b778..5586691 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -132,6 +132,12 @@ found:
     return 0;
   }
 
+  if((p->alarm_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){
@@ -146,6 +152,11 @@ found:
   p->context.ra = (uint64)forkret;
   p->context.sp = p->kstack + PGSIZE;
 
+  p->alarm_handler = 0;
+  p->alarm_intervals = 0;
+  p->alarm_tick_count = 0;
+  p->alarm_running = 0;
+
   return p;
 }
 
@@ -157,6 +168,8 @@ freeproc(struct proc *p)
 {
   if(p->trapframe)
     kfree((void*)p->trapframe);
+  if(p->alarm_trapframe)
+    kfree((void*)p->alarm_trapframe);
   p->trapframe = 0;
   if(p->pagetable)
     proc_freepagetable(p->pagetable, p->sz);
diff --git a/kernel/proc.h b/kernel/proc.h
index d021857..77339dc 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)
+  
+  void (*alarm_handler)();
+  struct trapframe *alarm_trapframe;
+  int alarm_tick_count;
+  int alarm_intervals;
+  int alarm_running;
 };
diff --git a/kernel/syscall.c b/kernel/syscall.c
index ed65409..cf8a2fc 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -101,6 +101,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.
@@ -126,6 +128,8 @@ static uint64 (*syscalls[])(void) = {
 [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 bc5f356..382d781 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
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 4ef466f..583cf80 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -92,3 +92,27 @@ sys_uptime(void)
   release(&tickslock);
   return xticks;
 }
+
+uint64
+sys_sigalarm(void)
+{
+  struct proc* p = myproc();
+
+  argint(0, &p->alarm_intervals);
+  argaddr(1, (uint64*)&p->alarm_handler);
+
+  p->alarm_tick_count = 0;
+
+  return 0;
+}
+
+uint64
+sys_sigreturn(void)
+{
+  struct proc* p = myproc();
+
+  *p->trapframe = *p->alarm_trapframe;
+  p->alarm_running = 0;
+
+  return p->trapframe->a0;
+}
diff --git a/kernel/trap.c b/kernel/trap.c
index 512c850..94af5c0 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c
@@ -67,6 +67,17 @@ usertrap(void)
     syscall();
   } else if((which_dev = devintr()) != 0){
     // ok
+    if(which_dev == 2) {
+      if (p->alarm_intervals > 0) {
+        p->alarm_tick_count++;
+        if (p->alarm_tick_count == p->alarm_intervals && p->alarm_running == 0) {
+          *p->alarm_trapframe = *p->trapframe;
+          p->trapframe->epc = (uint64)p->alarm_handler;
+          p->alarm_tick_count = 0;
+          p->alarm_running = 1;
+        }
+      }
+    }
   } else {
     printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
     printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
diff --git a/user/user.h b/user/user.h
index 4d398d5..9426153 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..fa548b0 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -36,3 +36,5 @@ entry("getpid");
 entry("sbrk");
 entry("sleep");
 entry("uptime");
+entry("sigalarm");
+entry("sigreturn");
本文由作者按照 CC BY 4.0 进行授权