MIT 6.S081 Lab4

97 阅读4分钟

Lab: Traps

Trap,中断,这一章实验主要考验中断及系统调用的理解。

涉及trap、trampoline、stack pointer、supervisor、risc-V assembly

RISC-V assembly

阅读call.asm的内容并回答相关问题:

  1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
  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.)
  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?

Here's a description of little- and big-endian and a more whimsical description.

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

翻译版本如下:

  1. 哪些寄存器包含函数的参数?例如,在 main 函数调用 printf 时,哪个寄存器保存着 13?

回答:a0-a7, a2

  1. 在主函数(main)的汇编代码中,函数 f 和 g 的调用位置在哪里?(提示:编译器可能会内联函数。)。

回答: 没有代码的位置,被一层一层内联到main里了

  1. 函数 printf 位于什么地址?

回答:0x0000000000000628

  1. main函数中jalr跳转到printf后的ra寄存器中的值是什么?

回答:0x0000000000000038,即jalr的下一条指令地址

  1. 运行以下代码。输出是什么?
unsigned int i = 0x00646c72printf (  "H%x Wo%s"  , 57616 , &i); 

这里有一个 ASCII 表,它将字节映射到字符。输出取决于 RISC-V 是小端序这一事实。如果 RISC-V 是大端序,为了得到相同的输出,你会将i设置为什么值?你需要将57616更改为不同的值吗? 这里有小端序和大端序的描述以及一个更奇特的描述

回答:输出"He110 World",不需要更改,十六进制和内存中的表示不是同一个概念

  1. 在下面的代码中,'y='后面会打印什么?(注意:答案不是一个特定的值。)为什么会这样?
printf (  "x=%d y=%d"  , 3 ); 

回答:会打印随机值,取决于寄存器a2的内容

在项目根目录添加answers-traps.txt并记录回答内容。

Backtrace

实现backtrace()打印调用栈。

在./kernel/defs.h中声明

// printf.c
void            backtrace(void);

在./kernel/riscv.h中添加fp - frame pointer的获取方法,内联汇编函数

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

然后是实现backtrace,本质非常简单,首先我们要明确调用栈的结构:

fp指向栈顶,sp指向栈底。栈空间在xv6中是由上往下扩展的,所以fp始终大于等于sp。当我们进行调用时,会往下开辟新的空间,即新的栈帧,此时sp会减少一定的字节数,fp设置成新sp的大小。

一个栈帧中从高到低第一个8字节fp-8存储return address,即函数调用返回地址,第二个8字节fp-16存储previous address,即上一个栈帧的fp地址,栈帧中还会保存一些寄存器、局部变量等信息,但是大小总是至少16个字节。

xv6中,栈大小为一个页面,那么在我们递归回退着打印调用栈时,根据栈是从上往下开辟的,我们就是在从下往上走,fp的值会逐渐增大,直到回到栈顶,也就是一个页面的顶部,使用PGROUNDUP来判断是否到达页面顶部,作为退出条件。

在./kernel/printf.c实现backtrace

backtrace(){
  uint64 fp = r_fp();
  while(fp!=PGROUNDUP(fp)){
    uint64 ra = *(uint64 *)(fp - 8);
    printf("%p\n",ra);
    fp = *(uint64 *)(fp - 16);
  }
}

在sys_sleep中调用backtrace()

uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  backtrace();
  ...
}

Make qemu编译运行,使用bttest测试有如下输出

$ bttest
0x0000000080002110
0x0000000080001fee
0x0000000080001cd8

Alarm

首先添加系统调用sigalarm和sigreturn,步骤在Lab2有描述,这里再次给出:

  1. 在user.user.h中注册
  2. 在user/usys.pl中增加系统调用存根 entry("XXXX")
  3. 在kernel/syscall.h中定义系统调用号 #define SYS_XXXXX XX
  4. 在kernel/syscall.c中使用extern定义系统调用函数,并将其调用号-调用函数指针的映射关系加入syscalls中
  5. 在kernel中实现系统调用

然后,在proc结构体中添加alarm相关字段:

  • alarm_interval:定时器时钟周期,0说明禁用
  • alarm_handler:时钟回调处理函数
  • alarm_ticks:距离下一次时钟的tick数
  • alarm_trapframe:中断处理的trapframe,用于中断处理完成后恢复程序正常执行
  • alarm_goingoff:是否有一个时钟中断正在处理,防止覆盖
// Per-process state
struct proc {
  ...
  // Alarm
  int alarm_interval;         // Alarm interval (0 - disabled)
  void(*alarm_handler)();      // Alarm handler
  int alarm_ticks;             // Ticks left for alarm
  struct trapframe * alarm_trapframe; // A copy of the trapframe before alarm
  int alarm_goingoff;          // Is an alarm going off
};

在sysproc.c中添加这两个系统调用对应的实现

uint64 sys_sigalarm(void)
{
  int n;
  uint64 fn;
  if (argint(0, &n) < 0)
  {
    return -1;
  }
  if (argaddr(1, &fn) < 0)
  {
    return -1;
  }
  return sigalarm(n, (void (*)())fn);
}

uint64 sys_sigreturn(void)
{
  return sigreturn();
}

其最终调用的2个处理函数sigalarm和sigreturn是我们新增的,首先在defs.h中添加定义

// trap.c
...
int             sigalarm(int ticks, void (*handler)());
int             sigreturn();

在trap.c中添加实现

  int sigalarm(int ticks, void (*handler)())
  {
    struct proc *p = myproc();
    p->alarm_interval = ticks;
    p->alarm_handler = handler;
    p->alarm_ticks = ticks;
    return 0;
  }

  int sigreturn()
  {
    struct proc *p = myproc();
    *p->trapframe = *p->alarm_trapframe;
    p->alarm_goingoff = 0;
    return 0;
  }

在proc.c中的allocproc和freeproc中添加初始化对应字段和释放的逻辑

static struct proc*
allocproc(void)
{
  struct proc *p;
  ...
found:
  ...

  // Allocate a trapframe page for alarm_trapframe
  if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  ...
  
  p->alarm_interval=0;
  p->alarm_handler=0;
  p->alarm_goingoff=0;
  p->alarm_ticks=0;

  return p;
}
static void
freeproc(struct proc *p)
{
  ...
  if(p->alarm_trapframe)
    kfree((void*)p->alarm_trapframe);
  p->alarm_trapframe = 0;
  ...
  p->alarm_interval=0;
  p->alarm_handler=0;
  p->alarm_goingoff=0;
  p->alarm_ticks=0;
  
  p->state = UNUSED;
}

在trap.c的usertrap函数中实现时钟的具体逻辑,在每次时钟中断的时候,若检测到该进程有设置时钟周期,alarm_interval != 0,则将其alarm_tick减小,进行时钟计时,当时钟倒计时结束alarm_tick <= 0且目前没有进行中断处理!alarm_goingoff时,保存原本的程序栈帧trapframe,通过修改epc将程序跳转到alarm_handler中处理,alarm_handler执行完后恢复trapframe回到原本的程序。

void usertrap(void)
{
  ...
  // give up the CPU if this is a timer interrupt.
  if (which_dev == 2)
  {
    if (p->alarm_interval != 0)
    {
      if (--p->alarm_ticks <= 0)
      {
        if (!p->alarm_goingoff)
        {
          p->alarm_ticks = p->alarm_interval;
          *p->alarm_trapframe = *p->trapframe;
          p->trapframe->epc = (uint64)p->alarm_handler;
          p->alarm_goingoff = 1;
        }
      }
    }
    yield();
  }
  
  usertrapret();
}

测试程序是alarmtest,添加到Makefile中

UPROGS=\
    ...
    $U/_alarmtest\

编译运行:

$ alarmtest
test0 start
...................................................................................................alarm!
test0 passed
test1 start
...alarm!
....alarm!
...alarm!
...alarm!
...alarm!
...alarm!
...alarm!
...alarm!
...alarm!
....alarm!
test1 passed
test2 start
................................................................alarm!
test2 passed

实验完成,make grade验证:

== Test answers-traps.txt == answers-traps.txt: OK 
== Test backtrace test == 
$ make qemu-gdb
backtrace test: OK (6.2s) 
== Test running alarmtest == 
$ make qemu-gdb
(3.2s) 
== Test   alarmtest: test0 == 
  alarmtest: test0: OK 
== Test   alarmtest: test1 == 
  alarmtest: test1: OK 
== Test   alarmtest: test2 == 
  alarmtest: test2: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (140.4s) 
== Test time == 
time: FAIL 
    Cannot read time.txt
Score: 84/85