1. 程式人生 > 其它 >MIT6.S081-Lab4 Traps

MIT6.S081-Lab4 Traps

開始日期:22.4.7

作業系統:Ubuntu20.0.4

Link:Lab Traps

目錄

Lab Traps

寫在前面

vscode+wsl2+unbuntu20.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
    

    效果如下:

參考連結

實驗內容

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>
    
  • 然後是參考檔案:

    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
    (這些操作或者暫存器的「對應程式碼」,也可以在檔案查到,我這裡就不列出來了)
    從而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/向晚】頂碗先生