4. Lab: traps


1. RISC-V assembly

1.1 要求

It will be important to understand a bit of RISC-V assembly, which you were exposed to in 6.004. There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program in user/call.asm.
Read the code in call.asm for the functions g, f, and main. The instruction manual for RISC-V is on the reference page. Here are some questions that you should answer (store the answers in a file answers-traps.txt):

阅读 call.ccall.asm

  • call.c
#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) {
  unsigned int i = 0x00726c64;
	printf("H%x Wo%s", 57616, &i);

  printf("%d %d\n", f(8)+1, 13);

1.2 实现

根据 call.ccall.asm 回答如下问题
1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
a0 ~ a7 register, a2 register

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.)
line 45 ~ 46, function f and function g all be inline by compiler

3. At what address is the function printf located?

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

5. 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?
a. output : HE110 World
b. i order : 0x00726c64
c. 57616 no need to change, because it printf by %x , not %s , %s print every character.

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);**
it will print a2 register value

2. Backtrace

2.1 要求

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:
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:
$ addr2line -e kernel/kernel
You should see something like this:

实现 backtrace,打印函数调用堆栈地址,可以通过 addr2line 将该地址转换成具体的代码位置。

2.2 分析


  • 获取当前函数的 caller 大致位置

当前栈帧的起止大致为 fp 寄存器到 sp 寄存器,每次调用新的函数时,会先将当前的 fp 寄存器压入栈中,然后将当前 sp 保存到 fp 寄存器,作为新函数的 fp。此外,调用函数时,call 指令会把执行函数完后的下一条指令地址,即 return address 压入到栈中,因此只需要获取该指令地址就可以知道 caller 的大致位置。

  • 获取整个堆栈

由于栈帧的格式是统一的,故 fp 寄存器到 return address 的偏移固定为 -8(因为当前为 64 位系统),获取整个堆栈只需要递归获取 fp 寄存器,依次根据偏移获取 return address 即可。目前用户栈的大小分配为 4kb ,即一页,因此可以通过 sp & (PGSIZE - 1) 来获取栈顶,当 fp >= stack_top 时,停止递归

2.3 实现

  uint64 fp = r_fp();
  uint64 stack_top = PGROUNDUP(fp);
  while (fp < stack_top)
    uint64 ret_addr = *(uint64*)(fp - 8);
    fp = *(uint64*)(fp - 16);
    printf("%p\n", ret_addr);

3. Alarm

3.1 要求

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.

3.2 分析

该实验可分为 2 部分

3.2.1 test0: invoke handler

int sigalarm(int ticks, void (*handler)());
假如执行 sigalarm(2, test); ,则表示每过 2 个 tick,执行一次 test 函数,这里一次 tick 表示执行了一次内核时钟中断。
这里的问题在于,如何执行 test 函数,触发条件很容易实现,通过在调用 sigalarm 的时候,设置初始值,每次时间中断统计当前执行过了多少个 tick 即可。
由于判断逻辑的条件在中断当中,中断返回时,需要设置 epc 寄存器,确定中断返回后要执行的代码。故可以修改该寄存器,将值改为 handler 的地址即可。

3.2.2 test1/test2(): resume interrupted code

第二部分的要求主要是为了弥补第一部分的实现导致的问题,直接强行设置 epc 寄存器为 handler,会导致执行 handler 时的上下文寄存器等资源实际上是中断发生时的上下文,当执行完 handler 时,当前的堆栈是异常的。
为了保证执行完 handler 之后,能正常返回到中断时的代码,该实验提供了一个额外的机制,即 handler 执行完毕时,需要执行 sigreturn 系统调用。
此时的问题就在于,如何利用 sigreturn ,将上下文环境恢复到满足执行 handler 的时钟中断的时候。这里需要了解中断的上下文是如何保存的,只需关注用户态中断,在发生用户态中断时,会进入中断入口 uservec,这里会将上下文所有寄存器保存到 struct proc.trapframe 中,退出中断时,执行 usertrapret,将 struct proc.trapframe 的数据恢复到寄存器中。
因此,此时问题简单化为,在触发满足执行 handler 的条件的时候(此时处于时钟中断),将当前被保存的 trapframe 备份一份,然后退出中断时会跳转到 handler,然后执行完 handler 时,会执行 sigreturn,此时又会触发中断,再将之前备份的 trapframe 覆盖回去即可。

3.3 实现

  • 中断相关实现
void handle_alarm(){
  struct proc *p = myproc();
  if (p->alarm_passed_ticks > p->alarm_ticks)
  if (p->alarm_ticks == p->alarm_passed_ticks){
    memmove(p->alarm_trapframe, p->trapframe, PGSIZE);
    p->trapframe->epc = p->alarm_handler;

void usertrap(void)
    // some code ...
    // give up the CPU if this is a timer interrupt.
    if(which_dev == 2){
        if (p->alarm_ticks != 0){
        // ... some code 
  • 系统调用
uint64 sys_sigalarm(void)
  uint64 handler;
  int alarm_ticks;

  if(argint(0, &alarm_ticks) < 0)
    return -1;
  if(argaddr(1, &handler) < 0)
    return -1;

  struct proc* p = myproc();
  p->alarm_ticks = alarm_ticks;
  if (alarm_ticks == 0)
    return 0;
  p->alarm_handler = handler;
  p->alarm_passed_ticks = 0;
  return 0;

uint64 sys_sigreturn(void)
  struct proc* p = myproc();
  memmove(p->trapframe, p->alarm_trapframe, PGSIZE);
  p->alarm_passed_ticks = 0;
  return 0;
posted @ 2022-04-05 09:52  lawliet9  阅读(12)  评论(0编辑  收藏  举报