当前位置 : 主页 > 编程语言 > 其它开发 >

MIT6.S081-Lab4 Traps

来源:互联网 收集:自由互联 发布时间:2022-05-30
开始日期: 22.4.7 操作系统: Ubuntu20.0.4 Link:Lab Traps 个人博客:Memory Dot 目录 Lab Traps 写在前面 vscode+wsl2+unbuntu20.04 只使用 gdb-multiarch 进入qemu-gdb 参考链接 实验内容 RISC-V assembly Backtrace

开始日期:22.4.7

操作系统:Ubuntu20.0.4

Link:Lab Traps

个人博客:Memory Dot

目录
  • Lab Traps
    • 写在前面
      • vscode+wsl2+unbuntu20.04
      • 只使用gdb-multiarch进入qemu-gdb
      • 参考链接
    • 实验内容
      • RISC-V assembly
      • Backtrace
      • Alarm
        • test0: invoke handlers
        • test1/test2(): resume interrupted code
    • 总结

Lab Traps 写在前面 vscode+wsl2+unbuntu20.04
  • 由于使用虚拟机,可能是分配的内存不够,遇到了内存泄露,故更换为vscode+wsl2+unbuntu20.04

  • 参考链接:

    Windows10开发环境搭建(2) | Terminal和VS Code

    WSL2 安装教程

  • 由于笔者mircosoft store无法使用,要自己下载windows terminal 和 ubuntu20.04

  • 成功了

只使用gdb-multiarch进入qemu-gdb
  • 此方案是在学习群一位群友提出的,事实上,按之前的方式,输入gdb-multiarch kernel/kernel之后就有「提示」了

    For help, type "help".
    Type "apropos word" to search for commands related to "word".
    warning: File "/home/duile/xv6-labs-2021/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
    To enable execution of this file add
            add-auto-load-safe-path /home/duile/xv6-labs-2021/.gdbinit
    line to your configuration file "/home/duile/.gdbinit".
    To completely disable this security protection add
            set auto-load safe-path /
    line to your configuration file "/home/duile/.gdbinit".
    
  • 使用第一个.gitinit文件使得/目录安全,该文件位于/home/user/.gitinit
    该文件要「自己手动创建」,文件内容只有一句,如下

    set auto-load safe-path /
    
  • /路径安全之后,就可以安全地执行第二个位于/home/user/xv6-labs-2021/.gdbinit的文件,文件内容在git clone的时候已经有了。

    set confirm off
    set architecture riscv:rv64
    target remote 127.0.0.1:26000
    symbol-file kernel/kernel
    set disassemble-next-line auto
    set riscv use-compressed-breakpoints yes
    
    • 可以注意到,语句target remote 127.0.0.1:26000,笔者之前就使用过了,这里的操作就是让Ubuntu系统自动执行语句。
  • 那么即可改为如下命令,即可进入qemu-gdb

  • one windows
    $ make CUPS=1 qemu-gdb
    another windows
    $ gdb-multiarch
    

    效果如下:

参考链接
  • Lab4: Traps
实验内容 RISC-V assembly
  • 以下是笔者的txt文件,仅供参考

    1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
    
    main()  : a0 = pc value or ret value a1 = 12, a2 = 13
    f()     : a0 = ret value
    g()     : a0 = ret value
    
    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.)
    
    2.1  no call f()(and no call g())
         make inline optimization that a1 = 12
    2.2  the compiler inline func (look at line:32)
       26 int f(int x) {
       27    e:   1141                    addi    sp,sp,-16
       28   10:   e422                    sd  s0,8(sp)
       29   12:   0800                    addi    s0,sp,16
       30   return g(x);
       31 }
       32   14:   250d                    addiw   a0,a0,3
       33   16:   6422                    ld  s0,8(sp)
       34   18:   0141                    addi    sp,sp,16
       35   1a:   8082                    ret
    
    3. At what address is the function printf located?
       line:630
    
    4. What value is in the register 'ra' just after the 'jalr' to 'printf' in 'main'?
       ra = pc+4 = 0x34 + 0x4 = 0x38
    
    5. Run the following code.
    
    	unsigned int i = 0x00646c72;
    	printf("H%x Wo%s", 57616, &i);
    
    What is the output? Here's an ASCII table that maps bytes to characters.
    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?
    
    5.1
    I run this process in win10, because ubuntu has warning(win10 also use little endian and I make some changes)
    
    #include<stdio.h>
    int main(){
    	unsigned int i = 0x00646c72;
    	printf("H%x Wo%s \n", 57616, &i);
        printf("H%x Wo%x \n", 57616, i);
    }
    
    output:
    He110 World
    He110 Wo646c72
    
    5.2
    in ASCII: 0x72->r, 0x6c->l, 0x64->d
    if RISC-V is big endian, we must change i (i = 0x00646c72 => i = 0x00726c64)
    because we would the value of fisrt addr is 0x72 in big endian
    
    No change 57616
    because we only use it hexadecimal value
    
    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);
    
    I also run this process in win10 and I also make some changes
    
    #include<stdio.h>
    int main(){
        printf("x=%d y=%d \n");
        printf("x=%d y=%d \n", 3);
    }
    
    output:
    x=11364944 y=11356192
    x=3 y=434198848
    
    before run this process,
    x belong to a register that value is 11364944
    y belong to another register that value is 434198848
    
    before run this process,
    x belong to a register that value be changed to 3
    y belong to another register that value also is 434198848
    
  • 笔者重点讲一下第4题,首先代码:

    30: 00000097            auipc ra,0x0
    34: 610080e7            jalr  1552(ra) # 640 <printf>
    
  • 然后是参考文件:riscv-spec.pdf

    P36

    AUIPC (add upper immediate to pc) uses the same opcode as RV32I. AUIPC is used to buildpc-relative addresses and uses the U-type format. AUIPC forms a 32-bit offset from the U-immediate,filling in the lowest 12 bits with zeros, sign-extends the result to 64 bits, adds it to the address of the AUIPC instruction, then places the result in register rd.

    P37

    Note that the set of address offsets that can be formed by pairing LUI with LD, AUIPC with JALR

  • 由此得知,auipc的作用是「把高位立即数加到pc上」即执行ra <- PC += (imm << 12)
    看第一行代码30: 00000097 => 30: 0000000000000000 00001 0010111,那么有
    pc = 0x30imm = 0x0rd = 00001 -> raopcode = 001011 -> auipc
    (这些操作或者寄存器的「对应代码」,也可以在riscv-spec.pdf查到,我这里就不列出来了)
    从而ra <- 0x30 += (0x0 << 12)得出ra = 0x30

    P21

    The indirect jump instruction JALR (jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1, then settingthe least-significant bit of the result to zero. The address of the instruction following the jump(pc+4) is written to register rd. Register x0 can be used as the destination if the result is not required.

  • 由此得知,jalr的作用是「跳转到偏移地址并将下一条pc指令存储到寄存器当中」
    即执行jump to address: base + offest ra <- pc + 0x4
    看第二行代码,34: 00000097 => 30: 011000010000 00001 000 00001 1100111,那么有
    pc = 0x34offest = 1552 = 0x610rs1 = rd = 00001 -> raopcode = 001011 -> jalr
    在第一行代码,我们已经算出ra = 0x30
    从而jump to address: ra + offest = 0x30 + 0x610 = 0x640
    ra <- pc + 0x4 = 0x34 + 0x4 = 0x38

  • 综上,在跳转到printf函数后,ra = 0x38

Backtrace
  • 题意:在一个程序栈的多个帧中实现返回地址的「回溯」,在这个过程中,把这些返回地址打印出来

  • 参考hints即可写出,是回溯时,这个循环的结束条件是什么?

    • hint提示到:一个stack的大小是一个页面即4kb,从而得知,这个页面上下限也是固定的地址。那么无论栈指针fp地址被改变成多少,靠fp计算出的PGROUNDUP(fp)PGROUNDDOWN(fp)都是不变的,笔者将其打印出来是:

      void
      backtrace(void)
      {
        uint64 fp = r_fp();
      	printf("%p, %p\n", PGROUNDUP(fp), PGROUNDDOWN(fp));
      }
      
      output:
      0x3fffff9000 0x3fffffa000
      

      从而,得出循环条件: while (PGROUNDUP(fp) == 0x3fffffa000 && PGROUNDDOWN(fp) == 0x3fffff9000),结合PGSIZE也可以写成while ((PGROUNDUP(fp) - PGROUNDDOWN(fp)) == PGSIZE)

    • 取出fp的值时,要先转为指针,再解除指针拿到里面的值,「因为fp本身是一个地址,不能直接用」,参考代码如下:

      void
      backtrace(void)
      {
        uint64 fp = r_fp();
        printf("backtrace:\n");
        uint64 ret_addr;
        while ((PGROUNDUP(fp) - PGROUNDDOWN(fp)) == PGSIZE){
          ret_addr = *((uint64*)(fp-8));
          printf("%p\n", ret_addr);
          fp = *((uint64*)(fp-16));
        }
      }
      
  • hints的其它内容别忘了

Alarm
  • 题意:CPU的每经历一个tick就会触发一次timer interrupt,我们需要安装一个sigalarm(n, fn)(原题意是调用,笔者认为理解为安装更好),使得xv6在nticks之后就能在CPU触发的timer interrupt中调用一次fn,这个fnhandler

    void
    test0()
    {
      int i;
      printf("test0 start\n");
      count = 0;
      /* install sigalrm */
      sigalarm(2, periodic);
      /* wait timer interrupt after 2 ticks */
      for(i = 0; i < 1000*500000; i++){
        if((i % 1000000) == 0)
          write(2, ".", 1);
        if(count > 0)
          break;
      }
      /* unstall sigalrm */
      sigalarm(0, 0);
      if(count > 0){
        printf("test0 passed\n");
      } else {
        printf("\ntest0 failed: the kernel never called the alarm handler\n");
      }
    }
    
  • 按照hints一步步来

test0: invoke handlers
  • 实现调用一次handler,也就是调用一次periodic(),因为可以看到test0只输出了一个alarm!

    test0 start
    ........alarm!
    test0 passed
    
  • 在实现调用一次handler之前,我们先回顾一下一次syscall是怎么发生的:

    • 硬件执行一些行为,做准备 [CPU]
    • 汇编指令准备[vector]
    • 在C代码中处理trap[handler]
    • 返回原来的mode(kernel/mode)
  • 而在这段过程中,需要思考它的program count: PC 是怎么变化,我们假设是一次syscall,如下

    When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?

    • ecall: sret是硬件指令,会将SEPC设置为PC,即SEPC <- PC <- ecallSEPC此时为ecall

    • userver: no operation about any PC

    • usertrap: p->trapfram->epc = r_sepc();,将SEPC传给epcepc此时为ecall

    • usertrap: p->trapframe->epc += 4;+4指向下一条指令,epc此时为ret

      .global sigalarm
      sigalarm:
       li a7, SYS_sigalarm
       ecall
       ret
      
    • usertrapret: w_sepc(p->trapframe->epc);,将epc传给SEPCSEPC此时为ret

    • userret: sret是硬件指令,可以将PC设置为SEPCPC此时为ret

      # return to user mode and user pc.
      # usertrapret() set up sstatus and sepc.
      sret
      
    • 在用户态执行PCret

  • 回到test0,我们需要满足,nticks之后,在CPU触发的timer interrupt中调用一次handler,参照上面的PC变化,以及提示说要修改usertrap,不难想出:在usertrap中添加 p->trapfram->epc = handler;,使得最终处于用户态会去执行handler函数。这条语句当然需要写在 if(which_dev == 2)之下,因为这是timer interrupt

  • 完成前五个hints

  • proc.h添加new field,passticks是为了配合ticks满足nticks之后,才调用一次handler

      // these are private to the process, so p->lock need not be held.
      int ticks;                   //ticks of alarm
      void (*handler)();           //handler function
      int passticks;               //ticks from the last handler to the current handler
    
  • 记得在allocproc,freeproc中分配,释放相关内容。

    allocproc()
    ...
    found:
      p->passticks = 0;
      p->ticks = 0;
      p->handler = 0;
    ...
    
    freeproc()
    ...
    p->passticks = 0;
    p->ticks = 0;
    p->handler = 0;
    ...
    
  • 编写sys_sigalarm,获得入参ticks(void*)handlerhandler通过argaddr()传进来时是类型是uint64,要转型为(void*)

    uint64
    sys_sigalarm(void)
    { 
      int ticks;
      uint64 handler;
      if(argint(0, &ticks) < 0)
        return -1; 
      if(argaddr(1, &handler) < 0)
        return -1;
      struct proc *p = myproc();
      p->ticks = ticks;
      p->handler = (void*)handler;
      return 0;
    }
    
  • 修改usertrap()满足nticks之后,才调用一次handler

    • 一定要注意,我们这是在timer interrupt
      // give up the CPU if this is a timer interrupt.
      if(which_dev == 2){
        p->passticks = p->ticks;
        while(p->ticks){
          p->ticks--;
          if(p->ticks == 0)
            p->trapframe->epc = (uint64)p->handler;
        }
        p->ticks = p->passticks;
        yield();
      }
    
test1/test2(): resume interrupted code
  • 先考虑test1,CPU触发timer interrupt之后,调用了一次handlerperiodic()

    void
    periodic()
    {
      count = count + 1;
      printf("alarm!\n");
      sigreturn();
    }
    
  • 它会接着执行一次syscallsigreturn(),还是一样:

    • 硬件执行一些行为,做准备 [CPU]
    • 汇编指令准备[vector]
    • 在C代码中处理trap[handler]
    • 返回user mode
  • 等到它回归user mode时,它的trapfram是属于periodic()的,但我们希望返回user mode时,它的trapfram是属于timer interrupt的。
    那我们就需要在调用periodic()之前先保存好timer_trapfram,在sys_sigreturn()返回user mode之前用timer_trapfram替换periodic()trapfram即可

  • 同时,为了满足test2Prevent re-entrant calls to the handler----if a handler hasn't returned yet, the kernel shouldn't call it again. 我们引入一个标志:handler_execute

  • proc.h添加new field

      // these are private to the process, so p->lock need not be held.
      int ticks;                   //ticks of alarm
      void (*handler)();           //handler function
      int passticks;               //ticks from the last handler to the current handler
      struct trapframe *timer_trapframe; // saves registers to resume in sigret 
      int handler_execute;         // handler executing  => 1, handler no executing => 0
    
  • 记得在allocproc,freeproc中分配,释放相关内容。

    allocproc()
    ...
    found:
      p->passticks = 0;
      p->ticks = 0;
      p->handler = 0;
      p->handler_execute = 0;
    
      // Allocate a timer_trapframe page.
      if((p->timer_trapframe = (struct trapframe *)kalloc()) == 0){
        freeproc(p);
        release(&p->lock);
        return 0;
      }
    ...
    
    freeproc()
    ...
    p->passticks = 0;
    p->ticks = 0;
    p->handler = 0;
    if(p->timer_trapframe)
      kfree((void*)p->timer_trapframe);
    p->timer_trapframe = 0;
    p->handler_execute = 0; 
    ...
    
  • 修改usertrap(),在调用periodic()之前先保存好timer_trapfram,同时满足periodic()的执行没有结束之前,不能调用它

    • 注意不要使用p->timer_trapframe = p->trapframe,它只能获得地址,而不是内容
      // give up the CPU if this is a timer interrupt.
      if(which_dev == 2){
        p->passticks = p->ticks;
        while(p->ticks){
          p->ticks--;
          if(p->ticks == 0 && p->handler_execute == 0){
            // we can't do it beacese we only get address of trapframe but not get content
            // p->timer_trapframe = p->trapframe; 
            memmove(p->timer_trapframe, p->trapframe , sizeof(struct trapframe));
            p->handler_execute = 1;
            p->trapframe->epc = (uint64)p->handler;
          }
        }
        p->ticks = p->passticks;
        yield();
      }
    
  • 修改usertrap(),在sys_sigreturn()返回user mode之前用timer_trapfram替换periodic()trapfram,同时,此时已经执行periodic()可以视作执行结束,要将handler_execute置为0

    int
    sys_sigreturn(void)
    {
      struct proc *p = myproc();
      memmove(p->trapframe, p->timer_trapframe, sizeof(struct trapframe));
      p->handler_execute = 0;
      return 0;
    }
    
  • 成果图

总结
  • 结束日期:22.4.10
  • 本次实验在过程中,没有很好地把已学知识联系起来,尤其是在PC变化那一块,我是知道的,但没有完全联系起来,当然我虽然知道,但确实没有很清晰,写完实验才弄清楚了
  • trapfram的地址和内容是有区别,传递时要注意,到底是一个页面内容,还是这个页面的地址
  • proc的新字段,在allocproc,freeproc中都需要处理
  • 最近在听【A-SOUL/向晚】顶碗先生
上一篇:C++篇:第十四章_编程_知识点大全
下一篇:没有了
网友评论