Home MIT 6.s081 Lab traps
Post
Cancel

MIT 6.s081 Lab traps


Lab: Traps (mit.edu)

RISC-V assembly

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

在main函数中,对printf的调用如下:

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

  24: 4635                 li a2,13

  26: 45b1                 li a1,12

  28: 00000517             auipc a0,0x0

  2c: 7b050513             addi  a0,a0,1968 # 7d8 <malloc+0xea>

  30: 00000097             auipc ra,0x0

  34: 600080e7             jalr  1536(ra) # 630 <printf>

可以看到通过li指令将13加载到了a2寄存器,将12加载到了a1寄存器,而12这里是在编译时编译器计算出了f(8)+1的值。在设置了a1和a2这两个寄存器的值后,执行了auipc这条指令。 auipc rd, immediate的作用是PC加立即数,也就是把符号位扩展的高20位左移12位加到PC上,结果写入到rd中。 auipc a0, 0x0的作用是将pc的值写入到a0寄存器中,然后addi a0, a0, 1968,使a0中的值变为0x7d8。 auipc ra, 0x0,取pc到ra寄存器,该指令对应的pc为0x30,然后通过jalr 1536(ra)进行无条件跳转,即跳转到0x30+0x600=0x640的位置,开始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.)

并没有调用,f和g两个函数都被内联了

3.At what address is the function printf located?

0x640

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

jalr指令跳转时,ra的值为0x30,而jalr将其下一条指令的值存入ra寄存器,因此跳转后ra寄存器的值为0x38

5. Run the following code.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?

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

输出为HE110 World. 57616对应的十六进制数位E110,RISC-V是小端的,也就是说i在内存从低位到高位的字节依次是72 6c 64 00,printf中参数%s将i作为字符串输出,72转换成ASCII码为r,6c为l,64为d.如果是大端法,i应该设置为0x726c6400,57616不需要修改。

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

根据汇编代码,printf将a2寄存器的值作为第三个参数,也就是说a2寄存器的值是什么,y就等于什么。

Backtrace

在kernel/printf.c中实现backtrace()函数,输出函数调用的堆栈信息。 编译器在每个函数栈帧中保存了caller的函数栈帧地址,因此backtrace可以利用这些栈帧指针不断回溯caller的栈帧。 当前正在执行的函数栈帧指针保存在s0寄存器中,可以通过r_fp函数读取:

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

函数栈帧的布局: Pasted image 20230405090620.png 从上图可以看出,函数调用栈向下增长,sp指针保存的是当前函数栈的栈顶,fp保存的是当前栈的栈底。当前函数的返回地址保存在fp-8位置处,而指向caller的fp的指针保存在fp-16位置处。xv6为每个栈分配一个页,因此可以使用PGROUNDUP获得当前栈的最高地址,backtrace的完整代码如下:

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

Alarm

添加一个sigalarm(interval, handler)系统调用,当应用调用signalarm(n, fn)时,每经过n个CPU中断,内核调用一次fn。当fn返回时,要求能够恢复到应用原本的上下文继续执行。

test0: invoke handler

按照MIT 6.s081 Lab system calls | clingfei中类似的过程,首先在user/user.h中添加系统调用sigalarm和sigreturn:

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

并在syscall.c和syscall.h中添加sys_signalarm和sys_return的声明。 根据hint,目前sigreturn只需要返回0:

uint64 sys_sigreturn(void) {
	return 0;
}

sys_sigalarm()需要在proc结构体中保存alarm interval和指向handler function的指针,分别对应系统调用参数中的interval和handler:

uint64 sys_sigalarm(void) {
    struct proc* p = myproc();
    if (argint(0, &p->interval) < 0 || argaddr(1, &p->handler) < 0)
      return -1;
    // printf("interval: %d, handler: %p", p->interval, p->handler);
	// p->ticks_since_last = 0;
    return 0;
}

在proc结构体中添加与alarm有关的信息,其中interval和handler用于保存系统调用参数,而ticks_since_last用于记录距离上次调用handler已经发生了几次时钟中断(在allocproc时置0):

struct proc {
	......
	int interval;        
	uint64 handler;
	uint64 ticks_since_last;
};

对于每次时钟中断,在usertrap函数中被处理,其中时钟中断对应的是设备中断,根据dev_intr的定义,当时钟中断发生时返回值为2,因此当which_dev为2时,使ticks_since_last+1,并判断ticks_since_last是否等于interval,设置p->trapframe->epc为p->handler,其中p->handler中保存的是系统调用时传递的参数地址,在usertrapret返回到用户态时,从trapframe中取出epc装入pc,从而执行handler:

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();
  
  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.
    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){
    // ok
    // only manipulate a process's alarm ticks if there is a timer interrupt
    if (which_dev == 2) {
        p->ticks_since_last++;
        if (p->ticks_since_last == p->interval) {
          p->trapframe->epc = p->handler;
        }
    }
  } 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)
    yield();

  usertrapret();
}

运行alarmtest,成功通过test0

test1/test2(): resume interrupted code

上述代码能够调用handler,但是无法回到用户程序执行系统调用时的上下文继续执行。alarmtest.c中的periodic,也就是上文所说的handler的定义如下:

void
periodic()
{
  count = count + 1;
  printf("alarm!\n");
  sigreturn();
}

也就是说,在handler中会调用sigreturn函数,用于返回系统调用时的上下文。 实验指导中给出了提示:

  • Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).

随之产生了一个疑问,在系统调用时uservec已经保存了所有的寄存器到trapframe中,返回时userret也从trapframe中恢复了寄存器,为什么这里依然说需要保存和恢复寄存器?

仔细观察usertrap中的代码,可以看到在发生时钟中断,usertrap进入时,通过p->trapframe->epc = r_sepc();保存了sepc寄存器到trapframe中,此时的pc指向的是时钟中断时正在执行的代码,但是对于时钟中断,在调用handler时,使用handler的指针覆盖了epc,从而导致之前保存的epc丢失。然后执行yield进行进程调度,当再次切换到该进程执行时,首先通过usertrapret和userret返回到用户态,此时除了sepc寄存器指向handler外,其他的寄存器都与发生时钟中断时一致,但是handler执行完,再进行系统调用sigreturn,此时一方面寄存器的值被handler改变,另一方面之前的epc也被覆盖。

为了能够在sigreturn中恢复到时钟中断时的执行现场,需要在uservec之外,再对寄存器做一次备份。在proc中添加一个trapframe类型的指针,称为trapframecopy。对于时钟中断,在usertrap中修改epc为handler之前,首先保存当前的trapframe到trapframecopy, 由于trapframe只占用了前280个字节,因此直接将trapframe页的后一半交给trapframecopy:

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();
  
  if(r_scause() == 8){
    // system call
	//......
  } else if((which_dev = devintr()) != 0){
    // ok
    // only manipulate a process's alarm ticks if there is a timer interrupt
    if (which_dev == 2) {
        p->ticks_since_last++;
        if (p->ticks_since_last == p->interval) {
          p->trapframecopy = p->trapframe + 512;
          memmove(p->trapframecopy, p->trapframe, sizeof(struct trapframe));
          p->trapframe->epc = p->handler;
        }
    }
  } else {
	//......
  }

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

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

在sys_sigreturn中,从trapframecopy拷贝回trapframe:

uint64 sys_sigreturn(void) {
    struct proc *p = myproc();
    if (p->trapframecopy != 0) {
      memmove(p->trapframe, p->trapframecopy, sizeof(struct trapframe));
    }
    p->ticks_since_last = 0;
    p->trapframecopy = 0;
    return 0;
}

为了防止在handler返回前内核再次调用handler,在sys_sigreturn返回前将ticks_since_last置0,这样即使再次发生时钟中断,由于当前执行时ticks_since_last已经等于interval,因此只会使ticks_since_last不断+1,而不会再次设置epc为handler,从而防止再次调用。

完成上述修改后,成功通过alarmtest和usertests。

This post is licensed under CC BY 4.0 by the author.