XV6学习(6)Lab: traps

这一个实验主要是对RISC-V的汇编、栈帧结构以及陷阱进行简单的了解,难度并不大。

代码放在github上。

RISC-V assembly (easy)

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

RISC-V的函数调用过程参数优先使用寄存器传递,即a0~a7共8个寄存器。返回值可以放在a0和a1寄存器。printf的参数13保存在a2寄存器。

Q2: 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调用直接使用了结果12,而f中的函数g调用直接内联在f中了。

Q3: At what address is the function printf located?

在0x630的位置

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

值应该为0x38,即函数的返回地址。

跳转并链接指令(jal)具有双重功能。若将下一条指令PC + 4的地址保存到目标寄存器中,通常是返回地址寄存器ra,便可以用它来实现过程调用。如果使用零寄存器(x0)替换ra作为目标寄存器,则可以实现无条件跳转,因为x0不能更改。像分支一样,jal将其20位分支地址乘以2,进行符号扩展后再添加到PC上,便得到了跳转地址。

跳转和链接指令的寄存器版本(jalr)同样是多用途的。它可以调用地址是动态计算出来的函数,或者也可以实现调用返回(只需ra作为源寄存器,零寄存器(x0)作为目的寄存器)。Switch和case语句的地址跳转,也可以使用jalr指令,目的寄存器设为x0。

Q5: 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?

结果为:He110 World; 不要修改为0x726c6400; 57616不需要进行改变,编译器会进行转换。

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

应该打印出寄存器a2的值,因为printf会从a2寄存器中读取第三个参数作为y的值。

Backtrace (moderate)

实现backtrace,递归打印函数调用栈。使用r_fp获取当前栈帧地址,由于栈是由高地址向低地址增长的,因此使用PGROUNDUP获得栈底地址,之后循环打印栈帧的函数的返回地址。

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

Alarm (hard)

这一个要求添加系统调用sigalarm来实现当用户程序运行了n个ticks后,触发一次回调函数。由之前的学习可以知道,时钟中断的处理是在usertrap函数中的if(which_dev == 2)里面的。

为了实现这个功能,首先在proc结构体中添加相应字段:


struct proc {
  ...
  // these are used for sys_alarm
  int duration;                // ticks after last alarm
  int alarm;                   // alarm every n ticks
  uint64 handler;              // handler for alarm
  struct trapframe *alarm_trapframe; // register saved for alarm
};

之后实现sys_alarm函数,将相关信息填入proc中:

uint64
sys_sigalarm(void)
{
  int ticks;
  uint64 handler;
  if(argint(0, &ticks) < 0)
    return -1;
  if(argaddr(1, &handler) < 0)
    return -1;
  
  struct proc* p = myproc();
  p->alarm = ticks;
  p->handler = handler;
  p->duration = 0;
  p->alarm_trapframe = 0;
  return 0;
}

而最关键的部分是在usertrap中,当发生时钟中断时,将p->duration增加,如果p->duration == p->alarm,那么就要触发一次回调函数,而触发的方法就是将p->trapframe->epc设置为回调函数地址,当陷阱处理程序结束后就会跳转到回调函数。

而为了保证回调函数不会破坏原程序的寄存器,需要对trapframe进行保存;我这里选择的方法是通过kalloc申请一个新的trapframe结构体,然后将trapframe复制一份。

为了保证回调函数执行期间不会重复调用,就可以判断p->alarm_trapframe是否为0,不为0说明上一次的回调函数还没有调用sigreturn,即函数未结束。

  if(which_dev == 2){
    if(p->alarm != 0){
      p->duration++;
      if(p->duration == p->alarm){
        p->duration = 0;
        if(p->alarm_trapframe == 0){
          p->alarm_trapframe = kalloc();
          memmove(p->alarm_trapframe, p->trapframe, 512);
          p->trapframe->epc = p->handler;
        }else{
          yield();
        }
      }else{
        yield();
      }
    }else{
      yield();
    }
  }

最后就是sigreturn函数,这个函数要做的工作就是将之前保存的alarm_trapframe还原到trapframe中,并将alarm_trapframe释放掉。别忘了在freeproc函数中也要对p->alarm_trapframe进行判断,防止程序异常结束时该页面没有被释放。

uint64
sys_sigreturn(void)
{
  struct proc* p = myproc();
  if(p->alarm_trapframe != 0){
    memmove(p->trapframe, p->alarm_trapframe, 512);
    kfree(p->alarm_trapframe);
    p->alarm_trapframe = 0;
  }
  return 0;
}
posted @ 2021-01-06 10:37  星見遥  阅读(3293)  评论(0编辑  收藏  举报