Skip to content

Latest commit

 

History

History
305 lines (225 loc) · 9.83 KB

Lab4-traps.md

File metadata and controls

305 lines (225 loc) · 9.83 KB

Lab4: traps

Lab4为函数调用以及陷入相关的实验

RISC-V assembly (easy)

该部分为一系列RISC-V汇编相关的问题,阅读user/call.asm对应的call.asm汇编文件,回答下面问题:

  1. 哪些寄存器包含函数的参数,例如,对于main调用printf函数,哪个寄存器存参数13?

对于RISC-V,前8个参数会放置在a0-a7寄存器,a2放置参数13,如下代码所示:

void main(void) {
  1c:	1101                	addi	sp,sp,-32
  1e:	ec06                	sd	ra,24(sp)
  20:	e822                	sd	s0,16(sp)
  22:	1000                	addi	s0,sp,32
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	7f850513          	addi	a0,a0,2040 # 820 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0
  34:	648080e7          	jalr	1608(ra) # 678 <printf>
  1. main汇编中哪里调用了函数fg

由上述代码可以看到,main中直接得到了12,并放到了a1寄存器,可见编译器进行了优化,直接得到了结果

  1. printf的地址在哪?

由汇编文件可以看到printf的地址为0x630,当做完alarm后,该汇编文件会发生变化,printf的地址也会变化

  1. 当要进入mainprintf函数,执行jalr指令后ra寄存器的值是多少?

ra应为函数调用中断点出的地址,也即jalr下一条指令的地址,为0x38

  1. 运行下列代码:
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

输出依赖于RISC-V是小端系统,如果RISC-V是大端,i应如何设置得到相同结果,是否需要改变57616的值?

小端即低字节放置在低地址,输出为:"HE110 World",如果为大端i为0x726c6400,57616不需要改变

  1. 下列代码:
printf("x=%d y=%d", 3);

y=将要输出什么,为什么会这样?

输出结果为x=3,但y是一个不确定的值,实际可能为a2寄存器的值

Backtrace (moderate)

任务

kernel/printf.c实现backtrace()函数,用于打印函数调用过程,在sys_sleep中插入该函数,之后运行测试

过程
  1. 内联汇编读取s0寄存器,即fp栈指针的值

代码如下所示,fp(s0)寄存器用于保存当前函数栈帧的首地址:

static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}
  1. 栈帧结构

如下所示,fp寄存器为当前栈帧的首地址,fp-8为上级函数的返回地址,fp-16为上级栈帧的首地址,一直沿着上级栈帧的地址,可以打印出整个栈的调用过程

Stack
                   .
                   .
      +->          .
      |   +-----------------+   |
      |   | return address  |   |
      |   |   previous fp ------+
      |   | saved registers |
      |   | local variables |
      |   |       ...       | <-+
      |   +-----------------+   |
      |   | return address  |   |
      +------ previous fp   |   |
          | saved registers |   |
          | local variables |   |
      +-> |       ...       |   |
      |   +-----------------+   |
      |   | return address  |   |
      |   |   previous fp ------+
      |   | saved registers |
      |   | local variables |
      |   |       ...       | <-+
      |   +-----------------+   |
      |   | return address  |   |
      +------ previous fp   |   |
          | saved registers |   |
          | local variables |   |
  $fp --> |       ...       |   |
          +-----------------+   |
          | return address  |   |
          |   previous fp ------+
          | saved registers |
  $sp --> | local variables |
          +-----------------+

  1. backtrace函数

不断通过fp = fp-16获取栈帧地址,对于每个栈帧,打印上级函数的返回地址

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

Alarm (hard)

任务

该节你将为xv6添加对于进程使用CPU时间时能够周期性地发出警报的功能,这对于计算密集型进程限制使用CPU时间或者进程希望周期性地执行某个动作很有用;更进一步,你将实现一个初级形式的用户态中断/故障处理程序,和处理应用中的页错误类似

首先添加sigalarm(interval, handler)系统调用,如果程序调用sigalarm(n, fn),则程序每消耗n个ticks,内核调用程序的fn函数,当fn返回,程序应该在之前中断的地方恢复执行;一个tick是xv6的一个计时单元,由硬件时钟生成中断;如果一个应用调用sigalarm(0, 0),内核应停止周期性地执行alarm调用

过程
  1. 添加sys_sigalarmsys_sigreturn两个系统调用定义

按之前lab添加系统调用的方式即可

  1. proc.h/struct proc添加alarm相关的成员变量

如下所示,alarm_ticks为alarm的周期,alarm_handler_addr为alarm处理函数的地址,该地址为用户进程的虚拟地址,这两个由sys_sigalarm系统调用参数设置;ticks为当前进程消耗的CPU时间,last_ticks为上一次执行alarm处理函数的开始CPU时间,alarm_regs为执行处理函数时保存与需要恢复的寄存器组值,alarm_running用来标记是否该进程正在执行处理函数中

  int alarm_ticks;                   // lab alarm
  uint64 alarm_handler_addr;         // lab alarm
  uint64 ticks;                      // lab alarm
  uint64 last_ticks;                 // lab alarm
  struct alarm_regs regs;            // lab alarm
  int alarm_running;                 // lab alarm
  1. 初始化proc结构体alarm相关变量

proc.c/allocproc函数中对上述定义的相关变量进行初始化,初始ticks为0

  // Init ticks for lab alarm
  p->ticks = 0;
  p->last_ticks = 0;
  p->alarm_running = 0;
  1. 添加sys_sigalarm系统调用实现

sys_sigalarm对进程proc结构体的alarm_ticksalarm_handler_addr变量进行设置,同时设置last_ticks为当前ticks,也即从当前开始计时

uint64 
sys_sigalarm(void)
{
  int ticks;
  uint64 handler_addr;

  if (argint(0, &ticks) < 0 || argaddr(1, &handler_addr) < 0)
    return -1;

  struct proc *p = myproc();
  p->alarm_ticks = ticks;
  p->alarm_handler_addr = handler_addr;
  p->last_ticks = p->ticks;

  return 0;
}
  1. 保存与恢复上下文

当进程从当前运行地方切换到处理函数入口地址时,应保存切换时的CPU寄存器值,这里在proc.h定义struct alarm_regs结构体,其需要保存的寄存器基本和trapframe一致

struct alarm_regs
{
  uint64 epc;
  uint64 ra;
  uint64 sp;
  .....
  uint64 s10;
  uint64 t5;
  uint64 t6;
};

当执行sys_sigalarm系统调用进入内核后,当前进程用户态的上下文,也即执行ecall指令时的状态,保存在进程结构体的trapframe中,由于sys_sigalarm系统调用返回后强制使得该进程跳转到了处理函数去执行,执行完成后在通过sys_sigreturn系统调用恢复,在这个处理函数执行过程中trapframe已经发生了很大变化,因此需要保证sys_sigalarm进入时的trapframesys_sigreturn返回时的trapframe一致,即可恢复到执行处理函数之前的位置继续执行

trap.c中定义保存与恢复上下文如下所示:

void
save_regs(struct proc *p)
{
  p->regs.epc = p->trapframe->epc;
  p->regs.ra = p->trapframe->ra;
  p->regs.sp = p->trapframe->sp;
  .........
  p->regs.t3 = p->trapframe->t3;
  p->regs.t4 = p->trapframe->t4;
  p->regs.t5 = p->trapframe->t5;
  p->regs.t6 = p->trapframe->t6;
}

void restore_regs(struct proc *p)
{
  p->trapframe->epc = p->regs.epc;
  p->trapframe->ra = p->regs.ra;
  p->trapframe->sp = p->regs.sp;
  p->trapframe->gp = p->regs.gp;
  .......
  p->trapframe->t5 = p->regs.t5;
  p->trapframe->t6 = p->regs.t6;
}
  1. 处理时钟中断

每当进程因为时钟中断陷入后,进程的CPU时间ticks增加,当alarm处理程序未在运行,并且设置了alarm周期时间,则当到期后就开始执行处理函数

首先p->last_ticks = p->ticks设置最后调用处理函数的开始时间为当前ticks

save_regs(p)保存了将要调用处理函数之前的CPU寄存器状态

p->trapframe->epc = p->alarm_handler_addr将陷入后的返回地址设置为处理函数的地址,当trampoline.S/userret最后的sret指令执行后,即将PC设置为了处理函数地址,也即执行该处理函数

p->alarm_running = 1表示该进程的处理函数正在执行

  // give up the CPU if this is a timer interrupt.
  // lab alarm
  if(which_dev == 2) {
    p->ticks++;
    if (p->alarm_ticks != 0 && p->alarm_running == 0) {
      if (p->last_ticks + p->alarm_ticks <= p->ticks) {
        p->last_ticks = p->ticks;

        save_regs(p);
        p->trapframe->epc = p->alarm_handler_addr;
        
        p->alarm_running = 1;
      }
    }
  1. 添加sys_sigreturn系统调用实现

sysproc.c中添加sys_sigreturn实现,主要作用是用户的alarm处理函数执行后,恢复到处理函数执行前的状态

uint64 
sys_sigreturn(void)
{
  struct proc *p = myproc();
  restore_regs(p);
  p->alarm_running = 0;

  return 0;
}
运行结果

image-20210402142321233

实验测试

image-20210402142613027

代码

https://github.com/whileskies/xv6-labs-2020/tree/traps