MIT6.S081 Lab 4 Traps

Lab 4 Traps

Backtrace(moderate)

backtrace:打印栈中错误发生位置上方的所有函数调用

编译器会在每个栈帧中放置一个栈帧指针,用于保存调用者栈帧指针的地址。您的反向跟踪应使用这些帧指针在堆栈中向上追踪,并在每个栈帧中打印保存的返回地址。

提示

  • 在 kernel/defs.h 中添加 backtrace 原型,以便在 sys_sleep 中调用 backtrace。

  • GCC 编译器会将当前执行函数的帧指针存储在寄存器 s0 中。将以下函数添加到 kernel/riscv.h:

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

    并在回溯中调用该函数来读取当前帧指针。该函数使用内联汇编读取 s0。

  • 这些讲义中有一张堆栈帧布局的图片。请注意,返回地址位于堆栈帧指针的固定偏移量(-8)处,而保存的帧指针位于帧指针的固定偏移量(-16)处。

  • xv6 为 xv6 内核中的每个堆栈分配一个 PAGE 对齐地址的页面。您可以使用 PGROUNDDOWN(fp) 和 PGROUNDUP(fp) 计算堆栈页面的顶部和底部地址(参见 kernel/riscv.h)这些数字有助于反向跟踪终止其循环。

一旦你的backtrace正常工作,就可以在 kernel/printf.c 的 panic 中调用它,这样当内核panic时,你就能看到内核的backtrace。

完成

kernel/printf.c

void backtrace(void){
  uint64* fp;
  uint64 retAddr; 
  fp = (uint64*)r_fp(); // 获取当前fp
  uint64 top = PGROUNDUP((uint64)fp);
  printf("backtrace:\n");
  while((uint64)fp < top){
    retAddr = *(fp - 1); // 获取返回地址
    printf("%p\n", retAddr);
    fp = (uint64*)*(fp - 2); // 指向前一fp
  }
}

(uint64)fp - 8 == fp - 1,fp是unit64类型的指针,fp - 1实际为fp向下移动sizeof(uint64),即8

虽然是栈指针但回过来看似乎直接定义 uint64 fp 更方便一些

结果

backtrace追踪的栈地址:

使用addr2line查看地址对应函数:

Alarm(hard)

在这个实验中,将为 xv6 添加一项功能,在进程占用 CPU 时定期发出警报。可能进程需要限制占用的CPU时间,或希望在计算的同时定期采取某些行动。更一般地说,您将实现一种原始形式的用户级中断/故障处理程序;例如,你可以使用类似的方法来处理应用程序中的页面故障。

您应该添加一个新的 sigalarm(interval, handler) 系统调用。如果应用程序调用 sigalarm(n,fn),那么程序每消耗 n 个 CPU 时间后,内核就会调用应用程序函数fn。当 fn 返回时,应用程序应继续运行。在 xv6 中,"tick "是一个相当随意的时间单位,由硬件定时器产生中断的频率决定。如果应用程序调用 sigalarm(0,0),内核就应停止产生alarm调用。

您将在 xv6 仓库中找到 user/alarmtest.c 文件。将其添加到 Makefile 中。在添加 sigalarm 和 sigreturn 系统调用(见下文)之前,该文件无法正确编译。

alarmtest 在 test0 中调用 sigalarm(2,periodic),要求内核每隔 2 个ticks强制调用 periodic(),然后旋转一段时间。您可以在 user/alarmtest.asm 中查看 alarmtest 的汇编代码,这对调试可能很方便。当 alarmtest 产生这样的输出且 usertests 也正确运行时,您的解决方案就是正确的:

完成后,您的解决方案将只有几行代码,但要做到正确可能很困难。我们将使用origin仓库中的 alarmtest.c 版本测试您的代码。你可以修改 alarmtest.c 来帮助调试,但要确保通过origin仓库中的 alarmtest 所有测试。

test0: invoke handler

首先修改内核,跳转到用户空间的警报处理程序,这将导致 test0 打印 "alarm!"。先不用担心 "alarm!"输出后会发生什么;如果程序在打印 "alarm!"后崩溃,现在也没关系。

提示

  • 您需要修改 Makefile,以便将 alarmtest.c 作为 xv6 用户程序编译。

  • 在 user/user.h 中正确的声明是

    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
    
  • 更新 user/usys.pl(生成 user/usys.S)、kernel/syscall.h 和 kernel/syscall.c,允许 alarmtest 调用 sigalarm 和 sigreturn 系统调用。

  • 目前,您的 sys_sigreturn 应该只返回 0。

  • sys_sigalarm() 应该在 proc 结构(kernel/proc.h)的新字段中存储警报间隔和处理函数的指针。

  • 您需要记录自上次调用(或下次调用前)进程的警报处理程序以来已经过去了多少个刻钟;为此,您还需要在 struct proc 中添加一个新字段。您可以在 proc.c 文件的 allocproc() 中初始化 proc 字段。

  • 每滴答一次,硬件时钟就会强制中断一次,由 kernel/trap.c 中的 usertrap() 处理。

  • 只有在有定时器中断的情况下,您才会想操作进程的闹钟滴答声;您需要类似于
    if(which_dev == 2) ...

  • 只有当进程有计时器未执行时,才调用警报函数。请注意,用户报警函数的地址可能是 0(例如,在 user/alarmtest.asm 中,periodic 位于地址 0)。

  • 您需要修改 usertrap(),以便当进程的警报间隔到期时,用户进程执行处理函数。当 RISC-V 上的陷阱返回用户空间时,用户空间代码恢复执行的指令地址由什么决定?

  • 如果告诉 qemu 只使用一个 CPU,使用 gdb 查看陷阱会更方便。make CPUS=1 qemu-gdb

  • 如果 alarmtest 打印出 "alarm!",则表示成功

完成

sys_sigalarm获取检查参数,并设置proc字段。当ticks满足要求时通过修改p->trapframe->epc来达到执行新的代码的效果

sysproc.c

uint64 sys_sigalarm(void){
  int ticks;
  uint64 hander;
  if(argint(0, &ticks) < 0) return -1;
  if(argaddr(1, &hander) < 0) return -1;
  if(ticks <= 0) return -1;
  struct proc *p = myproc();
  p->ticks = ticks;
  p->hander = hander;
  return 0;
}

uint64 sys_sigreturn(void){
  return 0;
}

trap.c/usertrap中添加定时器中断检测和对应代码

  if(which_dev == 2 && p->ticks != 0){
    p->passedticks++;
    if(p->passedticks == p->ticks){
      // execuate hander
      p->trapframe->epc = p->hander;
      p->passedticks = 0;
    }
  }

结果

test0 passed

test1/test2(): resume interrupted code

可能是 alarmtest 在打印 "alarm!"后在 test0 或 test1 中崩溃,或者是 alarmtest打印 "test1 失败",或者是 alarmtest 退出时没有打印 "test1 通过"。要解决这个问题,必须确保警报处理程序完成后,控制返回到用户程序最初被定时器中断中断的指令。必须确保寄存器内容恢复到中断发生时的值,这样用户程序才能在警报发生后不受干扰地继续运行。最后,每次警报触发后都应重置警报计数器,以便定期调用处理程序。

作为一个起点,我们为您做出了一个设计决定:用户警报处理程序在完成后必须调用 sigreturn 系统调用。请看 alarmtest.c 中的 periodic 示例。这意味着您可以在 usertrap 和 sys_sigreturn 中添加代码,使用户进程在处理完警报后正常恢复。

提示

  • 您的解决方案需要保存和恢复寄存器--您需要保存和恢复哪些寄存器才能正确恢复被中断的代码? 提示:会有很多)。
  • 当计时器关闭时,让 usertrap 在 struct proc 中保存足够的状态,以便 sigreturn 能正确返回被中断的用户代码。
  • 防止处理程序的重入调用,如果处理程序尚未返回,内核不应再次调用它。test2 测试了这一点。

通过 test0、test1 和 test2 后,运行 usertests,确保没有破坏内核的其他部分。

完成

添加标志位is_handering,中断检测中检测是否重入,sigreturn检测是否被hander进程调用

中断准备执行hander时保存trapframe,在sigreturn时恢复

uint64
sys_sigalarm(void){ // get arguments and set alarm
  int ticks;
  uint64 hander;
  if(argint(0, &ticks) < 0) return -1;
  if(argaddr(1, &hander) < 0) return -1;
  if(ticks < 0 || hander < 0) return 0;
  struct proc *p = myproc();
  if(ticks == 0 && hander == 0){ // disable
    // use ticks as flag
    p->ticks = 0; 
  }
  else if(ticks > 0){ // enable
    p->ticks = ticks;
    p->hander = hander;
  }
  else
    return -1;
  
  return 0;
}
uint64
sys_sigreturn(void){
  struct proc *p = myproc();
  if(p->is_handering == 1 && p->ticks != 0){ // is handering and enable
    // restore registers
    memmove(p->trapframe, p->savedtrapframe, sizeof(struct trapframe));
    // reset
    p->is_handering = 0;
    p->passedticks = 0;
    // back
    usertrapret();
  }
  else{
    return -1;
  }

  return 0;
}
if(which_dev == 2){
  if(p->ticks > 0 && p->is_handering == 0){ // alarm enabled and no hander not returned
    p->passedticks++;
    if(p->passedticks >= p->ticks){
      // store trapframe
      memmove(p->savedtrapframe, p->trapframe, sizeof(struct trapframe));
      // execuate hander
      p->is_handering = 1;
      p->trapframe->epc = p->hander;
    }
  }
  else{
    // give up the CPU if this is a timer interrupt.
    yield();
  }
}

结果

alarmtest通过

usertest未通过

最终发现问题出在给savedtrapframe分配空间,在freeproc里没释放

全部测试通过

posted @ 2024-12-16 11:11  zySail  阅读(97)  评论(0)    收藏  举报