MIT 6.S081 All in one

闲着没事把 6.S081 2021 ~ 2025 年的实验都写了一遍。

Lab0 - 环境配置

系统为Archlinux物理机,环境如下:

安装依赖

参考:https://pdos.csail.mit.edu/6.828/2021/tools.html

首先安装转义riscv64的包:

1
sudo pacman -S riscv64-linux-gnu-binutils riscv64-linux-gnu-gcc riscv64-linux-gnu-gdb
1
2
3
4
5
6
7
8
9
10
riscv64-linux-gnu-gcc --version
qemu-system-riscv64 --version

riscv64-linux-gnu-gcc (GCC) 15.1.0
Copyright (C) 2025 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

QEMU emulator version 10.2.2
Copyright (c) 2003-2025 Fabrice Bellard and the QEMU Project developers

安装 qemu:

图省事安装 qemu-arch-extra-git 就够了。用 qemu 环境比较多的同学可以直接安装 qemu-full

1
sudo pacman -S qemu-full

编译运行

参考:https://pdos.csail.mit.edu/6.828/2021/labs/util.html

实验用的 xv6 和 github 上的的不太一样,别傻乎乎去 clone github 上的。如果要其他年份的就把末尾的xv6-labs-2021改为其他年份就行。

1
2
git clone git://g.csail.mit.edu/xv6-labs-2021
cd xv6-labs-2021

在较早年份的实验中,xv6 默认是 master 分支,里面什么都没有,根据不同实验要求切换到相应分支即可。

1
2
3
4
git checkout util
ls
conf/ mkfs/ user/ fs.img gradelib.py Makefile xv6.out
kernel/ __pycache__/ compile_commands.json grade-lab-pgtbl* LICENSE README

2021 - 2024 年的实验直接编译会提示各种编译错误。这里只说一下我遇到的,大多都是因为 -Werror 标志 把所有 warningerror 处理。图省事可以直接在 Makefile 里把这个 flag 删了。

  1. 提示 user/sh.c:58:1: error: infinite recursion detected [-Werror=infinite-recursion]

user/sh.cruncmd 函数前添加一行__attribute__((noreturn))即可。

  1. 提示 user/usertests.c:2829:6: error: initialization of 'void (*)(char *)' from incompatible pointer type 'void (*)(void)' [-Wincompatible-pointer-types]

user/usertest.c 中将 rwsbrk() 添加一个 char * 参数即可。rwsbrk(char *argv)

2025 年的实验可以直接编译,无须任何改动。

然后运行 make qemu 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
make qemu
riscv64-linux-gnu-gcc -c -o kernel/entry.o kernel/entry.S
riscv64-linux-gnu-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -DSOL_PGTBL -DLAB_PGTBL -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o kernel/kalloc.o kernel/kalloc.c
riscv64-linux-gnu-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -DSOL_PGTBL -DLAB_PGTBL -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o kernel/string.o kernel/string.c

...

qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

xv6 kernel is booting

hart 2 starting
hart 1 starting
init: starting sh
$

最后出现这几行就说明编译成功了,按ctrl + a, x退出模拟环境。

配置 Lsp

一直使用 clangd 作为分析工具,需要 compile_commands.json 文件来告诉 clangd 编译参数。通过 bear 这个工具可以轻易的生成。

1
2
3
sudo pacman -S bear
make clean # 从头开始编译,否则生成的编译信息会不全
bear -- make qemu

xv6 根目录下出现 compile_commands.json 文件即可。

clangd 分析会生成 .cache/ 目录,不应该加入 git 维护的内容中,所以在 .gitignore 文件里新增一行:

1
2
3
4
...
ph
barrier
.cache/

至此 xv6 环境就配置完毕。

Lab1 util:Unix utilities | UNIX 实用程序

本节实验主要用于入门系统编程,利用系统调用底层工具。根据课程官网的要求,需要看完Lecture 1,阅读完教材第一章\(Operation \space System \space interfaces\)

此外,实验手册中的 Hints 很重要,大多数实验跟着提示一步一步实现都能完成。

Sleep (easy)

实现 sleep ticks 程序。即在终端输入 sleep 10 可以让内核“睡眠” \(10\) 个 ticks。

首先按照Hint-1的提示看一眼user/echo.c是什么,可以看到下面的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
```C
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc, char* argv[]) {
int i;

for (i = 1; i < argc; i++) {
write(1, argv[i], strlen(argv[i]));
if (i + 1 < argc) {
write(1, " ", 1);
} else {
write(1, "\n", 1);
}
}
exit(0);
}

argc存储终端传入的参数个数,argv存储传入的参数的具体内容。例如运行 echo hello 命令,argc 等于 \(2\)argv 分别为 "echo""hello"

xv6 内核已经实现了 sys_sleep 系统调用(见kernel/syscall.hkernel/sysproc.c)。直接处理参数然后调用 sys_sleep 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[]) {
int i;
if (argc < 2) {
fprintf(2, "Usage: sleep ticsk\n");
exit(1);
}

i = atoi(argv[1]);
sleep(i);

exit(0);
}

pingpong (eaxy)

练习 forkpipe 以及 write/read 系统调用。和课程上 \(pipe2\) 的例子差不多(see pipe2.c)。

注意管道是单向的,所以需要两个管道来实现父子进程的双向通信。

养成好习惯,不用的管道立刻释放,不占用 fd。这在 primes 实验很重要。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

#define READ 0
#define WRITE 1

int
main(int argc, char *argv[]) {
// parent to child
int fd1[2];
pipe(fd1);

// child to parent
int fd2[2];
pipe(fd2);

char buf[2];

int pid = fork();

// parent
if (pid != 0) {
// close useless pipe
close(fd2[WRITE]); close(fd1[READ]);

// send a byte to child
if (write(fd1[WRITE], "#", 1) != 1) {
fprintf(2, "cannot write a byte to child.\n");
exit(1);
}
close(fd1[WRITE]);

// read a byte from child
if (read(fd2[READ], buf, 1) != 1) {
fprintf(2, "cannot read a byte from child.\n");
exit(1);
}
close(fd2[READ]);

// print a pong message
fprintf(1, "%d: received pong\n", getpid());
wait(0);
} else {
// close useless pipe
close(fd1[WRITE]); close(fd2[READ]);

// read a byte from parent
if (read(fd1[READ], buf, 1) != 1) {
fprintf(2, "cannot read a byte from parent.\n");
exit(1);
}
close(fd1[READ]);

// print a ping message
fprintf(1, "%d: received ping\n", getpid());

// write a byte to parent
if (write(fd2[WRITE], buf, 1) != 1) {
fprintf(2, "cannot write a byte to parent.\n");
exit(1);
}
close(fd2[WRITE]);
}

exit(0);
}

Primes (hard)

这个实验很多教程实现的都有问题。大多数人实现的都是上一个进程把所有东西都输入管道之后再启动子进程,即并没有实现多个进程之间并行

要我们实现埃氏筛的管道版本。给没接触过算法竞赛的同学稍微解释一下,埃氏筛通过筛除已知质数的所有倍数,以此来得到质数。即第一轮遍历将所有 \(2\) 的倍数都筛出,第二轮遍历将所有 \(3\) 的倍数筛出,第三轮遍历将所有 \(5\) 的倍数筛出。C ++ 版本代码如下:

1
2
3
4
5
6
7
8
9
10
std::vector<bool> is_prime(N + 1, true);
std::vector<int> primes;

for (int i = 2;i < N;i ++) {
if (!is_prime[i]) continue;
primes.push_back(i);
for (int j = i + i;j < N;j += i) {
is_prime[i] = false;
}
}

这个实验要求我们把遍历改为进程,每一个进程负责筛选一个质因子。

每个进程都需要一遍读取上一个进程发来的数字,同时筛选后发给下一个进程。每个进程收到的第一个质数即该进程用于筛选的质因子。可以写出如下的代码:

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

#define READ 0
#define WRITE 1

void
seive(int left[2]) {
close(left[WRITE]);

int first;
// donot hove any more number.
if (read(left[READ], &first, sizeof(int)) == 0) {
exit(0);
}

fprintf(1, "prime %d\n", first);

int right[2];
pipe(right);

int cur;
int pid = fork();
if (pid == 0) {
seive(right);
} else {
close(right[READ]);

// read, filt and write
while (read(left[READ], &cur, sizeof(int)) == sizeof(int)) {
if (cur % first != 0) {
if (write(right[WRITE], &cur, sizeof(int)) != sizeof(int)) {
fprintf(2, "pid %d: write error.\n", getpid());
exit(1);
}
}
}

close(left[READ]);
close(right[WRITE]);

wait(0);
}
}

int
main(int argc, char *argv[]) {
int p[2];
pipe(p);

// process 1 push all number to process 2
// then process i filt all number that can be divide by i

int pid = fork();
// parent
if (pid != 0) {
close(p[READ]);

// sent all number to child
for (int i = 2;i <= 35;i ++) {
if (write(p[WRITE], &i, sizeof(int)) != sizeof(int)) {
fprintf(2, "cannot write %d to process 2.\n", i);
exit(1);
}
}

close(p[WRITE]);
// wait for all child finished.
wait(0);
} else {
seive(p);
}

exit(0);
}

find (moderate)

实现 find 程序,在特定目录下查找所有特定名称的文件。

提示中提到了 user/ls.c 目录,先看一下这个文件。一开始是一个 fmtname 函数,用于将文件名格式化为 DIRSIZ 个字符长度。之后反复使用 read 读取字节流到 dirent 结构体,并通过 fstat 函数读取 dirent 的状态到 state 结构体中。

struct dirent 定义在 kernel/fs.h 中:

1
2
3
4
5
6
7
// Directory is a file containing a sequence of dirent structures.
#define DIRSIZ 14

struct dirent {
ushort inum;
char name[DIRSIZ];
};

注释中明确说了 “目录是一个包括一系列dirent结构体的文件”。并且 name 字段的大小是 DIRSIZ,这也解释了为什么要用 fmtname 来格式化字符串长度。

struct stat 定义在 kernel/stat.h 中:

1
2
3
4
5
6
7
8
9
10
11
#define T_DIR 1      // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device

struct stat {
int dev; // File system's disk device
uint ino; // Inode number
short type; // Type of file
short nlink; // Number of links to file
uint64 size; // Size of file in bytes
};

结合 user/ls.c 中的相关代码:

1
2
3
4
5
6
7
8
9
10
if ((fd = open(path, 0)) < 0) {
fprintf(2, "ls: cannot open %s\n", path);
return;
}

if (fstat(fd, &st) < 0) {
fprintf(2, "ls: cannot stat %s\n", path);
close(fd);
return;
}

我们不难猜出,xv6 中一切都是以文件的形式来存储的,目录、文件、设备等所有东西只不过是不同格式的文件。通过 open 系统调用打开一个目录,通过 read 系统调用不断读取目录中的内容,存储在 struct dirent 结构体中。通过 fstat 函数来识别这个文件的信息,存储在 struct state 中。通过 state 中的 type 字段查看是 目录、文件还是设备。

如果是文件,则比较 de.name 是否和给定的参数相同。如果是目录,则递归的 find 子目录。大概照着 ls.c 抄一下。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

void find(char* path, char* filename) {
int fd;
if ((fd = open(path, 0)) < 0) {
fprintf(2, "find: cannot open %s\n", path);
return;
}

struct stat st;
if (fstat(fd, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}

if (st.type != T_DIR) {
fprintf(2, "find: %s should be a dir, but found a file.\n", path);
exit(1);
}

char buf[512];

// strlen(path) + 1 表示path的长度 + '\0'
// DIRSIZ 是文件名最大长度, 见kernel/fs.h 54行
// 最后还要在末位加上`/`,e.g. code/user -> code/user/
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
fprintf(2, "find: path too long.\n");
exit(1);
}

strcpy(buf, path);
char *p = buf + strlen(buf);
*p ++ = '/';
// 运算优先级: *p ---> p ++. 即p指向'/'的后一个位置

struct dirent de;
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0) continue;
// 将de.name复制到p后面,最大长度为DIRSIZ
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
if (stat(buf, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", buf);
continue;
}
switch (st.type) {
case T_FILE:
if (strcmp(de.name, filename) == 0) fprintf(1, "%s\n", buf);
break;
case T_DIR:
if (strcmp(de.name, ".") == 0) continue;
if (strcmp(de.name, "..") == 0) continue;
find(buf, filename);
}
}
}

int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(2, "Usage: find [path] <filename>\n");
exit(1);
}

if (argc == 2) {
find(".", argv[1]);
} else {
find(argv[1], argv[2]);
}

exit(0);
}

xargs (moderate)

有时候想通过管道传递参数给不同的命令,例如 echo world | echo hello。但是我们知道 echo 并不是从标准输入中获取参数,而是从 cmd 中传递。于是就需要一个程序 xargs,将从标准输入的参数拦截下来,并通过 exec 传递给另一个命令。即 echo world | xargs echo hello

所以我们只需要从 标准IO 中读取将上一个程序的输出,并将其拼接到 cmd 参数的后面,用 fork/exec 那一套运行 cmd 即可。这题主要难在 C 语言的大便字符串上。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"

int
main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(2, "Usage: xargs command [args...]\n");
exit(1);
}

char *cmd_argv[MAXARG];
char buf[512];
int i;

for (i = 1; i < argc; i++) {
cmd_argv[i - 1] = argv[i];
}

while (1) {
int p = 0;
int n;

while (1) {
n = read(0, &buf[p], 1);
if (n <= 0 || buf[p] == '\n') break;
p++;
}

if (n <= 0 && p == 0) break;

buf[p] = '\0';

cmd_argv[argc - 1] = buf;
cmd_argv[argc] = 0;

if (fork() == 0) {
exec(cmd_argv[0], cmd_argv);
fprintf(2, "xargs: exec %s failed\n", cmd_argv[0]);
exit(1);
} else {
wait(0);
}
}

exit(0);
}

sixfive (moderate) 2025

这是 2025 版的新实验。从用户参数中读取指定文件,并输出文件中所有能被 \(5\)\(6\) 整除的数字。一个合法数字应该由 "-\r\t\n./,"\(7\) 个字符分割。即 xv6 中的 \(6\) 不是一个合法数字,但 /6 中的 \(6\) 为合法数字。

没啥好说的,直接 read/write 完事。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(2, "Usage: sixfive {filename}\n");
exit(1);
}

for (int i = 1;i < argc;i ++) {
int fd;
if ((fd = open(argv[i], 0)) < 0) {
fprintf(2, "sixfive error: cannot open file %s.\n", argv[i]);
close(fd);
exit(1);
}

char c;
int cur = 0, flag = 0;

while (read(fd, &c, sizeof(char)) == sizeof(char)) {
if (c >= '0' && c <= '9') {
cur = cur * 10 + c - '0';
flag = 1;
} else {
if (c == '-' || c == '\r' || c == '\t' || c == '\n'
|| c == '.' || c == '/' || c == ',') {
if ((cur % 5 == 0 || cur % 6 == 0) && flag == 1)
fprintf(1, "%d\n", cur);
}
cur = 0; flag = 0;
}
}
close(fd);
}
exit(0);
}

memdump (easy) 2025

要实现有点类似 printf 的一个东西,通过解析 fmt 字符串,按照特定的格式解析 data,并输出。注意指针之间的转换即可。

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
void
memdump(char *fmt, char *data)
{
int len = strlen(fmt);
for (int i = 0;i < len;i ++) {
if (fmt[i] == 'i') {
printf("%d\n", *(int *)data);
data += 4;
} else if (fmt[i] == 'p') {
printf("%llx\n", *(long long *) data);
data += 8;
} else if (fmt[i] == 'h') {
// see kernel/printf.c.
// xv6's standard library does not support the %hd flag
// which used to printf short integer.
short cur = *(short *) data;
data += 2;
printf("%d\n", cur);
} else if (fmt[i] == 'c') {
printf("%c\n", *data);
data ++;
} else if (fmt[i] == 's') {
char *p = (char *)*(unsigned long long *)data;
printf("%s\n", p);
data += 8;
} else if (fmt[i] == 'S') {
int offset = strlen(data);
printf("%s\n", data);
data += offset;
}
}
}

exec (moderate)

给之前实现的 find 程序新增一个 -exec 标志,将找到的每一个文件名分别作为参数传递为 exec 。也没什么好说的,细心处理 fork/exec 即可。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/param.h"

char *exec_argv[MAXARG];
int has_exec = 0;
int exec_argc = 0;

void find(char* path, char* filename) {
int fd;
if ((fd = open(path, 0)) < 0) {
fprintf(2, "find: cannot open %s\n", path);
return;
}

struct stat st;
if (fstat(fd, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}

if (st.type != T_DIR) {
fprintf(2, "find: %s should be a dir, but found a file.\n", path);
close(fd);
return;
}

char buf[512];
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
fprintf(2, "find: path too long.\n");
close(fd);
return;
}

strcpy(buf, path);
char *p = buf + strlen(buf);
*p++ = '/';

struct dirent de;
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0) continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;

if (stat(buf, &st) < 0) {
continue;
}

if (st.type == T_FILE) {
if (strcmp(de.name, filename) == 0) {
if (has_exec) {
int pid = fork();
if (pid == 0) {
exec_argv[exec_argc] = buf;
exec_argv[exec_argc + 1] = 0;
exec(exec_argv[0], exec_argv);
fprintf(2, "find: exec %s failed\n", exec_argv[0]);
exit(1);
} else if (pid > 0) {
wait(0);
}
} else {
fprintf(1, "%s\n", buf);
}
}
} else if (st.type == T_DIR) {
if (strcmp(de.name, ".") != 0 && strcmp(de.name, "..") != 0) {
find(buf, filename);
}
}
}
close(fd);
}

int main(int argc, char* argv[]) {
if (argc < 3) {
fprintf(2, "Usage: find [path] [filename] [-exec cmd args...]\n");
exit(1);
}

char *path = argv[1];
char *filename = argv[2];

for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-exec") == 0) {
has_exec = 1;
int k = 0;
for (int j = i + 1; j < argc; j++) {
if (k < MAXARG - 2) {
exec_argv[k++] = argv[j];
}
}
exec_argc = k;
break;
}
}

find(path, filename);
exit(0);
}

Lab2 syscall: System Calls | 系统调用

这一章开始就正式进入内核开发。第二章主要是实现新的系统调用。

根据课程官网的要求,需要阅读完教材的第二章\(Operating \space system \space organization\)。并且读懂下列源代码:kernel/proc.h, kernel/defs.h, kernel/entry.S, kernel/main.c, user/initcode.S, user/init.c, 简略了解kernel/proc.c, kernel/exec.c

写这个 Lab 之前先总结一下 xv6 系统调用的过程。

用户进程中的系统调用实现在 user/usys.S 中。将系统调用号存放在 a7 寄存器中,执行 ecall 指令进入内核态。通过一系列 trap 指令将寄存器放入 trapfram->a7 中。

fork 为例,user/usys.S:

1
2
3
4
5
.global fork
fork:
li a7, SYS_fork
ecall
ret

之后调用 syscall() 函数(见 kernel/syscall.c),从 trapfram->a7 中取出系统调用号,并通过一个函数指针数组调用对应的系统调用实现。

kernel/syscall.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static uint64 (*syscalls[])(void) = { ... };

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

和进程有关系统调用的实现在 kernel/sysproc.c,和文件有关的在 kernel/sysfile.c。系统调用号存储在 kernel/syscall.h

System call tracing (moderate)

实现一个系统调用 trace mask,用于追踪特定的系统调用。

从上面的 syscall() 函数中可以看到,在 p->trapframe->a0 = syscalls[num]() 之前判断 num 是否在掩码中,如果有打印信息就可以了。

首先新建一个系统调用的存根:

  • kernel/syscall.h 中加入 #define SYS_trace 22
  • kernel/syscall.c 中加入 sys_trace 的声明 extern uint64 sys_trace(void);; 在 syscalls 数组中加入 [SYS_trace] sys_trace
  • user/user.h 中加入函数用户态接口声明 int trace(int);
  • user/usys.pl 中加入入口声明 entry("trace");

总结起来就是出现系统调用的地方都抄一个 trace 上去。

根据提示,新增一个 int trace_mask 成员变量到 struct proc 中,这样就可以记录哪些系统调用是要追踪的了。同时将 kernel/proc.c 中的 fork 中,将父进程的 trace_mask 复制给子进程。

kernel/sysproc.c 中新增 sys_trace 的实现,将用户参数复制到 struct proc 中新增的 trace_mask 中。

1
2
3
4
5
6
7
8
uint64
sys_trace(void) {
int mask;
if (argint(0, &mask) < 0)
return -1;
myproc()->trace_mask = mask;
return 0;
}

由于需要打印系统调用的名字,我们照 syscalls 数组抄一个从 系统调用号 映射到 系统调用名字 的数组。

1
2
3
4
5
6
7
8
9
static char *syscall_name[] = {
"#", // ensure the index stars from 1
"fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat",
"chdir", "dup", "getpid", "sbrk",
"sleep", "uptime", "open", "write",
"mknod", "unlink", "link", "mkdir",
"close", "trace", "sysinfo"
};

然后在 syscall 函数修改为如果有掩码就输出信息即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
if ((p->trace_mask >> num) & 1) {
printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

Sysinfo (moderate)

先按照上一个任务的方法加入一个 sysinfo 系统调用. 这里在 kernel 文件夹下再写一个 sysinfo.c 来实现这个系统调用. 在 MakefileOBJS 加入 $K/sysinfo.o \(因为我在sysinfo.c中实现,所以要链接这个文件。如果直接在kernel/proc.c中实现就不用).

filestat()函数能得知copyout函数的功能,加上sys_fstat()中虚拟地址是从a0读出来的。所以很容易能写出下面代码。

sysinfo.c

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
#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "date.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "proc.h"
#include "sysinfo.h"

extern uint64 freemem();
extern uint64 num_proc();


uint64
sys_sysinfo(void) {
struct sysinfo info;
struct proc *p;
uint64 va;

info.freemem = freemem();
info.nproc = num_proc();

p = myproc();
if (argaddr(0, &va) < 0)
return -1;
if(copyout(p->pagetable, va, (char *)&info, sizeof(info)) < 0)
return -1;

return 0;
}

然后是freemem()num_proc()

kernel/kalloc.c中添加,看一下前面的kalloc()函数,在看一下struct kmem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct run {
struct run* next;
};

struct {
struct spinlock lock;
struct run* freelist;
} kmem;

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void* kalloc(void) {
struct run* r;

acquire(&kmem.lock);
r = kmem.freelist;
if (r) kmem.freelist = r->next;
release(&kmem.lock);

if (r) memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}

其中freelist是一个链表,存储所有空闲内存页面的地址。所以我们只需要遍历这个链表,每一项都加上PGSIZE即可。

freemem:

1
2
3
4
5
6
7
8
9
10
11
uint64 freemem() {
struct run* r;
uint64 tot = 0;

acquire(&kmem.lock);
for (r=kmem.freelist; r; r = r->next)
tot += PGSIZE;
release(&kmem.lock);

return tot;
}

然后看一看 kernel/proc.c 中的 procdump 函数可以知道,proc 数组中存放的是所有进程控制块的地址,所以遍历 proc 即可。

kernel/proc.c中添加:

1
2
3
4
5
6
7
8
uint32 num_proc() {
uint32 tot = 0;
struct proc* p;
for (p = proc;p < &proc[NPROC];p ++) {
if (p->state != UNUSED) tot ++;
}
return tot;
}

Attack xv6 (moderate) 2024

这个实验中,kernel/vm.c 以及 kernel/kalloc.c 中的 memset 都被注释了,即释放、申请内存时,新内存会存储之前内存的值。这允许我们通过一些手段得到其他进程的信息。

先从 user/attacktext 中进行分析。首先父进程调用第一个 fork 来创建子进程 \(1\),通过 secrect 函数向某块内存写入一句话。之后立刻调用第二个fork 创建子进程 \(2\),通过 attack 函数尝试得到这段话。

再分别看看 secrect.cattact.cattack.c 即我们要实现的东西。secrect.c 首先通过 sbrk 系统调用申请了 \(32\) 页新内存,并在第 \(9\) 块页面的起点写了一条提示语句: "my very very very secret pw is: ",最后紧跟着写入一条密码。

user/secrect.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
int
main(int argc, char *argv[])
{
if(argc != 2){
printf("Usage: secret the-secret\n");
exit(1);
}
char *end = sbrk(PGSIZE*32);
end = end + 9 * PGSIZE;
strcpy(end, "my very very very secret pw is: ");
strcpy(end+32, argv[1]);
exit(0);
}

由于 secrectattack 中间并没有其他进程产生,也没有任何内存申请,所以子进程 \(2\) 中很大概率能分配到 attack 中释放的内存。

所以我们可以在 attack 中申请大量的内存页,然后遍历这些内存页,看看哪个页的开头有那句提示词,然后偏移 \(32\) 个字节即为密码。

需要注意的是,每一页的前 \(8\) 个字节在 freelist 中是用来存储 struct next *r 的,所以我们要从第 \(9\) 开始匹配。如果全部匹配的话,似乎会匹配到 .rodata 段,然后偏移 \(32\) 的位置是脏数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "kernel/types.h"
#include "kernel/fcntl.h"
#include "user/user.h"
#include "kernel/riscv.h"

int
main(int argc, char *argv[])
{
char *mem = sbrk(32 * PGSIZE);

for (int i = 0; i < 32; i++) {
char *page = mem + i * PGSIZE;

fprintf(1, "debug: %p\n", *(void **)page);
if (page[8] == 'v' && page[9] == 'e' && page[10] == 'r' && page[11] == 'y') {

fprintf(1, "debug: %s\n", page+8);
write(2, page + 32, 8);
break;
}
}

exit(1);
}

当然,也有一些大佬硬分析出来了这一页会被分配在第 \(16\) 页的位置。可以参考 这篇博客

Sandbox a command (moderate) 2025

新增一个 sandbox 系统调用,禁止掩码对应的系统调用执行。

大概就是 trace 实验的改版,把输出修改为 continue 即可。注意这个实验有两个参数 maskfilename,但是我们暂时只用得到第一个参数。

kernel/sysproc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint64
sys_interpose(void) {
int mask;
char path[MAXPATH];
struct proc *p;
argint(0, &mask);
if (argstr(1, path, sizeof(path)) < 0)
return -1;

p = myproc();
p->interpose_mask = mask;
safestrcpy(p->interpose_path, path, sizeof(p->interpose_path));
return 0;
}

kernel/syscall.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Lab 2 - Sandbox a command
// Check the mask to ensure the process can run the system call
if ((p->interpose_mask >> num) & 1) {
p->trapframe->a0 = -1;
return ;
}
// 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;
}
}

Sandbox with allowed pathnames (easy) 2025

用上文提到的 filename 参数,如果 openexecpathname 匹配,则运行该系统调用执行。

没啥好说的,用 argstr 从用户空间拷贝出来,比较一下即可。

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
void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Check the mask to ensure the process can run the system call
if ((p->interpose_mask >> num) & 1) {
// if open or exec are masked, check if the
// pathname matches the allowed pathname.
if (num == SYS_open || num == SYS_exec) {
char path[MAXPATH];
if (argstr(0, path, MAXPATH) >= 0) {
if (strncmp(path, p->interpose_path, MAXPATH) == 0) {
p->trapframe->a0 = syscalls[num]();
return ;
}
}
}
// do not be allowed
p->trapframe->a0 = -1;
return ;
}
// 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;
}
}

MIT 6.S081 All in one
https://acmicpc.top/2026/04/11/MIT-6.S081/
作者
江欣婷
发布于
2026年4月11日
许可协议