iT邦幫忙

2022 iThome 鐵人賽

DAY 17
0

前言

在前面我們看過了整個 trap 的流程,而在今天我們將以 exec() 這個 System call 來追蹤實際 trap 發生的情況。

exec() System call

exec()這個 System call 為例子,當我們在 user mode 底下使用了 exec() 這個 System call,首先會有一些參數的移動,如下。

# Initial process that execs /init.
# This code runs in user space.

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

可以看到一些參數會移動到 a0a1 暫存器,而 System call 對應的代號會存放在 a7 暫存器中,通過 syscall.h 可以得知 exec() 對應到的代號為 7,接著開始了整個 trap 的流程,首先執行 ecall (在 xv6 Traps (user mode) overview, ecall 提及)。

接著,uservec 會執行 usertrap(),在 usertrap() 中會通過 scause CSR 去判斷發生 trap 的原因,這裡發生 trap 的原因為 System call,接著 usertrap() 會去執行對應的 System call,通過 syscall() 這個函式。

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    // Use num to lookup the system call function for num, call it,
    // and store its return value in p->trapframe->a0
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

可以看到通過了 syscalls 這個陣列我們循找到對應的 System call,我們可以看到 syscalls 陣列的內容

static uint64 (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
};

可以看到 syscalls 為一個存放 function pointer 的陣列,通過 function pointer 去找到對應的 System call。

接著,kernel 會去執行 sys_exec()

uint64
sys_exec(void)
{
  char path[MAXPATH], *argv[MAXARG];
  int i;
  uint64 uargv, uarg;

  argaddr(1, &uargv);
  if(argstr(0, path, MAXPATH) < 0) {
    return -1;
  }
  memset(argv, 0, sizeof(argv));
  for(i=0;; i++){
    if(i >= NELEM(argv)){
      goto bad;
    }
    if(fetchaddr(uargv+sizeof(uint64)*i, (uint64*)&uarg) < 0){
      goto bad;
    }
    if(uarg == 0){
      argv[i] = 0;
      break;
    }
    argv[i] = kalloc();
    if(argv[i] == 0)
      goto bad;
    if(fetchstr(uarg, argv[i], PGSIZE) < 0)
      goto bad;
  }

  int ret = exec(path, argv);

  for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
    kfree(argv[i]);

  return ret;

 bad:
  for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
    kfree(argv[i]);
  return -1;
}

在程式碼一開始的部分會對參數進行一些檢查並且複製參數,這裡可以看到許多 arg 開頭的函式,前面說到在 System call 時,我們關注除了 a7 這個存放 System call 對應代號的暫存器以外,我們也需要 a0, a1 等存放參數的暫存器,前面在 uservec 中我們將暫存器的值全部都複製到了 trapframe 中,因此可以判定在這裡 kernel sapce 底下,我們可能通過讀取 trapframe 的內容來獲得 System call 的相關參數,上面最一開始使用到了argaddr()我們試著進行跟蹤。

void
argaddr(int n, uint64 *ip)
{
  *ip = argraw(n);
}

可以看到這邊呼叫了argraw()

static uint64
argraw(int n)
{
  struct proc *p = myproc();
  switch (n) {
  case 0:
    return p->trapframe->a0;
  case 1:
    return p->trapframe->a1;
  case 2:
    return p->trapframe->a2;
  case 3:
    return p->trapframe->a3;
  case 4:
    return p->trapframe->a4;
  case 5:
    return p->trapframe->a5;
  }
  panic("argraw");
  return -1;
}

這裡整個操作為 sys_exec() 通過 argaddr(1, &uargv) 傳入參數的指標,接著 argaddr(1, &uargv) 呼叫 argraw() 去對 uargv 指標進行讀寫。

這裡注意到一件事情,argraw() 是通過 trapframe 讀取到參數,而這一些參數是在 uservec 複製而來的,而來源為 user space,因此假設在 user space 我們輸入了一些有問題的惡意參數,像是讓我們能夠在目前的 process 存取到其他 process 等等,我們就會破壞 process 之間的隔離性,為了避免這樣的情況發生,我們通過 argraw() 抓取到參數後,我們需要對參數進行一些檢查,後面再將他複製到 kernel space 中。

而上面所說的檢查機制,我們使用到了 fetchaddr() 以及 fetchstr(),下面我們可以通過跟蹤這兩個函式來了解相關實作。

參數檢查機制,fetchstr()

int
fetchstr(uint64 addr, char *buf, int max)
{
  struct proc *p = myproc();
  if(copyinstr(p->pagetable, buf, addr, max) < 0)
    return -1;
  return strlen(buf);
}

fetchstr() 的功用為複製 user space 的參數到 kernel space,而主要的工作會在 copyinstr() 中完成。

int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  uint64 n, va0, pa0;
  int got_null = 0;

  while(got_null == 0 && max > 0){
    va0 = PGROUNDDOWN(srcva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (srcva - va0);
    if(n > max)
      n = max;

    char *p = (char *) (pa0 + (srcva - va0));
    while(n > 0){
      if(*p == '\0'){
        *dst = '\0';
        got_null = 1;
        break;
      } else {
        *dst = *p;
      }
      --n;
      --max;
      p++;
      dst++;
    }

    srcva = va0 + PGSIZE;
  }
  if(got_null){
    return 0;
  } else {
    return -1;
  }
}

copyinstr() 中,會傳入一張來自於 process 的 page table,從 user space 底下的虛擬記憶體地址 srcva,複製最多max bytes 到 kernel 的 dst 記憶體地址中。

首先看到 va0 = PGROUNDDOWN(srcva),找到 srcva 所在的 user space 底下的記憶體分頁的位置,接著呼叫 walkaddr(pagetable, va0),通過 walkaddr() 找到 va0 所對應到的物理記憶體地址 pa0,而這邊由於 xv6 物理記憶體地址與虛擬記憶體地址是使用直接映射的方式,因此 pa0 可以做為 kernel space 底下的虛擬記憶體地址,從 pa0 複製資料到 dst 中。

walkaddr()會檢查我們傳入的記憶體地址是否在 user space 的 address space 中,如果我們要去存取一個位於 kernel space 中的非法記憶體地址,我們可以想到該記憶體地址必然不會映射到 user space 的 address space,通過這樣的操作去避免一些惡意的存取。

walkaddr()

uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 pa;

  if(va >= MAXVA)
    return 0;

  pte = walk(pagetable, va, 0);
  if(pte == 0)
    return 0;
  if((*pte & PTE_V) == 0)
    return 0;
  if((*pte & PTE_U) == 0)
    return 0;
  pa = PTE2PA(*pte);
  return pa;
}

可以看到這邊對虛擬記憶體地址先判斷是否超過最大值,接著判斷 PTE 是否能被 MMU 進行有效的轉換。整個函式作用為在給定的 page table 中查看傳入的虛擬記憶體地址對應到的實體物理記憶體地址。

參數檢查機制,fetchaddr()

int
fetchaddr(uint64 addr, uint64 *ip)
{
  struct proc *p = myproc();
  if(addr >= p->sz || addr+sizeof(uint64) > p->sz) // both tests needed, in case of overflow
    return -1;
  if(copyin(p->pagetable, (char *)ip, addr, sizeof(*ip)) != 0)
    return -1;
  return 0;
}

一開始先對記憶體地址界線進行檢查,接著主要部分由 copyin() 進行處理。

int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(srcva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (srcva - va0);
    if(n > len)
      n = len;
    memmove(dst, (void *)(pa0 + (srcva - va0)), n);

    len -= n;
    dst += n;
    srcva = va0 + PGSIZE;
  }
  return 0;
}

這裡同樣會通過 walkaddr() 對記憶體地址進行上面所提及的檢查機制。

接著回到 sys_exec 正式執行 exec() 這個 System call。最後可以看到 System call 會有一個回傳值,我們可以回到 syscall() 查看這個回傳值的處理。

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    // Use num to lookup the system call function for num, call it,
    // and store its return value in p->trapframe->a0
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

p->trapframe->a0 = syscalls[num]() 可以看到這個回傳值會存放在 trapfram 中 a0 暫存器的位置,後面在 userret 中可以看到通過讀取 a0 暫存器並將其取出,作為回到 user mode 的 a0 暫存器的值。如果 a0 的值為一個正數,表示 System call 呼叫成功,如果為 -1 則表示失敗。

userret:
        # userret(pagetable)
        # switch from kernel to user.
        # a0: user page table, for satp.

        # switch to the user page table.
        csrw satp, a0
        sfence.vma zero, zero

        li a0, TRAPFRAME
.
.
.
        ld a0, 112(a0)
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

sret 回到 user mode。

到這裡,我們便通過exec()這個 System call,了解到了整個實際 System call 的 trap 流程,參數的檢查機制,以及虛擬記憶體地址使用直接映射所帶來的好處,而我們目前看到的都是由 user mode 引發的 trap,之後我們將看到在 user mode 到 supervisor mode 的 trap,以及相關的 trap handler。

reference

xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book


上一篇
Day-15 xv6 Trap (user mode): Trace Trap
下一篇
Day-17 Trap (supervisor mode, machine mode), conclusion Trap in xv6
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言