MIT6828 学习笔记 006 (lab syscall)

MIT6828 学习笔记 006 (lab syscall)

RayAlto OP

如果是从 g.csail.mit.edu clone 下来的话别忘了

1
git checkout syscall

1. 看书

实验建议我 把 4.3, 4.4 两节也看了

1.1. Sec 4.3. 代码:调用系统调用

Chapter 2 的后续: user/initcode.S:11 把执行 exec 的参数放进了寄存器 a0, a1 ,把系统调用号 SYS_exec 放进了寄存器 a7 ,这个号可以在系统调用表 (kernel/syscall.c:107) 里拿到对应的函数指针,然后通过 ecall 指令 trap 进内核并引起 uservecusertrap ,然后进入内核的 syscall (kernel/syscall.c:131) , syscall 根据 struct trapframe 里存放的寄存器 a7 的值 (SYS_exec) 调用 sys_exec (kernel/sysfile.c:434) 。 sys_exit 返回后 syscall 会把 sys_exec 的返回值保存到进程的 struct trapframea0 里,使用户空间对 exec 的调用返回这个值。

系统调用一般会返回负值表示错误; 0 或正值表示成功。如果系统调用号无效, syscall 会输出错误信息 (kernel/trap.c:148) 并返回 -1 (kernel/syscall.c:143)

1.2. Sec 4.4. 代码:系统调用参数

系统调用的实现需要获取用户代码传递的参数,因为用户代码一般调用系统调用的的包装函数,这时参数最初被放在 RISC-V C 调用规定的位置:寄存器里。内核的 trap 代码把用户的寄存器存放到当前进程的 struct trapframe 里,然后内核可以从这里读取传递的参数,比如 argint (kernel/syscall.c:56), argaddr (kernel/syscall.c:65) 和 argfd (kernel/sysfile.c:21) 用来获取系统调用的第 n 个参数(分别以整数、指针、 fd 的形式),这几个函数内部使用 argraw (kernel/syscall.c:33) 获得指定寄存器里的值。

  • argfd 只是 argint 的一层封装,确保传进来的 fd 确实是这个进程打开的
  • 现在的 Xv6 源码里还有 argstr (kernel/syscall.c:74)

一些系统调用的参数是指针,内核必须通过指针读写用户的内存,比如 exec 系统调用,要给内核传递一个 char*[] 指向用户空间的字符串参数,这就带来了两个挑战:首先用户程序可能有漏洞或有恶意代码,可能会给内核传递一个无效指针或为了访问内核内存的整活指针;其次 Xv6 内核 page table 映射和用户 page table 的不一样,所以内核不能使用常规指令来存取用户提供的地址。

内核实现了在用户提供的地址之间安全地传输数据的函数,比如 fetchstr (kernel/syscall.c:24) , exec 就是通过 fetchstr 来从用户空间获取文件名字符串参数的; fetchstr 内部通过 copyinstr (kernel/vm.c:402) 来实现具体的操作; copyinstr 从虚拟地址(进程的 struct trapframe 里的寄存器值)读取数据,通过 walkaddr (kernel/vm.c:108, 其内部调用 walk, kernel/vm.c:85) 获取物理地址,因为内核给每一个物理 RAM 地址都映射了一个对应的内核虚拟地址,所以 copyinstr 可以直接进行数据的复制。 walkaddr 可以检查用户提供的虚拟地址是否为用户进程空间的一部分,以确保程序不能给内核整活,类似的还有 copyout (kernel/vm.c:351) 可以把数据从内核写进用户提供的地址。

2. GDB 的使用

2.1. 如何调试内核

先在一个 shell 里运行内核:

1
make qemu-gdb

输出像这样:

1
2
3
4
*** Now run 'gdb' in another window.
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 1 -nographic
-global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device vi
rtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::26000

最后的 tcp::26000 意思是在我的电脑上算出的端口是 26000 。再在另一个 shell 打开一个 gdb :

1
riscv64-linux-gnu-gdb

然后在 gdb shell 里输入:

1
target remote localhost:26000

或者把实验源码的 .gdbinit 文件目录加入到 ~/.config/gdb/gdbinitset auto-load safe-path

就可以开始调试了

2.2. Xv6 Lab 的一些小提示

  • kernel/kernel.asmobjdump -S 出的汇编-源码对照文件
  • kernel/kernel.symobjdump -t 出的符号表
  • 如果没有用 gdb 的时候 panic 了,可以通过 addr2line -e <executable> <addr> 来获取具体 panic 在哪一行代码
  • 如果要用 qemu console 的 info mem ,可以 make CPUS=1 qemu 确保 qemu 硬件只有一个 CPU

2.3. 常用指令

  • location 可以是函数名 (func) 、代码位置 (main.c:114) 或内存地址 (*0x1145)
  • condition 可以是类似 foo == 114 之类的表达式
  • expression 可以是一个变量
  • format 比如 13x 为打印 13 个 hex 格式指令, 13i 为打印 13 个汇编指令
指令缩写功能
<ENTER>重复上一次操作
continuec/fg继续运行,直到断点或手动打断
finishfin继续运行,直到跳出当前函数
advance <l>adv继续运行到 l (location)
step [n]s逐行(源码)运行 n 次,会进入函数
stepi [n]si逐行(汇编指令)运行 n 次,会进入函数
next [n]n逐行(源码)运行 n 次,不进入函数
nexti [n]ni逐行(汇编指令)运行 n 次,不进入函数
break <l>bl (location) 设置一个断点
break <l> if <c>b设置断点,只有 c (condition) 满足时打断
info breakpointsi b列出所有断点信息
condition <n> <c>cond设置断点 n 只有 c 满足时打断
delete [b]d删除全部断点/断点 b
disable [b]dis禁用全部断点/断点 b ,不删除
enable [b]en启用全部断点/断点 b
watch <e>wae (expression) 取值发生变更时打断
watch -l <a>waa (address) 对应数据发生变更时打断
rwatch <e>rwe 被读取时打断
rwatch -l <a>rwa 被读取时打断
x[/f] [a]打印内存, f (format)
print/inspectp执行一个 C 表达式并以适当的格式打印出来
print/inspectp执行一个 C 表达式并以适当的格式打印出来
info registersi r列出所有寄存器信息
info framei f列出当前 stack frame 信息
list [l]l列出 l (location) 所在的代码上下文
backtrace/wherebt打印所有 stack frame 的 backtrace
layoutla显示 curses UI , Toggle: C-x,C-a/a/A
set variableset var更改变量的值
symbol-file <o>syo 加载符号表

2.4. 问题的答案

问:通过 backtrace 的输出判断哪个方法调用了 syscall

答: 0x0000000080001d18 in usertrap () at kernel/trap.c:67

问: step 过 struct proc* p = myproc(); 后在 gdb shell 里执行 p /x *p (以十六进制形式打印当前进程的 struct proc ), p->trapframe->a7 的值是什么?它代表什么?

答:我不确定题目允不允许 p p->trapframe->a7 ,因为 p /x *p 输出的 p->trapframe 是一个指针,值是地址,不能直接看到 a7 的值,但根据之前讲的 Xv6 启动过程来说这个值是 SYS_exit 应该是 7 ,通过内核里的系统调用表可以取到对应的 sys_exit 函数指针。

问: p /x $sstatus 可以打印寄存器 sstatus 值, CPU 之前的模式是什么?

答:看 RISC-V 文档 (Sec 4.1.1, p63) 的话 sstatusSPP 位表示进入 supervisor mode 之前的 mode (0 为 user mode , 1 为 supervisor mode), p /x $sstatus 输出为 0x200000022, SPP 位(第 9 个 bit )为 0 ,应该是 user mode 吧,这个真的不确定。

问:把 kernel/syscall.c:135 的 num = p->trapframe->a7; 改成 num = *(int*)0; 后运行 Xv6 会 panic ,根据输出的信息写出 panic 发生在的汇编指令,变量 num 对应的寄存器是那个?

答:在我的机器上最新编译运行出的结果是:

1
2
3
scause 0x000000000000000d
sepc=0x0000000080001ff8 stval=0x0000000000000000
panic: kerneltrap

就在 kernel/kernel.asm:4453 ,汇编指令为 lw a3,0(zero)num 对应的寄存器应该是 a3

问:通过在上面的错误位置设置断点和 gdb 的 asm layout 确定上一问的答案是否正确,为什么内核崩溃了?上面 scause 的值能证实这个原因吗?

答:在 gdb shell 执行 b *0x0000000080001ff8 可以设置断点 layout asm 切换 asm layout 看汇编指令, c 继续执行至断点发现上面的答案确实正确;原因应该是内核的虚拟地址并没有映射 0x0 ;看 RISC-V 文档 (table 4.2, p71) 的话上面 scause 的 interrupt 位为 0, exception code 为 13 (0x000000000000000d) ,对应描述是 “Load page fault” ,好像能证实?

问:上面的 scause 之类的都是内核 panic 后打印出来的,有时要确定问题所在还要知道 panic 时运行的程序。所以内核 panic 时运行的程序叫什么, pid 是多少

答: p p->name 输出 "initcode\000\000\000\000\000\000\000" 所以程序是 user/initcode, p p->uid 输出 1 所以 pid 是 1

3. QEMU 的使用

<C-a>c 可以进入 qemu shell ,再按一遍可以回到虚拟机

3.1. QEMU Shell

info mem 可以查看当前分页表

4. System call tracing

设计一个 trace 系统调用,参数是一个 bit mask ,比如传入 1 << SYS_fork 表示跟踪 fork 系统调用,当对应的 sys_exit 即将返回时要进行一行输出,格式为 {pid}: syscall {syscall_name} -> {return_value} ,注意子进程的系统调用也要一起跟踪

我的答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
diff --git a/user/user.h b/user/user.h
index 4d398d5..d932862 100644
--- a/user/user.h
+++ b/user/user.h
@@ -22,6 +22,7 @@ int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
+int trace(int);

// ulib.c
int stat(const char*, struct stat*);



diff --git a/user/usys.pl b/user/usys.pl
index 01e426e..9c97b05 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -36,3 +36,4 @@ entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
+entry("trace");



diff --git a/kernel/syscall.h b/kernel/syscall.h
index bc5f356..cc112b9 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -20,3 +20,4 @@
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
+#define SYS_trace 22



diff --git a/kernel/proc.h b/kernel/proc.h
index d021857..9dd2c13 100644
--- a/kernel/proc.h
+++ b/kernel/proc.h
@@ -104,4 +104,7 @@ struct proc {
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
+
+ // lab syscall
+ int trace_mask; // Trace mask
};



diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 3b4d5bd..c361041 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -91,3 +91,13 @@ sys_uptime(void)
release(&tickslock);
return xticks;
}
+
+// set a mask to trace masked system calls of child processes
+uint64
+sys_trace(void)
+{
+ int mask;
+ argint(0, &mask);
+ myproc()->trace_mask = mask;
+ return 0;
+}



diff --git a/kernel/proc.c b/kernel/proc.c
index 959b778..3ac7f8f 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -299,6 +299,9 @@ fork(void)
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);

+ // copy trace mask
+ np->trace_mask = p->trace_mask;
+
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;




diff --git a/kernel/syscall.c b/kernel/syscall.c
index ed65409..e09f700 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -101,6 +101,7 @@ 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.
@@ -126,6 +127,33 @@ static uint64 (*syscalls[])(void) = {
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
+[SYS_trace] sys_trace,
+};
+
+// system call names
+static const char* syscall_names[] = {
+[SYS_fork] "fork",
+[SYS_exit] "exit",
+[SYS_wait] "wait",
+[SYS_pipe] "pipe",
+[SYS_read] "read",
+[SYS_kill] "kill",
+[SYS_exec] "exec",
+[SYS_fstat] "fstat",
+[SYS_chdir] "chdir",
+[SYS_dup] "dup",
+[SYS_getpid] "getpid",
+[SYS_sbrk] "sbrk",
+[SYS_sleep] "sleep",
+[SYS_uptime] "uptime",
+[SYS_open] "open",
+[SYS_write] "write",
+[SYS_mknod] "mknod",
+[SYS_unlink] "unlink",
+[SYS_link] "link",
+[SYS_mkdir] "mkdir",
+[SYS_close] "close",
+[SYS_trace] "trace",
};

void
@@ -139,6 +167,13 @@ syscall(void)
// 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]();
+
+ // print system call traces
+ if((p->trace_mask & (1 << num)) != 0) {
+ printf("%d: syscall %s -> %d\n",
+ p->pid, syscall_names[num],
+ p->trapframe->a0);
+ }
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);

5. Sysinfo

添加一个系统调用 sysinfo 收集正在运行的系统的信息,唯一一个参数是 struct sysinfo* (kernel/sysinfo.h) ,这个系统调用应该对里面的成员变量赋值:

  • freemem: 可用内存的数量,单位: bytes
  • nproc: 状态为 UNUSED 的进程的数量
我的答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
diff --git a/user/user.h b/user/user.h
index 4d398d5..ff8b2af 100644
--- a/user/user.h
+++ b/user/user.h
@@ -23,6 +23,8 @@ int getpid(void);
int sleep(int);
int uptime(void);
int trace(int);
+struct sysinfo;
+int sysinfo(struct sysinfo*);

// ulib.c
int stat(const char*, struct stat*);



diff --git a/user/usys.pl b/user/usys.pl
index 01e426e..bc109fd 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -37,3 +37,4 @@ entry("sbrk");
entry("sleep");
entry("uptime");
entry("trace");
+entry("sysinfo");



diff --git a/kernel/syscall.h b/kernel/syscall.h
index bc5f356..0dfedc7 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -21,3 +21,4 @@
#define SYS_mkdir 20
#define SYS_close 21
#define SYS_trace 22
+#define SYS_sysinfo 23



diff --git a/kernel/defs.h b/kernel/defs.h
index a3c962b..679375c 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -63,6 +63,7 @@ void ramdiskrw(struct buf*);
void* kalloc(void);
void kfree(void *);
void kinit(void);
+int free_mem_count(void);

// log.c
void initlog(int, struct superblock*);
@@ -106,6 +107,7 @@ void yield(void);
int either_copyout(int user_dst, uint64 dst, void *src, uint64
len);
int either_copyin(void *dst, int user_src, uint64 src, uint64
len);
void procdump(void);
+int unused_proc_count(void);

// swtch.S
void swtch(struct context*, struct context*);



diff --git a/kernel/kalloc.c b/kernel/kalloc.c
index 0699e7e..3e8f942 100644
--- a/kernel/kalloc.c
+++ b/kernel/kalloc.c
@@ -80,3 +80,15 @@ kalloc(void)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
+
+
+int
+free_mem_count(void)
+{
+ int count = 0;
+ for (struct run* r = kmem.freelist; r != 0; r = r->next) {
+ count++;
+ }
+ count *= PGSIZE;
+ return count;
+}



diff --git a/kernel/proc.c b/kernel/proc.c
index 959b778..ebb6287 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -684,3 +684,15 @@ procdump(void)
printf("\n");
}
}
+
+int
+unused_proc_count(void)
+{
+ int count = 0;
+ for (struct proc* p = proc; p < &proc[NPROC]; p++) {
+ if (p->state == UNUSED) {
+ count++;
+ }
+ }
+ return count;
+}



diff --git a/kernel/syscall.c b/kernel/syscall.c
index ed65409..d39158f 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -102,6 +102,7 @@ extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_close(void);
extern uint64 sys_trace(void);
+extern uint64 sys_sysinfo(void);

// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
@@ -128,6 +129,7 @@ static uint64 (*syscalls[])(void) = {
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
[SYS_trace] sys_trace,
+[SYS_sysinfo] sys_sysinfo,
};

// system call names
@@ -154,6 +156,7 @@ static uint64 (*syscalls[])(void) = {
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
+[SYS_sysinfo] "sysinfo",
};

void



diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 3b4d5bd..e4c2aad 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -5,6 +5,7 @@
#include "memlayout.h"
#include "spinlock.h"
#include "proc.h"
+#include "sysinfo.h"

uint64
sys_exit(void)
@@ -101,3 +102,19 @@ sys_uptime(void)
myproc()->trace_mask = mask;
return 0;
}
+
+// get running system information
+uint64
+sys_sysinfo(void)
+{
+ uint64 addr;
+ argaddr(0, &addr);
+ struct sysinfo info;
+ info.freemem = free_mem_count();
+ info.nproc = unused_proc_count();
+ struct proc* p = myproc();
+ if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0) {
+ return -1;
+ }
+ return 0;
+}

这里不能通过 user/sysinfotest.ctestproc 测试,大概率是测试代码写错了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// user/sysinfotest.c 简化版
void testproc() {

uint64 nproc;

sinfo(&info);
nproc = info.nproc;

pid = fork();

if(pid == 0){
sinfo(&info);
if(info.nproc != nproc+1) {
printf("sysinfotest: FAIL nproc is %d instead of %d\n", info.nproc, nproc+1);
exit(1);
}
exit(0);
}

}

这里第 13 行创建了个子进程后测试要求新的 sysinfo.nproc 比创建子进程之前多 1 ,显然是错误的,因为题目要求 sysinfo.nprocUNUSED 状态的进程数量,所以新的 sysinfo.nproc 应该比创建子进程之前少 1 。

最后偷懒在 kernel/sysproc.c include 了 sysinfo.h 头文件,直接在里面实现了整个系统调用,可能不是很优雅,但我已经猪脑过载了,不知道如果想解耦的话应该怎么操作。但好像 include 了 sysinfo.h 的除了 user/sysinfotest.c 之外就没有了,要往 struct sysinfo 里写数据,再怎么解耦也逃不了 include sysinfo.h

6. 系统调用的过程

因为我是 dinner 所以再总结一下防止猪脑过载。比如进程调用了 fork ,这个系统调用声明在 user/user.h ,但是没有具体的 C 代码实现,因为 C 代码不是万能的,它的实现在 user/usys.S (user/usys.pl 自动生成) 里,是汇编代码,把系统调用对应的系统调用号 SYS_fork (kernel/syscall.h) 放进寄存器 a7

为什么 user/usys.S 可以用定义在 C 头文件 kernel/syscall.h 里的 SYS_fork 之类的符号(宏?):因为在 Makefile 里它被 CC 编译, CC 会进行预处理把 SYS_fork 之类的符号按照 kernel/syscall.h 定义的值进行替换

然后通过 ecall 指令,经过目前还不知道(现在知道了)的途径进入了 kernel/trampoline.S ,它会跳进 kernel/trap.c:36 的 usertrap 里,然后 syscall (kernel/syscall.c:162) 被调用,根据 p->trampframe->a7 的值就能在 syscalls (kernel/syscall.c:109) 找到 fork 的真正实现 sys_fork (kernel/sysproc.c:25) 。

现在知道了,内核在初始化时 (kernel/trap.c:29) 会给 stvec 寄存器写入 kernel/trampoline.S 中的 uservec (kernel/trampoline.S:21) 的地址, RISC-V CPU 接收到 ecall 指令后会转到 stvec 指向的地址继续执行