文章

MIT 6.S081 | Lab4: traps

课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html 代码地址:https://github.com/36-H/xv6-labs-2020/tree/traps

Lab4: traps

RISC-V assembly (easy)

Q: Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
A: a0-a7; 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.)
A: There is none. g(x) is inlined within f(x) and f(x) is further inlined into main()

Q: At what address is the function printf located?
A: 0x0000000000000616, main calls it with pc-relative addressing.

Q: What value is in the register ra just after the jalr to printf in main?
A: 0x0000000000000038, next line of assembly right after the jalr

Q: Run the following code.

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

What is the output?
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: "He110 World"; 0x726c6400; no, 57616 is 110 in hex regardless of endianness.

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

A: A random value depending on what codes there are right before the call.Because printf tried to read more arguments than supplied.
The second argument `3` is passed in a1, and the register for the third argument, a2, is not set to any specific value before the
call, and contains whatever there is before the call.

Backtrace(moderate)

回溯(Backtrace)通常对于调试很有用:它是一个存放于栈上用于指示错误发生位置的函数调用列表。 在kernel/printf.c中实现名为backtrace()的函数。在sys_sleep中插入一个对此函数的调用,然后运行bttest,它将会调用sys_sleep。你的输出应该如下所示:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

​ 在bttest退出qemu后。在你的终端:地址或许会稍有不同,但如果你运行addr2line -e kernel/kernel(或riscv64-unknown-elf-addr2line -e kernel/kernel),并将上面的地址剪切粘贴如下:

$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D

​ 你应该看到类似下面的输出:

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

​ 编译器向每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。你的backtrace应当使用这些帧指针来遍历栈,并在每个栈帧中打印保存的返回地址。

添加backtrace的函数声明

// kernel/defs.h
void            printf(char*, ...);
void            panic(char*) __attribute__((noreturn));
void            printfinit(void);
void            backtrace(void);

添加获取当前fp寄存器值的方法

// kernel/riscv.h

// 获取当前执行函数的帧指针 fp指向的是当前栈帧的开始地址也就是高位地址
// fp - 8 是 return Address 当前调用层应该返回到的地址
// fp -16 是 previous address 指向上一层栈帧的 fp 开始地址
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

实现backtrace函数

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

然后再sys_sleep()函数调用一次backtrace()即可。

Alarm(Hard)

YOUR JOB 在这个练习中你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。这对于那些希望限制CPU时间消耗的受计算限制的进程,或者对于那些计算的同时执行某些周期性操作的进程可能很有用。更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。例如,你可以在应用程序中使用类似的一些东西处理页面故障。如果你的解决方案通过了alarmtest和usertests就是正确的。

按下述函数添加系统调用,过程不再赘述

int             sigalarm(int ticks, void (*handler)());
int             sigreturn(void);

在线程proc定义中添加字段:

// kernel/proc.h
// Per-process state
struct proc {
  struct spinlock lock;
  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID
  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)

  int alarm_ticks;             // 总共的ticks
  int ticks;                   // ticks 计数
  void (*alarm_handler)();     // 回调函数
  struct trapframe *alarm_trapframe;  //  时钟中断时刻的 trapframe
  int handlingalarm;           // 标识是否在处理回调
};

实现sigalarmsigreturn

// kernel/sysproc.c
uint64
sys_sigalarm(void){
  int interval;
  uint64 handler;
  //转换参数
  if(argint(0, &interval) < 0)
    return -1;
  if(argaddr(1, &handler) < 0)
    return -1;

  return sigalarm(interval,(void(*)())(handler));
}

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

//kernel/trap.c

int sigalarm(int ticks, void(*handler)()) {
  // 设置 myproc 中的相关属性
  struct proc *p = myproc();
  p->alarm_ticks = ticks;
  p->alarm_handler = handler;
  p->ticks = 0;
  return 0;
}

int sigreturn() {
  // 将 trapframe 恢复到时钟中断之前的状态,恢复原本正在执行的程序流
  struct proc *p = myproc();
  *p->trapframe = *p->alarm_trapframe;
  p->handlingalarm = 0;
  return 0;
}

修改proc的初始化和释放代码

// kernel/proc.c
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
  struct proc *p;
  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;
found:
  p->pid = allocpid();
  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }
  // An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  //初始化alarm的关联参数
  if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }


  p->alarm_ticks = 0;
  p->ticks = 0;
  p->alarm_handler = 0;
  p->handlingalarm = 0;

  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;
  return p;
}

static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 0;
  p->sz = 0;
	@@ -149,6 +161,16 @@
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;

  //清除alarm的关联参数
  if(p->alarm_trapframe)
    kfree((void*)p->alarm_trapframe);
  p->alarm_trapframe = 0;
  p->alarm_ticks = 0;
  p->ticks = 0;
  p->alarm_handler = 0;
  p->handlingalarm = 0;

  p->state = UNUSED;
}

修改usertrap()

void
usertrap(void)
{
  // 设备中断类型
  int which_dev = 0;
  // 检查是否是从用户模式进入的中断
  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  // 设置中断向量表为内核的中断处理程序
  w_stvec((uint64)kernelvec);
  // 获取当前进程
  struct proc *p = myproc();
  
  // save user program counter.
  // 保存用户程序计数器(程序的当前执行地址)
  p->trapframe->epc = r_sepc();
  //scause: RISC-V在这里放置一个描述陷阱原因的数字。
  if(r_scause() == 8){
    // system call
    // 处理系统调用
    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    // 将sepc指针指向下一条指令,跳过ecall指令
    // sepc:当发生陷阱时,RISC-V会在这里保存程序计数器pc(因为pc会被stvec覆盖)。
    // sret(从陷阱返回)指令会将sepc复制到pc。
    // 内核可以写入sepc来控制sret的去向。
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    // 打开中断
    intr_on();
    // 调用系统调用处理程序
    syscall();
  } else if((which_dev = devintr()) != 0){
    //这里是在检查trap类型 如果不是设备中断,那么就是异常中断,
    //内核异常则为致命的error,所以跳至else代码块
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  // 用户中断
  if(which_dev == 2){
    //判断是否设置了时钟事件
    if(p->alarm_ticks != 0 ){
      // printf("设置了时钟事件 %d",p->alarm_ticks);
      p->ticks++;
      // printf("ticks = %d", p->ticks);
      //判断是否到达了足够的ticks
      if(p->ticks >= p->alarm_ticks){
        //判断是否在处理上一个回调
        if(!p->handlingalarm){
          p->handlingalarm = 1;

          //保存当前的trapframe;
          *p->alarm_trapframe = *p->trapframe;

          //加载回调
          p->trapframe->epc = (uint64)p->alarm_handler;

          //重置ticks
          p->ticks = 0;
        }
      }
    }
    yield();
  }
  usertrapret();
}
License:  CC BY 4.0