WangYu::Space

Study, think, create, and grow. Teach yourself and teach others.

MIT 6.828 - Lab - Traps

分类:操作系统标签: 6.828创建时间:2024-01-14 00:00:00

课程主页:https://pdos.csail.mit.edu/6.828/2023/index.html

RISC-V assembly

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

观察 call.asmmain 的汇编结果,可以看出 13 是由 a2 进行传递的。

  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	7d850513          	add	a0,a0,2008 # 800 <malloc+0x108>
  30:	00000097          	auipc	ra,0x0
  34:	610080e7          	jalr	1552(ra) # 640 <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0

RISC-V 中,a0-a7 用于传递整型,fa0-fa7 用于传递浮点型。这里 printf 的三个参数使用 a0-a2 来传递。

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

main 中没有对 f 函数的调用,因为 f 和 g 都是纯函数,这里参数也是字面量,所以 f(8) 的结果已经被计算出来了,其值为 12。

Q: At what address is the function printf located?

printf 的地址是 ra + 1552ra 的值由 auipc ra,0x0 计算,ra 的值为 0x64,因此 printf 的地址为 0x30 + 1552 = 0x640。这里采用的是 PC 相对寻址,在运行时 printf 的地址是多少,这就说不定了。

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

jalr	1552(ra) # 640 <printf>

JALR (jump and link register) 会跳转到 printf 处,并将 PC + 4 的值写入到 ra 中,因此 ra 的值为 0x38。

Q: Run the following code.

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

What is the output?

57616 转为 16 进制为 0xe110

这里把 0x00646c72 当做字符串来打印,因此 i 的四个字节需要被解释为字符,因为是小端存储,因此会被解释为 0x72 0x6c 0x64 0x00,对应字符串 “rld\0”,因此上面的 printf 会打印:

He110 World

Q: 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);

这里需要三个参数,但是只传了两个,printf 在获取第二个格式化参数时,会去 a2 中读取。因此具体打印什么,取决于此时 a2 中存储的是什么值。

Backtrace

这个问题是实现一个 backtrace 函数,backtrace 就是通过回溯调用栈,得到当前正在执行的函数的调用关系。

risc-v 的函数调用栈具有以下特征,每一个栈帧都在开头位置保存了函数返回地址和前一个栈帧的地址,可以通过 fp 拿到当前的栈帧,然后读取出 ra 和前一个栈帧的指针 fp,如此就可以向前回溯,直到到达栈底。

+============+ <------+
|     ra     |        |
+ - - - - - -+        |
|  revp fp   |        |
+ - - - - - -+        |
|    ...     |        |
|    ...     |        |   
+============+ <------|---+
|     ra     |        |   |
+ - - - - - -+        |   |
|  prev fp   |--------+   |
+ - - - - - -+            |
|    ...     |            |
|    ...     |            |
+============+ <-- fp     |
|     ra     |            |
+ - - - - - -+            |
|  prev fp   |------------+
+ - - - - - -+
|    ...     | 
|    ...     |
+============+

ra: return address
fp: frame pointer
prev fp: previous fp

实现方式如下:

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

void backtrace() {
  uint64 fp = r_fp();
  uint64 base = PGROUNDDOWN(fp);
  while (fp > base) {
    uint64 ra = *(uint64*)(fp - 8);
    fp = *(uint64*)(fp - 16);
    printf("%p\n", (void*)ra);
  }
}

r_fp 用于读取当前 fpfp - 8 就是 ra 的地址,fp - 16 就是前一帧的 fp 的地址。

因为调用栈的大小只有一页,因此当 fp 小于当前页的起始位置时,可以退出回溯。

Alarm

实现一个定时器,用户可以使用这个定时器执行周期性任务。

void handler() {
    printf("alarm\n");
    sigreturn();
}

int main() {
    sigalarm(10, handler)
}

sigalarm 用于设置周期性任务,当时间到了后,之前设置的回调就会被调用,在回调中 sigreturn 用于返回原来的执行流。

思路

将当前设置的 alarm 回调保存在当前进程的结构体中。在时钟中断中,判断是否到了执行的时机,如果到了,则切换到 alarm 的回调处执行。当执行完毕后,在 sigreturn 中再切换回来。

有两个关键点:

  1. usertrap 中处理时钟中断的地方,需要判断当前进程是否注册了定时器,以及定时器是否该触发。如果该触发,则跳到定时器回调中执行。
  2. sigreturn 中,跳转到触发定时器回调之前的地方执行。

问题:如何跳转到定时器回调,如何跳转回去?

trapframe 中保存了用户进程上下文,每次触发中断后,用户上下文都会保存在 proc->trapframe 里面。trapframe->epc 用来记录用户进程中断前该执行的下一条指令。 当希望跳转到定时器回调函数时,可以拷贝 proc->trapframe,并修改 trapframe->epc 为回调的地址。在 sigreturn 中可以恢复原来的 trapframe

实现

struct proc 中新增字段保存在定时器信息:

struct proc {
   // ...
   struct file *ofile[NOFILE];  // Open files
   struct inode *cwd;           // Current directory
   char name[16];               // Process name (debugging)
+
+  uint64 alarm_handler;   // 定时器回调
+  uint64 alarm_interval_ticks; // 回调周期
+  uint64 alarm_passed_ticks;   // 已经经过的时钟数
+  struct trapframe alarm_saved_trapframe; // 用于保存原 trapframe
 };

sigalarm 只需要保存用户传来的信息到 proc 结构体中:

uint64 sys_sigalarm(void) {
  int ticks;
  uint64 handler;
  argint(0, &ticks);
  argaddr(1, &handler);

  struct proc * p = myproc();
  p->alarm_interval_ticks = ticks;
  p->alarm_passed_ticks = 0;
  p->alarm_handler = handler;
  return 0;
}

usertrap 中处理时钟中断的地方处理定时器:

void
usertrap(void) {
   // ...

   // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarm_interval_ticks) {
      p->alarm_passed_ticks++;
      if (p->alarm_passed_ticks == p->alarm_interval_ticks) {
        memmove(&p->alarm_saved_trapframe, p->trapframe, sizeof(*p->trapframe));
        p->trapframe->epc = p->alarm_handler;
      }
    }
    yield();
  }

  // ...
}

如果 p->alarm_interval_ticks 不为 0,则说明该进程注册了定时器回调,可以累加计数器,当计数器达到设定定时周期后,可以将当前的 trapframe 保存下来,然后修改 trapframe->epc,指向定时器回调函数,当退出中断服务函数后,就会跳转到 trapframe->epc 处执行,即开始执行定时器的回调。

sys_sigreturn 中,将定时器的计数器清零,等待下一轮超时,并将在 usertrap 中保存下来的 trapframe 恢复回来,这样当 sigreturn 返回后,就会跳转到触发定时器之前的为止继续执行。

uint64 sys_sigreturn(void) {
  struct proc * p = myproc();
  p->alarm_passed_ticks = 0;  // 将计数器清零
  // 恢复原 trapframe
  memmove(p->trapframe, &p->alarm_saved_trapframe, sizeof(*p->trapframe));
  return p->trapframe->a0;
}

这里 sys_sigreturn 使用 p->trapframe->a0 作为返回值,起初我返回的是 0,但是通过不了下面这个测试:

void
test3()
{
  uint64 a0;

  sigalarm(1, dummy_handler);
  printf("test3 start\n");

  asm volatile("lui a5, 0");
  asm volatile("addi a0, a5, 0xac" : : : "a0");
  for(int i = 0; i < 500000000; i++)
    ;
  asm volatile("mv %0, a0" : "=r" (a0) );

  if(a0 != 0xac)
    printf("test3 failed: register a0 changed\n");
  else
    printf("test3 passed\n");
}

这是说 a0 寄存器的值被定时器回调修改了,在定时器触发前后,a0 寄存器的值应该保持不变。

因为定时器的回调函数是在用户态调用的,最后调用 sigreturn 回到内核态, sigreturn 执行完以后,会再次回到用户态,系统调用执行后的结果是会通过 a0 传递给用户的,见下面 syscall 的实现:

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    // Use num to lookup the system call function for num, call it,
    // and store its return value in p->trapframe->a0
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

但这里我们不希望定时器回调函数改变 a0 寄存器,但 sigreturn 的返回值确实会被赋值给 trapframe->a0,而此时的 trapframe 恰记录的是触发定时器回调之前的上下文。 因此,可以在 sigreturn 中返回 trapframe->a0 的值。

下面是上面介绍的几个重点函数的执行流程:

完整变更

diff --git a/Makefile b/Makefile
index 473a471..f6c327b 100644
--- a/Makefile
+++ b/Makefile
@@ -188,6 +188,7 @@ UPROGS=\
 	$U/_grind\
 	$U/_wc\
 	$U/_zombie\
+	$U/_alarmtest\
 
 
 
diff --git a/kernel/defs.h b/kernel/defs.h
index a3c962b..3f91f3b 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..81a40ca 100644
--- a/kernel/printf.c
+++ b/kernel/printf.c
@@ -123,6 +123,7 @@ panic(char *s)
   printf(s);
   printf("\n");
   panicked = 1; // freeze uart output from other CPUs
+  backtrace();
   for(;;)
     ;
 }
@@ -133,3 +134,21 @@ printfinit(void)
   initlock(&pr.lock, "pr");
   pr.locking = 1;
 }
+
+static inline uint64
+r_fp()
+{
+  uint64 x;
+  asm volatile("mv %0, s0" : "=r" (x) );
+  return x;
+}
+
+void backtrace() {
+  uint64 fp = r_fp();
+  uint64 base = PGROUNDDOWN(fp);
+  while (fp > base) {
+    uint64 ra = *(uint64*)(fp - 8);
+    fp = *(uint64*)(fp - 16);
+    printf("%p\n", (void*)ra);
+  }
+}
\ No newline at end of file
diff --git a/kernel/proc.c b/kernel/proc.c
index 58a8a0b..00d1f32 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -124,6 +124,9 @@ allocproc(void)
 found:
   p->pid = allocpid();
   p->state = USED;
+  p->alarm_handler = 0;
+  p->alarm_passed_ticks = 0;
+  p->alarm_interval_ticks = 0;
 
   // Allocate a trapframe page.
   if((p->trapframe = (struct trapframe *)kalloc()) == 0){
diff --git a/kernel/proc.h b/kernel/proc.h
index d021857..e9b0e0c 100644
--- a/kernel/proc.h
+++ b/kernel/proc.h
@@ -104,4 +104,9 @@ struct proc {
   struct file *ofile[NOFILE];  // Open files
   struct inode *cwd;           // Current directory
   char name[16];               // Process name (debugging)
+
+  uint64 alarm_handler;
+  uint64 alarm_interval_ticks;
+  uint64 alarm_passed_ticks;
+  struct trapframe alarm_saved_trapframe;
 };
diff --git a/kernel/syscall.c b/kernel/syscall.c
index ed65409..91b69ac 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..67ca3a4 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 3b4d5bd..55ed65d 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -67,6 +67,9 @@ sys_sleep(void)
     sleep(&ticks, &tickslock);
   }
   release(&tickslock);
+
+  backtrace();
+
   return 0;
 }
 
@@ -91,3 +94,23 @@ sys_uptime(void)
   release(&tickslock);
   return xticks;
 }
+
+uint64 sys_sigalarm(void) {
+  int ticks;
+  uint64 handler;
+  argint(0, &ticks);
+  argaddr(1, &handler);
+
+  struct proc * p = myproc();
+  p->alarm_interval_ticks = ticks;
+  p->alarm_passed_ticks = 0;
+  p->alarm_handler = handler;
+  return 0;
+}
+
+uint64 sys_sigreturn(void) {
+  struct proc * p = myproc();
+  p->alarm_passed_ticks = 0;
+  memmove(p->trapframe, &p->alarm_saved_trapframe, sizeof(*p->trapframe));
+  return p->trapframe->a0;
+}
\ No newline at end of file
diff --git a/kernel/trap.c b/kernel/trap.c
index 512c850..60f1180 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c
@@ -77,8 +77,16 @@ usertrap(void)
     exit(-1);
 
   // give up the CPU if this is a timer interrupt.
-  if(which_dev == 2)
+  if(which_dev == 2) {
+    if (p->alarm_interval_ticks) {
+      p->alarm_passed_ticks++;
+      if (p->alarm_passed_ticks == p->alarm_interval_ticks) {
+        memmove(&p->alarm_saved_trapframe, p->trapframe, sizeof(*p->trapframe));
+        p->trapframe->epc = p->alarm_handler;
+      }
+    }
     yield();
+  }
 
   usertrapret();
 }
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");

评论 (评论内容仅博主可见,不会公开显示)