Lab2 syscall: 記錄
本文紀錄課程中的作業 Lab: system calls 的解題過程
關於 xv6 的環境架設,可以參考 [MIT-6.S081-2020] OS課程—-Xv6作業系統的環境架設 ,但是在這篇文章裡,將會使用 2022 年的版本
大綱
- xv6 有哪些 system call,以及他們的作用為何 ?
- 以程式碼的觀點來理解 xv6 的 system call 流程
- Using gdb
- 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 大步驟
user/cat.c
:呼叫read()
user/user.h
用 C 語言宣告read()
user/usys.S
用組與實作read()
- 組語
li a7, SYS_read
把 read 的編號丟到 registera7
裡 ecall
會連接到第2步驟
- 組語
kernel/syscall.c
的syscall()
- 藉由 register
a7
的內容得知現在要執行sys_read()
- 藉由 register
kernel/sysfile.c
的sys_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.c
的 syscall()
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.c
的 sys_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()
可以被成功編譯
先來說一下以程式碼的關點而言,我們想要的目的是:
- 在 user space 的
user/trace.c
使用了trace()
最後會呼叫到
- kernel space 的
kernel/sysproc.c
的sys_trace()
(要自己加) 這個 system call
所以我們一方面要處理 user space 的編譯問題,另一方面則是 kernel space 的問題:
1. user space:
- 在
user/user.h
多加int trace(int)
這個宣告:讓user/trace.c
知道trace()
的 prototype trace()
的實作(組合語言):- 在
user/usys.pl
加入entry("trace")
- 在
kernel/syscall.h
加入#define SYS_trace 22
- 在
此時使用 make qemu
已經可以順利開機了,但是執行 trace 32 grep hello README;
卻會失敗,這是因為在 kernel 中,sys_trace()
還沒有被實作出來(根本還不存在),因此接下來要在 kernel 中加入 sys_trace()
這個 function
2. kernel space
- 在
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, // 新增這個!
};
- 在
kernel/sysproc.c
加入sys_trace()
的本體
uint64
sys_trace(void)
{
// TODO
return 0;
}
此時使用 make qemu
把 xv6 開機,並且執行 trace 32 grep hello README;
已經不會出現 error 了,可是行為上什麼都不會發生,因為我們還沒有把 sys_trace()
實作完成