Lab_system_calls
Prepare work
4.3 Code: Calling system calls
在user/initcode.S中,用户代码将exec的参数放入寄存器a0和a1中,并将系统调用数放进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
用户代码与内核中系统调用直接传递参数可以通过寄存器,argint、argaddr都从中检索第n个系统调用,它们调用argraw来读取寄存器。
但一些系统调用需要讲指针作为参数传递,例如exec系统调用向内核传递了一个指向用户空间中字符串参数的指针数组。内核实现了安全的在用户提供的地址间传递数据的功能,例如fetchstr,文件系统调用使用fetchstr从用户空间检索字符串文件名参数,然后调用copyinstr进行后面的具体处理
copyinstr从pagetable中的虚拟地址srcva复制最多max字节到dst,它调用walkaddr在软件中遍历页表,以确定srcva的物理地址pa0。由于内核将所有物理RAM的地址映射到相同的内核虚拟地址,copyinstr可以直接将字符串字节从pa0复制到dst,walkaddr检查用户提供的虚拟地址是否属于进程的用户地址空间。copyout则用于将数据从内核复制到用户提供的地址。
相关源文件
The user-space "
stubs" that route system calls into the kernel are inuser/usys.S, which is generated byuser/usys.plwhen you run make. Declarations are inuser/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.candkernel/syscall.h. 将系统调用路由到实现它的内核函数的内核空间代码位于kernel/syscall.c和kernel/syscall.h。Process-related code is
kernel/proc.handkernel/proc.c. 进程相关的代码是kernel/proc.h和kernel/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->a7and what does that value represent? (Hint: lookuser/initcode.S, the first user programxv6starts.)p->trapframe->a7的值是什么,这个值代表什么?(提示:查看user/initcode.S,xv6启动的第一个用户程序。)
输入几次n单步执行到num = p->trapframe->a7:

然后输入p /x *p打印当前进程的proc struct的十六进制值:
查看kernel/proc.h,以下是一些关键字段的解释:
其中trapframe的a7字段通过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中搜索sepc值80001c1e,该文件包含编译内核的汇编代码。
这就是崩溃指令,即我们修改的那一行。
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
tracesystem 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 theforksystem call, a program callstrace(1 <<SYS_fork), whereSYS_forkis a syscall number fromkernel/syscall.h. You have to modify thexv6kernel 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. Thetracesystem 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中添加prototype即int trace(int);然后在user/usys.pl中添加sub即entry("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.h的proc结构体中加一个跟踪掩码trace_mask,然后在kernel/sysproc.c中实现系统调用:
然后再修改proc.c中fork函数的代码,添加np->trace_mask = p->trace_mask,复制跟踪掩码;最后在kernel/syscall.c中,先添加一个系统调用名称数组,然后修改syscall函数增加打印跟踪输出的部分:
调用已经写好的用户程序trace.c进行验证:
通过测试。
Attack xv6 (moderate)
user/secret.cwrites an 8-byte secret in its memory and then exits (which frees its memory). Your goal is to add a few lines of code touser/attack.cto find the secret that a previous execution ofsecret.cwrote to memory, and write the 8 secret bytes to file descriptor 2. You'll receive full credit ifattacktestprints: "OK: secret is ebb.ebb". (Note: the secret may be different for each run ofattacktest.)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.c、attacktest.c等。The bug is that the call to
memset(mem, 0, sz)at line 272 inkernel/vm.cto clear a newly-allocated page is omitted when compiling this lab. Similarly, when compilingkernel/kalloc.cfor this lab the two lines that usememsetto put garbage into free pages are omitted. The net effect of omitting these 3 lines (all marked byifndef 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.ccopies 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进程释放内存时,它们会被添加到空闲链表中,在xv6的kalloc实现中,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