不落辰

知不可乎骤得,托遗响于悲风

0%

操作系统-xv6-lab4-trap

  • part 2 : 利用栈桢,打印函数调用.
  • part 3 : 为xv6增加sys_sigalarm和sys_sigreturn系统调用
    • sys_sigalarm : 注册callback以及超时时间
    • tick Interrupt : tick超时,返回user到callback
    • sys_sigreturn : user callback 结束. 通过该syscall 返回之前被tick超时打断的pc
    • 实现重点是 在tick超时时 将 trapframe 进行备份,再返回user执行callback. 当user callback结束sys_sigreturn时,再将之前的trapframe替换出来.
      • 最主要原因是trapframe里面保存了epc(user进入kernel前的指令地址),kernel返回user时一般pc返回到epc+4
      • 所以要通过备份trapframe,来保证callback结束后可以返回到user正确的pc

PART1

Read the code in call.asm for the functions g, f, and main

  • Q1 : Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
    • a0 , a1 , a2 , a3, a4, a5 , a6 , a7
    • a2 holds 13
  • Q2 : 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的调用进行了内联
    • 编译器将 main对f的调用进行了内敛。
  • Q3 :At what address is the function printf located?
    • 0x630
  • Q4 :What value is in the register ra just after the jalr to printf in main?
    • │$5 = (void (*)()) 0x38 ,jalr 指令的下一条汇编指令的地址
  • Q5 :
    • Run the following code.
    • unsigned int i = 0x00646c72; printf(“H%x Wo%s”, 57616, &i);
    • What is the output ?
      • HE110 World
    • 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?
      • i -> 0x726c6400
      • 57616不需要,因为57616的十六进制就是0xe110
  • Q6
    • printf(“x=%d y=%d”, 3);
    • what is going to be printed after ‘y=’?
      • a2寄存器里原先是啥就输出啥

  • 关于xv6的函数栈帧
    • s0 : 当前栈帧的底部指针
    • sp : 栈顶指针
      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        # sp : 栈顶指针
        # s0 : 栈帧底指针
        int g(int x) {
        0: 1141 addi sp,sp,-16 # 栈顶指针sp 下移16字节
        2: e422 sd s0,8(sp) # 存栈底指针s0/fp到sp+8的位置
        4: 0800 addi s0,sp,16 # 将sp+16即原sp的值作为新的栈帧
        return x+3;
        }
        6: 250d addiw a0,a0,3 # x = x + 3
        8: 6422 ld s0,8(sp) # 从sp+8恢复原栈帧到s0
        a: 0141 addi sp,sp,16 # 回收栈顶指针
        c: 8082 ret

PART2

Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) (see kernel/riscv.h. These number are helpful for backtrace to terminate its loop.

  • stack frame结构

    • s0 : 当前栈帧的底部指针。又称为frame pointer (fp)
    • sp : 栈的顶部指针
  • code

    • 逻辑:
        1. 通过reg s0 获取当前 frame 的 frame pointer
        1. 打印调用该过程的 函数地址return address *(frame pointer - 8)
        1. 找到上一级frame: prev frame pointer = frame pointer - 16
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          void backtrace()
          {
          // framepointer
          uint64 fp = r_fp();
          // cur_fp 当前遍历到的栈帧地址
          for(uint64 cur_fp = fp;cur_fp >= PGROUNDDOWN(fp) ; )
          {
          uint64 func = *((uint64*)(cur_fp - 8)); // 调用本级函数的 上一级函数的调用本层函数的 代码地址
          printf("%p\n",func);
          cur_fp = *((uint64*)(cur_fp-16)); // fp - 16
          }
          }
  • bug注意

    • 不是要打印每个函数stack frame的地址,因为栈帧是用来存放函数调用过程中的变量的。打印栈帧的地址只是打印了存放那些变量的内存的地址。
    • 而我们要获知的是 函数调用的过程,也即,我们要按照调用过程打印出函数代码的地址,而不是其调用过程中栈帧的地址。
    • 也即,打印return address 而非 prev frame fp

PART3

  • In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.

    • 在本练习中,您将向 xv6 添加一个功能,该功能会在进程使用 CPU 时间时定期发出警报。这对于想要限制其占用的 CPU 时间的计算绑定进程,或者对于想要计算但又希望采取一些定期操作的进程可能很有用。更一般地说,您将实现用户级中断/错误处理程序的原始形式;例如,您可以使用类似的方式来处理应用程序中的页面错误。如果解决方案通过了警报测试和用户测试,则它是正确的。
  • You should add a new sigalarm(interval, handler) system call. If an application calls sigalarm(n, fn), then after every n “ ticks “ of CPU time that the program consumes, the kernel should cause application function fn to be called. When fn returns, the application should resume where it left off. A tick is a fairly arbitrary unit of time in xv6, determined by how often a hardware timer generates interrupts. If an application calls sigalarm(0, 0), the kernel should stop generating periodic alarm calls.

    • 您应该添加新的 sigalarm(间隔、处理程序)系统调用。如果一个应用程序调用sigalarm(n,fn),那么在程序消耗每n个“滴答”的CPU时间之后,内核应该导致应用程序函数fn被调用。当 fn 返回时,应用程序应从中断的位置继续。在 xv6 中,滴答是一个相当任意的时间单位,由硬件计时器生成中断的频率决定。如果应用程序调用 sigalarm(0, 0),内核应停止生成周期性警报调用。
  • You’ll find a file user/alarmtest.c in your xv6 repository. Add it to the Makefile. It won’t compile correctly until you’ve added sigalarm and sigreturn system calls (see below).

    • 您可以在 xv6 存储库中找到一个文件 user/alarmtest.c。将其添加到生成文件。在添加 sigalarm 和 sigreturn 系统调用之前,它不会正确编译(见下文)。
  • alarmtest calls sigalarm(2, periodic) in test0 to ask the kernel to force a call to periodic() every 2 ticks , and then spins for a while. You can see the assembly code for alarmtest in user/alarmtest.asm, which may be handy for debugging. Your solution is correct when alarmtest produces output like this and usertests also runs correctly:

    • alarmtest 在 test0 中调用 sigAlarm (2, periodic) 来要求内核每 2 个即时报价强制调用 periodic(),然后旋转一段时间。您可以在user/alarmtest.asm中看到alarmtest的汇编代码,这对于调试可能很方便。当 alarmtest 产生如下输出并且用户测试也正常运行时,您的解决方案是正确的:

bug

  • bug1

    • 企图在alarm到时的时候,在kernel里面调用之前user注册到proc中的alarm_handler
    • 结果访问了不可访问呢的区域,panic kerneltrap.
    • 原因:user注册到proc结构体当中的callback:alarm_handler,其注册的函数地址是user pagetable维护的虚拟地址(在本例中是0x0),而非kernel pagetable维护的虚拟地址。callback是在user process的text段而非kernel process的地址空间里。因此crash。

  • bug2

    • code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      错误做法code
      if(p->ticks_passed >= p->ticks_interval)
      {
      // 替换trapframe
      p->back_trapframe = p->trapframe;
      p->trapframe = kalloc(); 即便kalloc了。trampoline使用的也不是这里新kalloc的trapframe。不过kernel的C code 倒是会使用这个新kalloc的trapframe。因此,这妥妥的bug。crash。panic。
      assert(p->trapframe!=0,"kalloc trapframe");
      *(p->trapframe) = *(p->back_trapframe);
      p->trapframe->epc = (uint64) p->alarm_handler;
      ...
      }

      正确code
      if(p->ticks_passed >= p->ticks_interval && p->is_in_cb!=1)
      {
      p->ticks_passed = 0;
      // 备份trapframe
      *(p->back_trapframe) = *(p->trapframe);
      // handler使用当前trapframe
      p->trapframe->epc = (uint64) p->alarm_handler;
      p->is_in_cb = 1;
      }
    • 擅自改变使用的trapframe地址。我一开始是每次需要使用alarm_handler的时候,就把原先的trapframe存起来,kalloc一个trapframe给alarm_handler使用,(通过改变proc中的trapframe指针指向)。这样做且不说效率,正确性上来讲就是错误的。原因见下
    • 看网上博客都说不这么做是因为效率原因,tmd,臭不要脸,人云亦云。
    • 前言:首先易知 trapframe这个结构体,在trampoline的uservec和userret汇编,以及kernel 的 C code中均被使用到。
      • 在trampoline中,是通过user pagetable维护的虚拟地址0x3ffffffe000引用的.
      • 在kernel的C code中,是通过p->trapframe引用的,其地址使用的自然是kernel pagetable维护的物理地址。如 (struct trapframe ) 0x87f62000.
      • 我们易知对于同一个user process ,kernel 的 C code 使用的user的trapframe自然也要和trampoline使用的user的trapframe保持一致,(因为是使用的应当同一个process的trapframe)也即该p->trapframe应当和trampoline通过0x3ffffffe000使用的是physical memory中的t同一个p->trapframe!否则就会crash!!
    • 接下来见错误原因:user pagetable维护的的trapframe地址是固定在0x3ffffffe000的。我们不能错误的使用一个新kalloc的trapframe。错误的认为他会被trampoline使用。
      • 首先,这个新在kernel kalloc的trapframe,只在kernel的pagetable建立了从虚拟地址到物理地址的映射,user 的pagetable甚至都不知道kernel已经已经新给他kalloc了一个trapframe。除非你在给他的userpagetable映射一下,不过挺困难+麻烦的。
      • 其次,如后文所述,我们的trampoline的uservec和userret使用的始终是0x3ffffffe00处的trapframe。如果换一个新地址的trapframe,还要再改kernel传递给trampoline的参数。
      • 最后,如果我们没改userpagetable也没修改trampoline,那么,就会出现trampoline使用一个trapframe,同时kernel的C code使用另一个trapframe的现象。
    • user pagetable维护的trapframe 0x3ffffffe000地址是什么时候需要被使用,被那里使用?
      • 答:在trampoline的uservec和userret被使用了。无论是uservec还是userret,trampoline始终使用的是user pagetable维护的0x3ffffffe000地址处的trapframe。
      • uservec : 将user trapframe的地址从$sscratch(之前由kernel保存地址到这里的)加载到$a0中来使用
      • userret : 使用$a0中保存的user trapframe地址,返回user之前,将$a0保存在$sscratch中。
    • 为什么user trapframe的地址0x3ffffffe000?trampoline中userret时$a0是如何获得该地址的?
      • 为什么trapframe的地址0x3ffffffe000:
        • 是process刚一被kernel创建并分配的时候,就指定了这一块空间是trapframe,并建立在user pagetable建立了映射。
      • userret 中 $a0如何获得该地址
        • kernel usertrapret
          1
          2
          3
          4
          5
          6
          7
          8
          9
          #define TRAPFRAME (TRAMPOLINE - PGSIZE)
          // tell trampoline.S the user page table to switch to.
          uint64 satp = MAKE_SATP(p->pagetable);
          // jump to trampoline.S at the top of memory, which
          // switches to the user page table, restores user registers,
          // and switches to user mode with sret.
          uint64 fn = TRAMPOLINE + (userret - trampoline);
          // pass user trapframe address and user page table satp
          ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);

关键问题

  • 关于为什么在调用alarm_handler时 需要新用一个trapframe , 将原先的trapframe备份起来。
    • 因为如果没有将原先的trapframe保存起来的话,我们的handler继续使用原先的trapframe,那么当我们的handler结束后,调用syscall sigreturn结束alarm_handler,那么该user的pc该返回哪里?如果我们就这样不做任何改动,那么pc就是返回到alarm_handler的最后一条指令的下一条地址。可是显然,那是个错误的地址,。我们应当返回的是当时被ticks interrupt 进而执行alarm_handler 时 被打断的指令,继续执行那个指令。
    • 因此,我们需要在执行alarm_handler执行之前保存其上下文trapframe(epc)备份起来(为了之后恢复)
      • *(p->back_trapframe) = *(p->trapframe);
    • 然后将当前trapframe提供给alarm_handler去执行。
      • p->trapframe->epc = (uint64) p->alarm_handler;
    • 最后在alarm_handler执行结束后调用sigreturn的时候,在sys_return中恢复上下文。
      • sigreturn : *(p->trapframe) = *(p->back_trapframe);

核心流程


  • sigalarm
  • alarm_handler触发
  • sigreturn 返回

疑问

  • 还有一个问题待解决:
    • handler执行需要用到原先的执行流的除了$epc的其他上下文吗?是有用还是有害?
      • 需要。有用。trapframe里的几乎都需要感觉。
    • 那么handler执行之后,改变的寄存器啥的,究竟是破坏了原先的pc流的trapframe,还是对原先的pc流有用?
      • 我第一想法是感觉handler改变的寄存器应该是对原先的pc流有用。
      • 不过从代码上看,我们在sigreturn时,直接将原先的pc流的trapframe覆盖了当前handler执行完的trapframe,似乎是handler的上下文对原先的pc流不再有意义?
        • 问题:那如果有一个变量在handler时,由于频繁的++–,一直存在reg中,但是这个变量在pc流中handler之后又会被使用。如果不管handler执行后的reg的话,这个变量的改变值不会丢失吗?
          • (我认为)会丢失,不过该syscall的设计者应当限制user的使用方式。从测试程序中可以看到,handler会强制和内存读写,而不会优化成对寄存器读写。
            1
            2
            3
            4
            5
            6
            7
            8
            62
            63 void __attribute__ ((noinline)) foo(int i, int *j) {
            64 if((i % 2500000) == 0) {
            65 write(2, ".", 1);
            66 }
            67 *j += 1;
            68 }
            69