6.1810 Lab - System Calls

实验指导:Lab - System Calls

实验任务

Using gdb

Looking at the backtrace output, which function called syscall?

bt 之后可以看出是 usertrap()。

What is the value of p->trapframe->a7 and what does that value represent?

  • initCode.S 中可以看到 li a7, SYS_execa7 寄存器中存放的是 system call 的 ID。
  • 在执行完 *p = myproc() 之后 p /x $a7 打印出来的值是 0x7,查看系统调用表,它代表 SYS_fstat
kernel/syscall.c
1
2
3
4
5
6
7
8
9
10
11
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, // The 0x7-th syscall
// ...
};

What was the previous mode that the CPU was in?

RISC-V privileged instructions 表示 「When a trap is taken, SPP is set to 0 if the trap originated from user mode, or 1 otherwise」。于是我们要看 SPP 位,SPP 位是 sstatus 寄存器的第八位。p /x $sstatus1 的结果是 0x20000022,第八个比特位是 0,于是可以确认是 user mode。

Write down the assembly instruction the kernel is panicing at. Which register corresponds to the variable num?

在 kernel.asm 中可以看到是这条:80001c6c: 00002683 lw a3,0(zero) # 0 <_entry-0x80000000>。指令是 lw,寄存器是 a3。

Why does the kernel crash? Hint: look at figure 3-3 in the text; is address 0 mapped in the kernel address space? Is that confirmed by the value in scause above?

VMA 0x0 (PMA 0x80000000) 在内核空间。scause = 0xd 是 Load page fault。应该是因为在用户态不能 dereference 内核空间的地址。

What is the name of the process that was running when the kernel paniced? What is its process id (pid)?

PID 是 0x1,进程名是 initcode。

System call tracing

这个 task 要求实现一个 syscall 来 trace 给定的 syscall。例如 trace 32 grep hello README 可以打印出 grep hello README 的进程和它所有的子进程的 read 调用,32 是 read 的掩码。这里的掩码(mask)可以用一个 32 位的数表示,每一位代表一个 syscall。xv6 总共的 syscall 的数量只有不到 32 个。

整理一下 xv6 syscall 的流程:

  • 用户程序调用 user/user.h 中的 syscall wrapper。这些 wrapper 的实现代码在 user/usys.S 中,这个汇编代码由 user/usys.pl 生成。
  • user/usys.pl 生成代码用 ecall 指令系统调用。
  • $stvec 寄存器中保存着响应代码的地址,当 ecall 被触发后跳转到 $stvec 指向的代码,即 tampoline.S。这一步是由硬件来完成的。在真正跳转到 $stvec 之前硬件会自动保存一些硬件上下文,例如 $sepc / $sstatus / $scause
  • trampoline 的代码会保存用户上下文,然后跳转到 kernel/trap.c:usertrap
  • kernel/trap.c:usertrap 调用 kernel/syscall.c:syscall 函数。这也验证了我们在第一个任务中看到的。
  • kernel/syscall.c:syscall 通过查表来确定和调用系统调用函数。
  • kernel/syscall.c:syscall 使用 kernel/syscall.c:usertrapret 结束调用,跳转到 trampoline 中的 userret
  • userret 恢复用户状态,调用 sret 恢复硬件上下文($sepc / $sstatus / $scause)并跳转到用户代码。
Xv6 System Call Flow
1
2
3
4
5
6
7
8
9
10
user/user.h:syscall_wrapper_func
-> usys.S (generated by usys.pl)
-> ecall
-> $stvec (address of trampoline)
-> kernel/trampoline.S:trampoline
-> kernel/trap.c:usertrap
-> kernel/syscall.c:syscall
-> kernel/syscall.c:usertrapret
-> kernel/trampoline.S:userret
-> Go back to user mode code

所以,要给 xv6 加一个 trace 的 system call 需要首先在 user/user.h 中加入一个 wrapper 函数声明:

user/user.h
1
int trace(int);

接着在 user/usys.pl 中加入一个入口点,让构建程序可以生成这个 wrapper 函数的定义:

user/usys.pl
1
entry("trace");

编译之后可以看到 user/usys.S 中生成了函数的定义:

user/usys.S
1
2
3
4
trace:
li a7, SYS_trace
ecall
ret

对于 trampoline 和 usertrap 我们无需修改,下一步我们考虑让 kernel/syscall.c 支持 SYS_trace。首先在 kernel/syscall.h 中定义 SYS_trace

kernel/syscall.h
1
#define SYS_trace 22

然后在系统调用表中加入对这个函数的支持:

kernel/syscall.c
1
2
3
4
5
6
7
8
extern uint64 sys_trace(void);

// ...

static uint64 (*syscalls[])(void) = {
// ...
[SYS_trace] = sys_trace,
};

下面来实现这个 syscall 的功能。这个 syscall 需要把 mask 放入 proc 结构中,好让其他 syscall 发生的时候查询 proc 结构中的 mask,来决定是否要 log 这个 syscall。

kernel/proc.h
1
2
3
4
struct proc {
// ...
uint32 trace_mask;
};
kernel/sysproc.c
1
2
3
4
5
6
7
8
uint64
sys_trace(void)
{
int mask;
argint(0, &mask);
myproc()->trace_mask = mask;
return 0;
}

接下来考虑 fork 的情况,让子进程也支持 trace 给定的 syscall,于是我们要在 fork 的时候把 mask 复制给子进程的 proc 结构。

kernel/proc.c
1
2
3
4
5
6
7
8
int
fork(void)
{
// ...
// Copy the trace mask.
np->trace_mask = p->trace_mask;
return pid;
}

最后在 kernel/syscall.c:syscall 中根据条件来打印 trace:

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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static char* syscalls_name[] = {
[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
syscall(void)
{
int num;
struct proc *p = myproc();

// ...

if (p->trace_mask && (p->trace_mask & (1 << (num)))) {
printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], (int)p->trapframe->a0);
}
}

Attack xv6

xv6 kernel 的 kalloc / kfree 用一个链表管理页面的分配和回收。如果 kfree 不清空页面的数据,在这个页面下一次被 kalloc 出去的时候,当前拥有这个页面的人就可以读到上一个人写入这个页面的数据。这个题让我们基于这样一个原理,来实现 attack,窃取上一个用户的 secret。

这个题很有意思。我最初刷 syscall 实验的时候这个题卡住了,我是把 6.1810 所有 9 个实验全都刷完之后再回过头来看这个实验。

kalloc / kfree 遵循 FILO 的原则,也就意味着 secret 的第 10 个 page 在 attack 里面会是另一个 index。我一开始觉得这个 page 的 index 应该是固定的,我十分执着于找到这个 index。我在分别运行 secret 和 attack 的时候其实找到了这个 index,但是发现它并不适用于 attacktest:因为 attacktest 这个程序 fork 了两次,并非分别执行 secret 和 attack,「谁能拿到这个 secret 页面」这件事情就变得更加复杂一些。 

我转而开始研究 attacktest 这个程序。通过打表的方法,我发现其实在 attacktest 这个程序里这个 page 的 index 其实也是固定的(只是和分别执行两个程序不太一样),并且我还搞到了这个 index,但是我觉得我需要理解为什么是这个 index。这就涉及到要分析每个 user program 分配 page 的行为,比如说我们知道有一些页面会被 fork 的时候 copy 到新的进程(比如 stack / heap / trapframe 等等)涉及新的 kalloc,有的不涉及 kalloc(比如 trampoline),进程结束的时候会有 kfree 的动作。于是我的思路变成了一层一层地分析每个 page 是什么时候被 kalloc / kfree 的,在纸上模拟出来。这条路是理论可行的,不过有些烧脑子。尝试梳理这个过程,未果,经常就是把自己绕进去了。

其实在此之前我还关注到了 secret 程序在打印真正的 secret 之前,还打印了“my very very very secret pw is:”,这个就在 page 开头。于是我就找哪个 page 开头是这样的字符串,结果还是没找着,当时我就纳了闷儿了。想了很久,这条路也没走通,就暂时放弃开始做后面的 labs 了。

时隔一阵子,我做完了 mmap,再回来看这个题,又开始往这两个方向尝试。可能是对 kernel 的 code 更熟悉了,这次我也想了很久,但是最终意识到 kfree 把一个 page 归还到链表上的时候,一个 page 的前 8 个字节是会被改掉的!(因为要作为 freelist 的指针)恍然大悟。这是这个题最坑的地方。这里需要理解 kalloc.c 中 kalloc / kfree 的原理,这个在 page table 实验的博文中我贴了个图。(更详细的内容在 6.1810 Lab - Page Tables)

于是只要从第 8 位之后开始匹配就行了。

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

char *prefix = "my very very very secret pw is:";

int
main(int argc, char *argv[])
{
// your code here. you should write the secret to fd 2 using write
// (e.g., write(2, secret, 8)

char *end = sbrk(100 * PGSIZE);
int len = strlen(prefix);

for (int i = 0; i < 100; ++i) {
char *p = end + i * PGSIZE;
for (int j = 8; j < len; ++j) {
if (p[j] != prefix[j]) {
goto next;
}
}

write(2, p + 32, 8);

next:
continue;
}

exit(1);
}

实验结果