MIT 6.S081 2021: Lab traps


RISC-V assembly

Which registers contain arguments to functions? For example, which register holds 13 in main's call to  printf?

第一道題目是RISC-V匯編的相關內容。課程網頁上reference里給了一些參考資料,我搜了一下第一本The RISC-V Reader,沒想到還有中文版,建議看一下第三章:The RISC-V Reader: An Open Architecture Atlas (riscvbook.com)

下圖是RISC-V各寄存器的用途:

 


 

函數各參數顯然是在a0-a7中傳遞的,再看一下printf附近的匯編代碼,根據"li a2,13"顯然可知是a2。

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(8)+1的值直接計算出來傳入printf中。

At what address is the function  printf located?

 

 

這個auipc是把0x0左移12位,再加上PC值0x30存到ra里。jalr是先根據ra算出跳轉地址,再把現在的PC+4存入ra,作為稍后printf的返回地址。所以printf的位置是0x30+1552=0x640。(后面注釋也寫了)注意這個值是會變的,不同的環境可能不一樣。

What value is in the register  ra just after the  jalr to  printf in  main?

把運行到jalr處的PC+4存入ra,也就是0x38。

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.

打印結果是HE110 World。第一個(57616)2=(E110)16 這個顯然。i=110 0100 0110 1100 0111 0010,在內存里每個字節還要反過來存儲。因此直接查詢ascii表就行了。

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);

結果是x=3 y=1。y的值取決於printf第三個參數,a2寄存器的值。為了驗證,我們在0x38處下斷點:

 

 

運行到斷點處,執行一下info reg看看寄存器內容,$a2=1:

 

Backtrace

在kernel/printf.c里實現函數backtrace(),要求是打印棧里存儲的frame pointer。Hints里提示了risc-v棧幀的結構,很明顯是一個鏈表結構,因此我們需要做的事情就只是遍歷鏈表。

void backtrace(void)
{
  struct proc* p=myproc();
  printf("backtrace:\n");
  uint64 fp=r_fp();
  while(1)
  {
    fp=fp-16;//入棧的上一級frame pointer
      uint64 ret=*((uint64*)(fp+8));        
      fp=*((uint64*)fp);//訪問這個地址
    if(PGROUNDUP(fp) != p->kstack+PGSIZE)
    {
      break;
    }
    printf("%p\n",ret);//打印一下返回的PC值
  }
​
}

被調用者的棧幀保存的frame pointer存儲在距離棧底16B的位置,占8B空間(記住棧的空間是從高到低擴展的),指向調用者的棧底。因此,首先把r_fp()得到的寄存器值減去16,得到frame pointer的地址;然后讀出存儲在距離棧底8B的return address 也就是fp+8。

這里用了一點技巧,先把fp+8轉換成uint64類型的指針,然后再使用*運算符把fp+8里面存儲的值讀出來。用相同的方法訪問fp得到上一級被調用者棧幀的開頭,如果fp所處的頁表最上端已經不是p->kstack指向的頁表最上端,說明fp已經脫離了內核分配給它的棧幀,循環終止。

多說一句:對於一個C結構體或者C++類

typedef struct e
{
    int x;
    int y;
    int a;
    int b;
    int c;
    int d;
}Entity;

想計算某一個成員相對結構體開頭的偏移量,怎么辦?以b為例:

printf("%d",&(((Entity*)0)->b));

先把0強制轉換成Entity*類型的指針,也就是欺騙編譯器,告訴它:有一個從地址0開始的Entity類,並使用&運算符計算這個類中b的地址,就可以得到結果12。

 

Alarm

這個實驗的要求還是比較復雜的,做之前一定要看一下視頻

先梳理一下用戶進程進入內核態的過程;

1.調用ecall,ecall負責把進程從user mode提升到kernel mode,把PC當前值存進sepc里,把stvec寄存器里的地址復制到PC

2.stvec里是trampoline.S中uservec的地址,CPU執行下面的指令,切換用戶頁表到內核頁表,保存各寄存器到tramframe,從trapframe中恢復kernel stack地址等參數,跳轉到usertrap()

3.usertrap執行系統調用或者處理中斷,調用usertrapret()。如果執行系統調用的話,就將p->trapframe->epc加上4,這樣稍后返回用戶態的時候就會從調用trap的下一條指令開始執行。

4.usertrapret存儲kernel相關信息,跳轉到trampoline.S中的userret執行下面的匯編代碼

5.userret恢復寄存器和頁表,最終返回用戶態。

 

搞清楚系統調用是怎么進行的之后,我們來看程序要求。要求實現一個sigalarm函數,傳入一個函數指針handler,每過若干次時鍾中斷就執行一次handler。根據提示,我們用if(which_dev == 2)來檢測時鍾中斷。需要注意的是,程序傳入的是用戶地址空間中的函數指針,是不能在內核中執行的,需要返回到用戶態中執行。所以,當需要執行handler時,應該做的事情是把函數指針賦給p->trapframe->epc,這樣返回用戶態的時候CPU就會從handler的位置執行指令,而不是試圖在usertrap()等地方調用函數指針。有了這個基本思路就可以寫代碼了。先給struct proc添加幾項:

struct proc {
  struct spinlock lock;
​
  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID
​
  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process
​
  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct trapframe *alarm_save; // 保存調用sigalarm之前的寄存器的原始數據
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
  int alarm_int;               //tick時間間隔
  uint64 handler;              //sigalarm()需要處理的handler
  int tick_passed;             //上一次執行handler之后過去的時間
  int flag;                    //是否正在調用handler
};

在allocproc()里初始化:

found:
  p->pid = allocpid();
  p->state = USED;
  p->tick_passed=0;
  p->alarm_int=-1;
  p->handler=0;
  p->flag=0;
​
  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
​
  //分配alarm_save
  if((p->alarm_save = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

freeproc():

if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->alarm_save)//記得釋放alarm_save
    kfree((void*)p->alarm_save);
  p->alarm_save = 0;

修改后的usertrap:

void
usertrap(void)
{
  int which_dev = 0;
​
  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");
​
  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);
​
  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call
​
    if(p->killed)
      exit(-1);
​
    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;
​
    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();
​
    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
    //intr_on();
    if(which_dev == 2&&p->alarm_int!=0)
    {
      p->tick_passed+=1;
      if(p->tick_passed==p->alarm_int)
      {
        if(p->flag==0)
        {
          memmove(p->alarm_save,p->trapframe,PGSIZE);
          p->trapframe->epc=p->handler;
          p->flag=1;//只有sigreturn可以將它置為0
        }
        p->tick_passed=0;
      }
    }
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }
​
  if(p->killed)
    exit(-1);
​
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
  {
    yield();
  }
   
​
  usertrapret();
}

兩個系統調用:

uint64 sys_sigalarm(void)
{
  struct proc* p=myproc();
  //獲取參數
  int ticks;
  uint64 handler_ptr;
  argint(0,&ticks);
  argaddr(1,&handler_ptr);
  //傳入參數
  p->alarm_int=ticks;
  p->handler=handler_ptr;
​
  return 0;
}
​
uint64 sys_sigreturn(void)
{
  
  struct proc* p=myproc();
  p->flag=0;//記得p->flag重置為0
  memmove(p->trapframe,p->alarm_save,PGSIZE);
  return 0;
}

 

代碼說明:

1.alarm_int是指定的時間間隔,在proc中初始置為-1,tick_passed初始置為0,所以不調用sigalarm就永遠不會執行下面memmove等操作。sys_sigalarm()唯一的作用是把時間間隔和函數指針存入struct proc之中。

2.如果alarm_int==0,則不執行任何操作。

3.如何保證執行完handler之后回到原來trap的位置? 在proc中設置新參數struct trapframe *alarm_save。初始化進程時給這個指針分配一頁內存。在修改epc到handler之前,把整個trapframe復制到alarm_save指向的頁。(當然這個頁只能在kernel mode中訪問)

4.系統調用sigreturn()的作用就是:handler完成后,再進入usertrap,復制這個alarm_save到trapframe中,這樣稍后再執行usertrapret()時,trapframe和剛執行sigalarm()時的trapframe就完全相同了。

5.如何保證執行handler時不再次執行sigalarm():在proc中聲明一個變量flag,初始化為0。只有flag==0時才允許執行修改epc和memmove的操作,執行完之后flag置1,只有sigreturn才有權限把它重新置為0。這樣,如果在handler執行時遇到時鍾中斷,當tick_passed等於alarm_int時就不會執行handler操作,但是仍然會執行重置或累加tick_passed的操作。


免責聲明!

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



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