Lab_system_calls

Prepare work

4.3 Code: Calling system calls

user/initcode.S中,用户代码将exec的参数放入寄存器a0a1中,并将系统调用数放进a7

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

然后系统调用数将与kernel/syscall.c中的函数指针表匹配;

// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
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,
};

syscall从保存在trapframe中的a7中检索系统调用数,并用它来索引syscalls

而对于上面的系统调用,a7包含的系统调用对应在syscall.h中:

当系统调用实现函数并返回时,syscall将返回值记录在p->trapframe->a0中,原始用户空间调用exec即返回此值。通常返回负数表示错误,返回零或正数表示成功,若系统调用号无效则syscall会打印错误并返回-1

4.4 Code: System call arguments

用户代码与内核中系统调用直接传递参数可以通过寄存器,argintargaddr都从中检索第n个系统调用,它们调用argraw来读取寄存器。

但一些系统调用需要讲指针作为参数传递,例如exec系统调用向内核传递了一个指向用户空间中字符串参数的指针数组。内核实现了安全的在用户提供的地址间传递数据的功能,例如fetchstr,文件系统调用使用fetchstr从用户空间检索字符串文件名参数,然后调用copyinstr进行后面的具体处理

copyinstrpagetable中的虚拟地址srcva复制最多max字节到dst,它调用walkaddr在软件中遍历页表,以确定srcva的物理地址pa0。由于内核将所有物理RAM的地址映射到相同的内核虚拟地址,copyinstr可以直接将字符串字节从pa0复制到dstwalkaddr检查用户提供的虚拟地址是否属于进程的用户地址空间。copyout则用于将数据从内核复制到用户提供的地址。

相关源文件

  • The user-space "stubs" that route system calls into the kernel are in user/usys.S, which is generated by user/usys.pl when you run make. Declarations are in user/user.h 用户空间的“存根”将系统调用路由到内核中,这些存根位于 user/usys.S ,当你运行 make 时由 user/usys.pl 生成。声明位于 user/user.h

  • The kernel-space code that routes a system call to the kernel function that implements it is in kernel/syscall.c and kernel/syscall.h. 将系统调用路由到实现它的内核函数的内核空间代码位于 kernel/syscall.ckernel/syscall.h

  • Process-related code is kernel/proc.h and kernel/proc.c. 进程相关的代码是 kernel/proc.hkernel/proc.c

切换分支

开始实验

Using gdb (easy)

这个实验是学习GDB调试器的使用。运行前别忘了安装RISC-V架构的GDB调试工具:

在一个窗口中运行:

然后在另一个窗口中:

输入layout src将窗口分为两部分,显示gdb在源代码中的位置:

backtrace打印堆栈回溯:

Looking at the backtrace output, which function called syscall? 查看回溯输出,哪个函数调用了 syscall ?

通过backtrace查看栈帧,见上面,调用syscall的函数是usertrap

What is the value of p->trapframe->a7 and what does that value represent? (Hint: look user/initcode.S, the first user program xv6 starts.) p->trapframe->a7 的值是什么,这个值代表什么?(提示:查看 user/initcode.Sxv6 启动的第一个用户程序。)

输入几次n单步执行到num = p->trapframe->a7

然后输入p /x *p打印当前进程的proc struct的十六进制值:

查看kernel/proc.h,以下是一些关键字段的解释:

其中trapframea7字段通过proc.h的注释可知偏移量为168字节,那么有:

通过命令查看地址的实际值,在syscall.h中查找,0x7对应SYS_exec,符合系统正常工作的情况。

What was the previous mode that the CPU was in? CPU 之前处于什么模式?

打印特权寄存器,根据RISC-V特权架构手册,sstatus寄存器中的SSP位指示进入监管者模式前CPU处于什么模式。0表示用户模式,1表示监管模式。

Write down the assembly instruction the kernel is panicing at. Which register corresponds to the variable num? 写下内核崩溃时的汇编指令。哪个寄存器对应于变量 num ?

将 syscall 开头的语句 num = p->trapframe->a7替换为 num = * (int *) 0,然后运行:

kernel.asm中搜索sepc80001c1e,该文件包含编译内核的汇编代码。

这就是崩溃指令,即我们修改的那一行。

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? (See description of scause in RISC-V privileged instructions) 为什么内核会崩溃?提示:查看文本中的图 3-3;地址 0 在内核地址空间中是否映射? scause 中的值是否确认了这一点?(参见 RISC-V 特权指令中 scause 的描述)

我们现在需要观察处理器和内核在故障指令处的状态。首先启动gdb

在另一个窗口中:

设置断点后再运行:

指令lw a3,0(zero)尝试从地址0读取数据,对应代码num = * (int *) 0,根据下图xv6内核地址空间布局,地址0属于未映射区域。

再回去看scause=0xd(二进制1101),最高位1表示异常,低四位13对应Load page fault,而stval=0x0记录了故障地址为0。这二者结合直接可以验证地址0未映射,因为若映射但权限错误会有scause=0xF或其他。

What is the name of the process that was running when the kernel paniced? What is its process id (pid)? 内核恐慌时正在运行的进程名称是什么?它的进程 ID 是什么( pid )?

通过命令打印进程名称:

进程名为initcode,进程id为1

System call tracing (moderate)

In this assignment you will add a system call tracing feature that may help you when debugging later labs. You'll create a new trace system call that will control tracing. It should take one argument, an integer "mask", whose bits specify which system calls to trace. For example, to trace the fork system call, a program calls trace(1 << SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print a line when each system call is about to return, if the system call's number is set in the mask. The line should contain the process id, the name of the system call and the return value; you don't need to print the system call arguments. The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes. 在本作业中,您将添加一个系统调用跟踪功能,这可能有助于您在调试后续实验时使用。您将创建一个新的 trace 系统调用来控制跟踪。它应该接受一个参数,即一个整数“掩码”,其位指定要跟踪的系统调用。例如,要跟踪 fork 系统调用,程序调用 trace(1 << SYS_fork) ,其中 SYS_fork 是来自 kernel/syscall.h 的系统调用号。您需要修改 xv6 内核,以便在每个系统调用即将返回时,如果系统调用号在掩码中设置,则打印一行。该行应包含进程 ID、系统调用的名称和返回值;您不需要打印系统调用参数。 trace 系统调用应为调用它的进程及其随后 fork 的任何子进程启用跟踪,但不应影响其他进程。

首先解决编译问题,在user/user.h中添加prototypeint trace(int);然后在user/usys.pl中添加subentry("trace");最后在kernel/syscall.h中添加system call number#define SYS_trace 22。当然,也别忘了在Makefile中添加$U/_trace。这样在编译时,Makefile会调用 perl 脚本user/usys.pl生成user/usys.S,即实际的系统调用存根,它们使用RISC-Vecall指令切换到内核。

根据提示,先在kernel/proc.hproc结构体中加一个跟踪掩码trace_mask,然后在kernel/sysproc.c中实现系统调用:

然后再修改proc.cfork函数的代码,添加np->trace_mask = p->trace_mask,复制跟踪掩码;最后在kernel/syscall.c中,先添加一个系统调用名称数组,然后修改syscall函数增加打印跟踪输出的部分:

调用已经写好的用户程序trace.c进行验证:

通过测试。

Attack xv6 (moderate)

user/secret.c writes an 8-byte secret in its memory and then exits (which frees its memory). Your goal is to add a few lines of code to user/attack.c to find the secret that a previous execution of secret.c wrote to memory, and write the 8 secret bytes to file descriptor 2. You'll receive full credit if attacktest prints: "OK: secret is ebb.ebb". (Note: the secret may be different for each run of attacktest.) user/secret.c 在其内存中写入一个 8 字节的秘密,然后退出(这会释放其内存)。你的目标是在 user/attack.c 中添加几行代码,以找到之前执行的 secret.c 写入内存的秘密,并将这 8 个秘密字节写入文件描述符 2。如果 attacktest 打印出:“OK: secret is ebb.ebb”,你将获得满分。(注意:每次运行 attacktest 时,秘密可能不同。)

You are allowed to modify user/attack.c, but you cannot make any other changes: you cannot modify the xv6 kernel sources, secret.c, attacktest.c, etc. 你可以修改 user/attack.c ,但不能进行其他更改:你不能修改 xv6 内核源代码、secret.cattacktest.c 等。

The bug is that the call to memset(mem, 0, sz) at line 272 in kernel/vm.c to clear a newly-allocated page is omitted when compiling this lab. Similarly, when compiling kernel/kalloc.c for this lab the two lines that use memset to put garbage into free pages are omitted. The net effect of omitting these 3 lines (all marked by ifndef LAB_SYSCALL) is that newly allocated memory retains the contents from its previous use. 该漏洞在于编译此项目时,省略了在 kernel/vm.c 的第 272 行调用 memset(mem, 0, sz) 以清除新分配页面的操作。同样地,在为此实验编译 kernel/kalloc.c 时,使用 memset 将垃圾数据放入空闲页面的两行代码也被省略了。省略这三行(均标记为 ifndef LAB_SYSCALL )的净效应是新分配的内存保留了其先前使用的内容。

首先读一下secret.c的代码:

涉及到内存的分配情况:

即问题在于如何获取到写入密码的地址,最先想到的是尝试申请相同大小的内存分配,然后进行字符串匹配找到写密码的位置:

但是这种方式一直不对,去自学指南上看了下别人的问答,整体思路没问题,但是匹配的条件需要放宽一点,匹配一半即可:

运行通过:

user/secret.c copies the secret bytes to memory whose address is 32 bytes after the start of a page. Change the 32 to 0 and you should see that your attack doesn't work anymore; why not? user/secret.c 将秘密字节复制到页面开始后 32 字节的内存地址。将 32 改为 0,你应该会看到你的攻击不再起作用;为什么?

考虑内存分配和释放的机制,当secret进程释放内存时,它们会被添加到空闲链表中,在xv6kalloc实现中,kfree释放页面时,会在页面起始处存储空闲链表指针next,导致secret被破坏。而原32字节的偏移位于页面较后位置,避开了kalloc的元数据存储区。

到此Lab2就做完了。

Optional challenge exercises

  • Print the system call arguments for traced system calls (easy). 打印被跟踪系统调用的参数(简单)。

  • Find a bug in xv6 that allows an adversary to break process isolation or crash the kernel and let us know. (Side channels such as Meltdown are out of scope, although we will cover them in lecture.) 在 xv6 中找到一个允许对手破坏进程隔离或使内核崩溃的错误,并告知我们。(诸如 Meltdown 之类的侧信道攻击不在范围内,尽管我们会在讲座中讨论它们。)

Last updated