前言
在这个lab中我们会实现一些系统调用,这些系统调用类似于我们使用的write,它实际上是用户层的一个接口,而他真正的实现是在内核中实现的,由kernel来实现的,中间我们需要trap陷入内核,内核执行完,用户层就能够获得对应的结果
Using gdb
题目
In many cases, print statements will be sufficient to debug your kernel, but sometimes being able to single step through some assembly code or inspecting the variables on the stack is helpful.
To learn more about how to run GDB and the common issues that can arise when using GDB, check out this page.
To help you become familiar with gdb, run make qemu-gdb and then fire up gdb in another window (see the gdb bullet on the guidance page). Once you have two windows open, type in the gdb window:
(gdb) b syscall
Breakpoint 1 at 0x80002142: file kernel/syscall.c, line 243.
(gdb) c
Continuing.
[Switching to Thread 1.2]
Thread 2 hit Breakpoint 1, syscall () at kernel/syscall.c:243
243 {
(gdb) layout src
(gdb) backtrace
The layout command splits the window in two, showing where gdb is in the source code. The backtrace prints out the stack backtrace. See Using the GNU Debugger for helpful GDB commands.
Answer the following questions (store the answers in answers-syscall.txt):
Looking at the backtrace output, which function called syscall?
Type n a few times to step pass struct proc *p = myproc(); Once past this statement, type p /x *p, which prints the current process’s proc struct (see kernel/proc.h>) in hex.
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.)
The processor is running in kernel mode, and we can print privileged registers such as sstatus (see RISC-V privileged instructions for a description):
(gdb) p /x $sstatus
What was the previous mode that the CPU was in?
In the subsequent part of this lab (or in following labs), it may happen that you make a programming error that causes the xv6 kernel to panic. For example, replace the statement num = p->trapframe->a7; with num = * (int *) 0; at the beginning of syscall, run make qemu, and you will see:
xv6 kernel is booting
hart 2 starting
hart 1 starting
scause 0x000000000000000d
sepc=0x000000008000215a stval=0x0000000000000000
panic: kerneltrap
Quit out of qemu.
To track down the source of a kernel page-fault panic, search for the sepc value printed for the panic you just saw in the file kernel/kernel.asm, which contains the assembly for the compiled kernel.
Write down the assembly instruction the kernel is panicing at. Which register corresponds to the varialable num?
To inspect the state of the processor and the kernel at the faulting instruction, fire up gdb, and set a breakpoint at the faulting epc, like this:
(gdb) b *0x000000008000215a
Breakpoint 1 at 0x8000215a: file kernel/syscall.c, line 247.
(gdb) layout asm
(gdb) c
Continuing.
[Switching to Thread 1.3]
Thread 3 hit Breakpoint 1, syscall () at kernel/syscall.c:247
Confirm that the faulting assembly instruction is the same as the one you found above.
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)
Note that scause was printed by the kernel panic above, but often you need to look at additional info to track down the problem that caused the panic. For example, to find out which user process was running when the kernel paniced, you can print out the process’s name:
(gdb) p p->name
What is the name of the binary that was running when the kernel paniced? What is its process id (pid)?
This concludes a brief introduction to tracking down bugs with gdb; it is worth your time to revisit Using the GNU Debugger when tracking down kernel bugs. The guidance page also has some other other useful debugging tips.
分析
-
Q:Looking at the backtrace output, which function called syscall?
A:我们从backtrace的调用链可以发现这里的usertrap()调用了syscall
-
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.)
我们从syscall.h里面可以发现7代表的就是exec的系统调用号 -
What was the previous mode that the CPU was in?
sstatus的第8位如果是0,说明他之前是从用户态切换过来的,如果是1说明他之前是从内核切换过来的
0x22的第8位=0,所以他是之前是在用户态
-
Write down the assembly instruction the kernel is panicing at. Which register corresponds to the varialable num?
我们现在把那个代码修改成0,运行出来的结果是
sepc就是在trap的时候,从哪个地方进行陷入,我们只需要跳转到这个位置即可
我们跳转到了sepc这个位置,去查看是哪里的代码出现了问题
发现就是那一句num=(int)0s2里面就是对应num那个变量
-
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)
首先在虚拟地址映射的时候0,并没有被映射到任何地址上,我们在程序中使用的地址,所有都是虚拟地址
所以,自然无法获取他对应的值 -
What is the name of the binary that was running when the kernel paniced? What is its process id (pid)?
system call tracing
题目
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 out 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.
We provide a trace user-level program that runs another program with tracing enabled (see user/trace.c). When you’re done, you should see output like this:
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
…
$
In the first example above, trace invokes grep tracing just the read system call. The 32 is 1<<SYS_read. In the second example, trace runs grep while tracing all system calls; the 2147483647 has all 31 low bits set. In the third example, the program isn’t traced, so no trace output is printed. In the fourth example, the fork system calls of all the descendants of the forkforkfork test in usertests are being traced. Your solution is correct if your program behaves as shown above (though the process IDs may be different).
Some hints:
-
Add $U/_trace to UPROGS in Makefile
-
Run make qemu and you will see that the compiler cannot compile user/trace.c, because the user-space stubs for the system call don’t exist yet: add a prototype for the system call to user/user.h, a stub to user/usys.pl, and a syscall number to kernel/syscall.h. The Makefile invokes the perl script user/usys.pl, which produces user/usys.S, the actual system call stubs, which use the RISC-V ecall instruction to transition to the kernel. Once you fix the compilation issues, run trace 32 grep hello README; it will fail because you haven’t implemented the system call in the kernel yet.
-
Add a sys_trace() function in kernel/sysproc.c that implements the new system call by remembering its argument in a new variable in the proc structure (see kernel/proc.h). The functions to retrieve system call arguments from user space are in kernel/syscall.c, and you can see examples of their use in kernel/sysproc.c.
-
Modify fork() (see kernel/proc.c) to copy the trace mask from the parent to the child process.
-
Modify the syscall() function in kernel/syscall.c to print the trace output. You will need to add an array of syscall names to index into.
-
If a test case passes when you run it inside qemu directly but you get a timeout when running the tests using make grade, try testing your implementation on Athena. Some of tests in this lab can be a bit too computationally intensive for your local machine (especially if you use WSL).
分析与代码
这个任务中,我们会实现一个系统调用去追踪一个我们想要专追踪的系统调用,当他被调用的时候,就会打印出来内容,我就直到在我们运行的程序里面使用了这个系统调用,父进程要去追踪,他的子进程也需要去追踪是否调用这个系统调用,我们设置的mask的当中2进程的第几个1,对应的就是我们要追踪的系统调用,同时输出一行(pid,syscallname,retofsyscall)
- 添加_trace到makefile里面
UPROGS=\
...
$U/_trace\
-
我们需要在user.h里面添加trace函数的定义
int trace(int);//声明这个系统调用函数
-
在usy.pl添加一个一个函数的存根,使得我们能够进入这个系统调用
entry("trace");
-
在syscall.h里面添加函数的系统调用号
#define SYS_trace 22
-
在sysproc.c里面添加sys_trace的函数实现,在process进程结构体里面添加要追踪的函数调用号掩码,这样在这个进程调用系统调用时候,就能够判断调用了那些系统调用了
proc.hstruct proc { ... int mask; //trace 使用的掩码,系统调用都设置在这个mask里面,第一个bit位就是系统调研参数为1 };
sysproc.c
uint64 sys_trace(void) { return 0; }
-
我们在sys_trace里面需要去检索用户的系统调用参数,使用
argint
可以获取用户的int类型参数,将获得的mask掩码设置进process里面uint64 sys_trace(void) { int mask; argint(0, &mask); //从内核中获取参数int参数使用这个函数,把获得的数值放到mask里面 myproc()->mask = mask; //把这个掩码进行设置,这个数值就是proc里面的mask,设置进去 return 0; }
-
在fork函数里面复制对应的掩码,因为fork会生成一个新的进程,我们需要他生成的新进程调用系统调用的时候,也能够追踪到
int fork(void)
{
...
np->mask = p->mask;
...
}
- 在syscall.c里面添加一个系统调用的数组
static char *syscallname[] = {
"",
"fork",
"exit",
"wait",
"pipe",
"read",
"kill",
"exec",
"fstat",
"chdir",
"dup",
"getpid",
"sbrk",
"sleep",
"uptime",
"open",
"write",
"mknod",
"unlink",
"link",
"mkdir",
"close",
"trace",
};//系统调用名,静态修饰,只能在这个文件里面调用
- 在syscall.c里面进行判断,如果mask和此时正在进行的系统调用号&,为1,说明是我们需要追踪的系统调用,所以我们需要打印他的进程pid,系统调用名,以及他的返回值
void syscall(void)
{
...
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]();//把系统调用的返回数值放到a0里面
//系统调用执行完,打印一个消息
if ((1 << num) & p->mask) //把他按位与以下,判断是否在mask里面,从里面检索,发现只要有,里面设置的系统调用,就触发这个地方,打印出来
{
printf("%d: syscall %s -> %d\n", p->pid, syscallname[num], p->trapframe->a0); //进程id,系统调用的名称,返回值
}
}
...
}
Sysinfo
题目
In this assignment you will add a system call, sysinfo, that collects information about the running system. The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h). The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED. We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.
Some hints:
-
Add $U/_sysinfotest to UPROGS in Makefile
-
Run make qemu; user/sysinfotest.c will fail to compile. Add the system call sysinfo, following the same steps as in the previous assignment. To declare the prototype for sysinfo() in user/user.h you need predeclare the existence of struct sysinfo:
struct sysinfo;
int sysinfo(struct sysinfo *);
Once you fix the compilation issues, run sysinfotest; it will fail because you haven’t implemented the system call in the kernel yet.
-
sysinfo needs to copy a struct sysinfo back to user space; see sys_fstat() (kernel/sysfile.c) and filestat() (kernel/file.c) for examples of how to do that using copyout().
-
To collect the amount of free memory, add a function to kernel/kalloc.c
-
To collect the number of processes, add a function to kernel/proc.c
分析与代码
这个任务我们需要收集正在运行的系统的信息,系统调用的参数是伊戈尔struct sysinfo结构体,里面一个nproc就是我们没有被设置的进程数,freemem就是没有被使用的内存大小,这两个都是我们在内核需要设置的
- 我们重复上面操作,把系统调用函数声明定义
- sysinfo需要将struct sysinfo这个结构体拷贝回用户层,使用
copyout()
- 我们应该在kalloc.c 里面收集自由的内存
- 我们需要在proc.c 里面收集UNSET的进程数
kernel/proc,c
收集的时候就遍历所有的进程,在操作的时候要上锁,避免因为多线程操作出现的脏读
void unsetprocnum(uint64* nproc)//收集进程数
{
// //收集进程数字
*nproc=0;
struct proc *p;
for (p = proc; p < &proc[NPROC]; p++)
{
acquire(&p->lock);//上锁进行下面一系列操作
if (p->state != UNUSED)//收集进程为UNUSED的数
{
(*nproc)++;
}
release(&p->lock);
}
}
kernel/kalloc.c
我们就是自由内存链表里面查找没有用过的内存,这里的每个都是以一页为单位
void colletmemory(uint64 *freemem)
{
//统计空闲的内存量大小
*freemem = 0;
struct run *r;
acquire(&kmem.lock);//上锁
r = kmem.freelist; //获得空闲的链表
while (r)
{
(*freemem) += PGSIZE;
r = r->next;
}
release(&kmem.lock);
}
在sysproc.c的sys_info()
里面调用这两个函数
我们从用户态获得了用户要填充的结构体,我们在内核台中进行赋值再传回到用户态即可
uint64
sys_sysinfo(void)
{
struct sysinfo si;
unsetprocnum(&si.nproc); //从内核里面获取空闲进程数,放到结构体里面
colletmemory(&si.freemem); //从内核里面获取红线的内存量
uint64 addr; //用户指针指向struct sysinfo,获得虚拟地址
argaddr(0, &addr);//
struct proc *p = myproc();
if (copyout(p->pagetable, addr, (char *)&si, sizeof(si)) < 0)//把从内核获得的参数,拷贝给用户态
{
return -1;
}
return 0;
}