Lab2 syscall: 記錄

6.1810 的課程網址

本文紀錄課程中的作業 Lab: system calls 的解題過程

關於 xv6 的環境架設,可以參考 [MIT-6.S081-2020] OS課程—-Xv6作業系統的環境架設 ,但是在這篇文章裡,將會使用 2022 年的版本

大綱

  1. xv6 有哪些 system call,以及他們的作用為何 ?
  2. 以程式碼的觀點來理解 xv6 的 system call 流程
  3. Using gdb
  4. System call tracing

1. xv6 有哪些 system call,以及他們的作用為何 ?

0. kernel/syscall.h 定義 syste mcall 的編號

// System call numbers
#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
#define SYS_pipe    4
#define SYS_read    5
#define SYS_kill    6
#define SYS_exec    7
#define SYS_fstat   8
#define SYS_chdir   9
#define SYS_dup    10
#define SYS_getpid 11
#define SYS_sbrk   12
#define SYS_sleep  13
#define SYS_uptime 14
#define SYS_open   15
#define SYS_write  16
#define SYS_mknod  17
#define SYS_unlink 18
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21

2. 以程式碼的觀點來理解 xv6 的 system call 流程

以下使用 user/cat.c 為例,來探討 xv6 的 system call 流程,流程中有 3 大步驟

  1. user/cat.c:呼叫 read()
    • user/user.h 用 C 語言宣告 read()
    • user/usys.S 用組與實作 read()
      • 組語 li a7, SYS_read 把 read 的編號丟到 register a7
      • ecall 會連接到第2步驟
  2. kernel/syscall.csyscall()
    • 藉由 register a7 的內容得知現在要執行 sys_read()
  3. kernel/sysfile.csys_read()
    • 實際上的 system call 實作

1. user/cat.c:呼叫 read()

// user/cat.c
#include "user/user.h"

void
cat(int fd)
{
  int n;

  while((n = read(fd, buf, sizeof(buf))) > 0) { // read() 需要呼叫到 system call
    if (write(1, buf, n) != n) {
      fprintf(2, "cat: write error\n");
      exit(1);
    }
  }
  if(n < 0){
    fprintf(2, "cat: read error\n");
    exit(1);
  }
}

請注意這裡的 read() 雖然在 user/user.h 中有宣告

// user/user.h
int read(int, void*, int);

但是 read() 並沒有被 C 語言實作出來, 而是利用 user/usys.pl 這個腳本產生出來的組合語言 user/usys.S

.global read
read:
 li a7, SYS_read   # 把 read 的編號丟到 register a7 裡
 ecall             # 進入 kernel mode 並且跑到步驟 2 執行
 ret

2. kernel/syscall.csyscall()

syscalls 這個 array 的宣告比較少見,可以參考 [C 語言筆記–Day14] pointer, function, array 同時出現在一行宣告時該如何解讀

// system 的宣告,實作位於 kernel/sysproc.c 以及 kernel/sysfile.c
extern uint64 sys_fork(void);
extern uint64 sys_exit(void);
extern uint64 sys_wait(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_kill(void);
extern uint64 sys_exec(void);
extern uint64 sys_fstat(void);
extern uint64 sys_chdir(void);
extern uint64 sys_dup(void);
extern uint64 sys_getpid(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_uptime(void);
extern uint64 sys_open(void);
extern uint64 sys_write(void);
extern uint64 sys_mknod(void);
extern uint64 sys_unlink(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_close(void);

// syscalls 應該要被解讀為
// array of
// pointers to
// function with no arguments
// returning uint64
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,
};

void
syscall(void)     // cat.c 呼叫 read() ,read() 執行組語 ecall 之後會跑來這裡執行
{                 // 原因要到 lab trap 才會說明,目前只需要知道他會跑來這裡就可以了
  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]();   // 這裡是在呼叫 kernel/sysfile.c 的 sysr_read()
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

3. kernel/sysfile.csys_read()

system call 本身的實作,systemcall

uint64
sys_read(void) // 由步驟 2 呼叫而到這裡執行
{
  struct file *f;
  int n;
  uint64 p;

  argaddr(1, &p);
  argint(2, &n);
  if(argfd(0, 0, &f) < 0)
    return -1;
  return fileread(f, p, n);
}

3. Using gdb

這個題目是 2022 年的版本才有出現的,目的在於熟悉 gdb 的操作

用 gdb-multiarch debug xv6 的方式

這裡會需要開啟 2 個終端機 先在其中一個終端機輸入

make qemu-gdb

在另一個終端機輸入

gdb-multiarch

4. System call tracing

這個 lab 只需要照著下面的 some hints 一步一步的做,就可以完成了

4.1. 在 Makefile 中的 UPROGS 區塊新增 $U/_trace:

之後再執行 make qemu 會發現以下的錯誤訊息:

user/trace.c:17:7: error: implicit declaration of function ‘trace’ [-Werror=implicit-function-declaration]
   17 |   if (trace(atoi(argv[1])) < 0) {
      |       ^~~~~

這是因為 trace() 還沒有宣告

4.2. 讓 trace() 可以被成功編譯

先來說一下以程式碼的關點而言,我們想要的目的是:

  1. 在 user space 的 user/trace.c 使用了 trace()

最後會呼叫到

  1. kernel space 的 kernel/sysproc.csys_trace()(要自己加) 這個 system call

所以我們一方面要處理 user space 的編譯問題,另一方面則是 kernel space 的問題:

1. user space:
  • user/user.h 多加 int trace(int) 這個宣告:讓 user/trace.c 知道 trace() 的 prototype
  • trace() 的實作(組合語言):
    1. user/usys.pl 加入 entry("trace")
    2. kernel/syscall.h 加入 #define SYS_trace 22

此時使用 make qemu 已經可以順利開機了,但是執行 trace 32 grep hello README; 卻會失敗,這是因為在 kernel 中,sys_trace() 還沒有被實作出來(根本還不存在),因此接下來要在 kernel 中加入 sys_trace() 這個 function

2. kernel space
  1. kernel/syscall.c 中加入
...
extern uint64 sys_unlink(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_close(void);
extern uint64 sys_trace(void); // 新增這個!

// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
static uint64 (*syscalls[])(void) = {
/* ... */
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_trace]   sys_trace, // 新增這個!
};
  1. kernel/sysproc.c 加入 sys_trace() 的本體
uint64
sys_trace(void)
{
    // TODO
    return 0;
}

此時使用 make qemu 把 xv6 開機,並且執行 trace 32 grep hello README; 已經不會出現 error 了,可是行為上什麼都不會發生,因為我們還沒有把 sys_trace() 實作完成

4.3. 開始實作 SYS_trace 這個 system call