6.S081 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

// 未完待续...