Lab4: traps

RISC-V assembly

  • Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
    • a0-a7 registers contain arguments to functions. Specifically, a2 holds 13 in main’s call to printf.
    • 原型代码
    #include "kernel/param.h"
    #include "kernel/types.h"
    #include "kernel/stat.h"
    #include "user/user.h"
    
    int g(int x) {
      return x+3;
    }
    
    int f(int x) {
      return g(x);
    }
    
    void main(void) {
      printf("%d %d\n", f(8)+1, 13);
      exit(0);
    }
    
    • call.asm中找到调用printf的地方,可以看到它的三个参数分别被放到了a0-a2寄存器
      • image.png
  • 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.)
    • The assembler code of main does not call the f and g functions. The compiler optimizes it.
    • 计算一下就会发现f(8)+1就是12,这里的汇编代码并没有展示调用函数的压栈操作,而是直接内联展开了函数的调用(编译器优化)
  • At what address is the function printf located?
    • 0x642
    • image.png|475
    • 在汇编代码中直接就有函数的地址,不过这个值应该不是固定的,我最开始做的时候是0x642,如果make clean后重新make fs.img有可能地址会发生改变
  • What value is in the register ra just after the jalr to printf in main?
    • 0x38
    • image.png|475
    • 执行jalr前的PC是0x34,执行后PC+4得到0x38
  • What is the output?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? unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, &i);
    • He110 World i=0x726c6400 No
    • hello部分很好解释,57616=0xE110
    • 注意到RISC-V是小端序的,因此i值每两位转化为一个char的话就会变为rld'\0'
    • 若改成大端序,就需要把i的值改成大端;57616的打印与大小端无关,因此不用改变
  • In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?
    • x=3 y=1130405448
    • We put one argument to the register(actually we need two), but printf still reads value from a2 register, so the function takes a random value from that register as its argument.
    • 这里的y值是不确定的。函数传参时,会把参数依此压入从a0开始的寄存器,这里的printf本来需要三个参数,也就是用到a0-a2寄存器,但只给了两个参数,因此这里输出的a2是不确定的值

Backtrace

  • 背景知识
    • 每调用一个函数,函数都会为自己创建一个Stack Frame
    • 栈是从高地址开始向低地址使用。所以栈总是向下增长(下图中上面是高地址,下面是低地址)
    • SP(Stack Pointer)指向Stack的底部并代表了当前Stack Frame的位置,FP(Frame Pointer)指向当前Stack Frame的顶部
    • FP-8是返回地址,FP-16是上一个stack frame的fp地址
    • image.png|250
  • 基本思想:使用帧指针(FP)遍历这个栈并打印出每个栈帧保存的return address,这样就可以看出调用链,便于我们调试
  • 根据hint,我们在kernel/riscv.h中加入下面的代码获取当前的fp
// kernel/riscv.h
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}
  • 同样根据hint,使用PGROUNDDOWN(fp) 来获取页末的fp
  • 这样fp起始和终止的位置就都知道了,于是可以像遍历链表一样完成遍历及打印
void 
backtrace(void)
{
  for (uint64* fp = (uint64*)r_fp(), *bottom = (uint64*)PGROUNDDOWN((uint64)fp);
      fp > bottom ; fp = (uint64*)fp[-2])
    printf("%p\n", fp[-1]); 
}

Alarm

  • 背景知识
    • 系统调用与中断处理
      • 进入内核空间,保存用户寄存器到进程陷阱帧
      • 陷阱处理过程
      • 恢复用户寄存器,返回用户空间
        • 但此时返回的并不是进入陷阱时的程序地址,而是处理函数handler的地址,而handler可能会改变用户寄存器
  • 如何保存和恢复上下文
    • 使用中间变量保存寄存器值?定时器中断发生时,内核需要将控制权转交给handler。为此,内核会修改 epc(预期程序计数器)的值,使其指向 handler 的地址。这是唯一一个我们明确知道会被覆盖的值
    • 然而,handler 在执行过程中也可能触发trap。如果这种情况发生,当前的 trapframe(包含寄存器状态的数据结构)可能会被新的中断或异常覆盖,导致原始状态丢失
    • 为了避免这种情况,内核需要在调用 handler 之前,完整地保存整个 trapframe 的内容到一个安全的地方。这样,无论 handler 在执行过程中触发了多少次中断,原始的寄存器状态都可以被保留下来
  • 如何避免 handler 的重复调用
    • handler 可能需要一段时间来执行。在这个时间段内,如果再次发生定时器中断,理论上应该再次调用 handler。但是,如果 handler 正在执行,我们不希望它被重复调用,因为这会导致它占用所有的CPU时间,从而影响系统的整体性能
    • 为了解决这个问题,内核可以在进程的状态中设置一个标记(比如 alarm_on),用来指示 handler 是否正在执行。当 handler 开始执行时,内核将这个标记设置为1。在每次中断处理时,内核首先检查这个标记:
      • 如果标记表明 handler 正在执行,内核将跳过对 handler 的调用,避免重复执行
      • 一旦 handler 执行完毕,它应该通过调用 sigreturn 系统调用来清除这个标记,并恢复原始的寄存器状态
  • Makefile user.h usys.pl syscall.h syscall.c中添加调用、声明等,不再赘述
  • usertrap:中断处理
// trap.c
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
// void usertrap(void)

// give up the CPU if this is a timer interrupt.
  if (which_dev == 2) {
    if (!p->alarm_on && p->alarm_ticks && ++p->alarm_past == p->alarm_ticks) 
    {
      p->alarm_on = 1;
      *p->pre_trapframe = *p->trapframe;
      p->alarm_past = 0;
      p->trapframe->epc = (uint64)p->alarm_handler;
    }
    yield();
  }
  • sys_sigalarm:接收用户程序提供的警报时间间隔和处理程序地址,并将它们存储在当前进程的控制块中。这样,当定时器中断发生时,内核可以使用这些信息来确定是否需要触发警报,并调用相应的处理程序
uint64
sys_sigalarm(void)
{
  struct proc *p = myproc();       // 获取当前进程结构体
  argint(0, &p->alarm_ticks);      // 从用户空间获取警报时间间隔并存储到proc
  argaddr(1, (uint64*)&p->alarm_handler);  // 获取处理程序的地址,并存储到p->alarm_handler
  p->alarm_past = 0;               // 重置alarm_past
  return 0;
}
  • sys_sigreturn:恢复用户程序执行前的状态,并从信号处理中返回。重置与警报相关的进程状态,确保警报处理程序不会重复执行,并在处理程序执行完毕后正确地恢复用户程序的状态
    • 函数的返回值会被保存在寄存器a0、a1中,而这些系统调用,包括sys_sigreturn都是有返回值的,这就会导致进行了系统调用后a0的值会被污染,因此我们直接返回原来的a0值即可
uint64
sys_sigreturn(void)
{
  struct proc *p = myproc();
  if (p->alarm_on)
  {
    *p->trapframe = *p->pre_trapframe;  // 恢复陷阱帧
    p->alarm_past = 0;
    p->alarm_on = 0;
  }
  return p->trapframe->a0;
}
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值