MIT-6.S081-2020實驗(xv6-riscv64)四:traps


實驗文檔

概述

這次實驗內容比較分散,總體來說難度不是太高。

內容

Backtrace

要求在內核中對程序的調用棧進行遍歷,輸出每一級調用的返回地址。首先我們分析Riscv的棧幀結構,查看call.asm中main函數的匯編代碼:

void main(void) {
  1c:   1141                    addi    sp,sp,-16
  1e:   e406                    sd  ra,8(sp)
  20:   e022                    sd  s0,0(sp)
  22:   0800                    addi    s0,sp,16
  printf("%d %d\n", f(8)+1, 13);
  24:   4635                    li  a2,13
  26:   45b1                    li  a1,12
  28:   00000517            auipc   a0,0x0
  2c:   7c050513            addi    a0,a0,1984 # 7e8 <malloc+0xea>
  30:   00000097            auipc   ra,0x0
  34:   610080e7            jalr    1552(ra) # 640 <printf>
  exit(0);
  38:   4501                    li  a0,0
  3a:   00000097            auipc   ra,0x0
  3e:   27e080e7            jalr    638(ra) # 2b8 <exit>

棧是由高地址向低地址增長的,可見在進入函數時,會由高向低依次壓入ra寄存器的值(當前函數的返回地址)、s0寄存器的值(功能上類似x86的ebp寄存器),然后令s0寄存器指向當前函數的棧幀首部。之后再有什么局部變量sp寄存器再向下。可見我們只需要逐級取出棧中存儲的s0寄存器的值,就可以定位到每一級的棧幀頭部,就能獲得棧中存儲的ra寄存器的值了。什么時候結束呢,注意到每個進程的用戶棧只有一頁,因此對棧中任意位置調用PGROUNDUP宏就可以獲得棧底向上一個字節的位置,所以如果只要當前得到的s0寄存器大於等於剛才得到的位置就可以終止了:

void backtrace(void) {
    uint64 fp = r_fp(), base = PGROUNDUP(fp);
    printf("backtrace:\n");
    while (fp < base) {
        printf("%p\n", *((uint64*)(fp - 8)));
        fp = *((uint64*)(fp - 16));
    }
}

Alarm

要求在時鍾中斷的處理程序中調用用戶態的函數並返回,主要涉及的是兩個態的切換,不過因為相關的匯編代碼程序中已經有了,調用函數就行了。這個和我做8086匯編實驗時編寫的時鍾中斷處理程序相比有更難的地方,也有更容易的地方。難主要是這個包含不同態的切換。容易主要在這個中斷處理函數可以直接由操作系統來調用,時鍾中斷觸發的函數是內核里的代碼,由內核負責統一調度需要執行什么函數;而8086則需要自己手動修改中斷矢量表,讓其指向自己的中斷處理函數,同時還需要自己進行一些段寄存器的切換等內存管理操作,稍有不慎就系統崩潰,挺麻煩的。

回到這個實驗,添加系統調用函數的過程就不說了,和實驗二相似,sys_sigalarm函數內部主要是把一些信息保存到proc結構體里,重點是usertrap函數里時鍾中斷相關的內容,當判斷當前已注冊時鍾中斷處理函數且到達調用的時刻數時,由於時鍾中斷處理函數在用戶態,所以順着函數后面的usertrapret就能回到用戶態,但是必須保證回到用戶態時執行的是處理函數的函數頭,觀察上面系統調用相關的代碼注意到回到用戶態時執行的代碼位置是由p->trapframe->epc決定的,所以修改這個屬性就可以了。

這是test0的部分,test1和test2的部分要求在處理函數調用sys_sigreturn后要能夠回到時鍾中斷前的程序運行位置,而且回到那個位置時寄存器什么的狀態都和以前完全一樣。觀察到usertrap函數中proc結構體里的trapframe就是干這個活的,存儲中斷前的狀態,所以我就直接在proc結構體里新聲明了另一個trapframe結構體,然后進入處理函數前把trapframe屬性的值復制給新trapframe保存,調用sys_sigreturn時就把新trapframe的值送回給原trapframe屬性,就能恢復原來的狀態了:

  if(which_dev == 2) {
      if (p->ticks > 0 && p->duration > -1) {
          p->duration++;
          if (p->duration >= p->ticks) {
              p->duration = -1;
              p->state_time = *p->trapframe;
              p->trapframe->epc = p->handler;
              intr_on();
          } else yield();
      } else yield();
  }

p->ticks表示執行處理函數的間隔,p->duration表示距離上一次執行處理函數的時刻數。這里我用p->duration=-1表示當前正在執行處理函數。p->state_time就是新添加的trapframe結構體。yield函數的作用是讓出CPU(標記當前進程為RUNNABLE然后調度),有點像異步程序中執行IO時的掛起,個人感覺在執行處理函數的時候就接着運行別讓出CPU了,不過網上也看到有人一律yield的,這個應該無所謂,因為在用戶看來系統怎樣調度進程和自己沒啥關系,只要程序能正常運行就行了。

uint64 sys_sigalarm(void) {
    if(argint(0, &myproc()->ticks) < 0)
        return -1;
    if(argaddr(1, &myproc()->handler) < 0)
        return -1;
    return 0;
}

uint64 sys_sigreturn(void) {
    struct proc *p = myproc();
    p->duration = 0;
    *p->trapframe = p->state_time;
    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM