[Lab Report] MIT 6.S081 Lab: system calls (v2022)
Using gdb
Looking at the backtrace output, which function called
syscall
?
We begin with launching gdb
:
$ make qemu-gdb
And follow the instructions step by step:
$ gdb-multiarch -x .gdbinit
...
(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 code above produces the following result:
#0 syscall () at kernel/syscall.c:163
#1 0x0000000080001d94 in usertrap () at kernel/trap.c:67
#2 0x0505050505050505 in ?? ()
It appears that syscall was called by usertrap, as indicated by the backtrace output.
What is the value of
p->trapframe->a7
and what does that value represent? (Hint: lookuser/initcode.S
, the first user program xv6 starts.)
We proceed by entering n
twice so that struct proc *p = myproc()
is excuted. Here’s the result of p/x *p
:
(gdb) n
(gdb) n
(gdb) p/x *p
$1 = {lock = {locked = 0x0, name = 0x80008178, cpu = 0x0}, state = 0x4, chan = 0x0, killed = 0x0, xstate = 0x0, pid = 0x1,
parent = 0x0, kstack = 0x3fffffd000, sz = 0x1000, pagetable = 0x87f73000, trapframe = 0x87f74000, context = {ra = 0x800014b8,
sp = 0x3fffffde80, s0 = 0x3fffffdeb0, s1 = 0x80008ea0, s2 = 0x80008a70, s3 = 0x1, s4 = 0x0, s5 = 0x3, s6 = 0x80019d40,
s7 = 0x8, s8 = 0x80019e68, s9 = 0x4, s10 = 0x1, s11 = 0x0}, ofile = {0x0 <repeats 16 times>}, cwd = 0x800171b0, name = {0x69,
0x6e, 0x69, 0x74, 0x63, 0x6f, 0x64, 0x65, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, syscall_trace = 0x0}
Now, check the value of p->trapframe->a7
:
(gdb) p p->trapframe->a7
$2 = 7
It can be inferred from the code in user/initcode.S
that the system call number to be executed is stored in register a7
:
start:
la a0, init
la a1, argv
li a7, SYS_exec
ecall
In this case, the system call number is 7. As we can tell from kernel/syscall.h
, this number actually represents SYS_exec
:
// System call numbers
...
#define SYS_exec 7
What was the previous mode that the CPU was in?
(gdb) p/t $sstatus
$4 = 100010
According to RISC-V privileged instructions, the SPP bit indicates the privilege level at which a hart was executing before entering supervisor mode. When a trap is taken, SPP is set to 0 if the trap originated from user mode, or 1 otherwise. When an SRET instruction (see Section 3.3.2) is executed to return from the trap handler, the privilege level is set to user mode if the SPP bit is 0, or supervisor mode if the SPP bit is 1; SPP is then set to 0.
As we can see, the value of the SPP bit is 0, which indicates that the privilege level before entering the kernel for the system call was user mode.
We proceed by replacing the statement num = p->trapframe->a7
with num = * (int *) 0
in the syscall
function of kernel/syscall.c
.
void
syscall(void)
{
...
struct proc *p = myproc();
// num = p->trapframe->a7;
num = * (int *) 0;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
...
Write down the assembly instruction the kernel is panicing at. Which register corresponds to the varialable
num
?
Running make qemu
produces the following panic:
xv6 kernel is booting
hart 2 starting
scause 0x000000000000000d
sepc=0x0000000080002078 stval=0x0000000000000000
panic: kerneltrap
sepc
here refers to the address of the code where the kernel experienced a panic. In this case we should search for 80002078
in kernel/kernel.asm
:
num = * (int *) 0;
80002078: 00002903 lw s2,0(zero) # 0 <_entry-0x80000000>
As we can see, it was indeed the num = * (int *) 0
statement that caused the kernel to panic. And corresponding assembly instruction is lw a3, 0(zero)
with num
being s2
.
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 ofscause
in RISC-V privileged instructions)
Rerun gdb
, and set a breakpoint at the location where the panic occurs:
(gdb) b *0x0000000080002078
Breakpoint 1 at 0x80002078: file kernel/syscall.c, line 168.
(gdb) c
Continuing.
Thread 1 hit Breakpoint 1, syscall () at kernel/syscall.c:168
168 num = * (int *) 0;
(gdb) layout asm
Entering “n”, and the panic will occur again. At this point, we can terminate the process and check the value of the scase
register:
(gdb) n
^C
Thread 1 received signal SIGINT, Interrupt.
panic (s=s@entry=0x80008380 "kerneltrap") at kernel/printf.c:127
127 ;
(gdb) p $scause
$1 = 13
According to RISC-V privileged instructions, the value 13 here represents a load page fault. In particular, an error occurred while loading data from memory address 0 into s2
. As we can see from the course textbook, address 0 does not map to the kernel space; rather, it begins at virtual address 0x80000000.
What is the name of the binary that was running when the kernel paniced? What is its process id (
pid
)?
This information can be obtained by printing the ‘name’ field of the ‘proc’ data structure:
(gdb) b syscall
Breakpoint 1 at 0x80002060: file kernel/syscall.c, line 163.
(gdb) c
Continuing.
Thread 1 hit Breakpoint 1, syscall () at kernel/syscall.c:163
163 {
(gdb) layout src
(gdb) n
(gdb) n
(gdb) p p->name
$1 = "initcode\000\000\000\000\000\000\000"
It can be observed that this user program is the initcode
, which happens to be the first process in xv6. Printing the proc
structure would allow one to examine additional information about this process:
(gdb) p *p
$2 = {lock = {locked = 0, name = 0x80008178 "proc", cpu = 0x0}, state = RUNNING, chan = 0x0, killed = 0, xstate = 0, pid = 1,
parent = 0x0, kstack = 274877894656, sz = 4096, pagetable = 0x87f73000, trapframe = 0x87f74000, context = {ra = 2147488952,
sp = 274877898368, s0 = 274877898416, s1 = 2147520160, s2 = 2147519088, s3 = 1, s4 = 0, s5 = 3, s6 = 2147589440, s7 = 8,
s8 = 2147589736, s9 = 4, s10 = 1, s11 = 0}, ofile = {0x0 <repeats 16 times>}, cwd = 0x800171b0 <itable+24>,
name = "initcode\000\000\000\000\000\000\000", syscall_trace = 0}
We can see that the pid
of initcode
is 1.
System call tracing
The task becomes trivial following the hints:
- Add
$U/_trace
toUPROGS
inMakefile
.
UPROGS=\
...
$U/_zombie\
$U/_trace\
- Add a prototype for the system call to
user/user.h
, a stub touser/usys.pl
, and a syscall number tokernel/syscall.h
.
// user/user.h
// system calls
...
int uptime(void);
int trace(int);
// user/usys.pl
...
entry("uptime");
entry("trace");
// kernel/syscall.h
// System call numbers
...
#define SYS_close 21
#define SYS_trace 22
- Add a
sys_trace
function inkernel/sysproc.c
that implements the new system call by remembering its argument in a new variable in theproc
structure inkernel/proc.h
. (Whilesys_trace
can technically be located in any file, placing it inkernel/sysproc.c
simplifies its classification.)
// kernel/proc.h
// Per-process state
struct proc {
...
char name[16]; // Process name (debugging)
uint64 syscall_trace;
};
// kernel/sysproc.c
uint64
sys_trace(void)
{
int mask;
argint(0, &mask);
myproc()->syscall_trace = mask;
return 0;
}
- Modify
fork
inkernel/proc.c
to copy the trace mask from the parent to the child process.
int
fork(void)
{
...
safestrcpy(np->name, p->name, sizeof(p->name));
np->syscall_trace = p->syscall_trace;
pid = np->pid;
...
}
- Modify the
syscall
function inkernel/syscall.c
to print the trace output.
// Prototypes for the functions that handle system calls.
...
extern uint64 sys_close(void);
extern uint64 sys_trace(void);
// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
static uint64 (*syscalls[])(void) = {
...
[SYS_close] sys_close,
[SYS_trace] sys_trace,
};
const char *syscall_names[] = {
...
[SYS_close] "close",
[SYS_trace] "trace",
};
void
syscall(void)
{
...
num = p->trapframe->a7;
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]();
if (p->syscall_trace & (1 << num)) {
...
}
Sysinfo
Likewise, the task becomes trivial following the hints.
- Add
$U/_sysinfotest
toUPROGS
inMakefile
.
UPROGS=\
...
$U/_trace\
$U/_sysinfotest\
- Add a prototype for the system call to
user/user.h
, a stub touser/usys.pl
, and a syscall number tokernel/syscall.h
.
// user/user.h
// system calls
...
int trace(int);
struct sysinfo;
int sysinfo(struct sysinfo *);
// user/usys.pl
...
entry("trace");
entry("sysinfo");
// kernel/syscall.h
// System call numbers
...
#define SYS_trace 22
#define SYS_sysinfo 23
- Add a
sys_sysinfo
function inkernel/sysproc.c
that copies astruct sysinfo
back to user space.
// kernel/sysproc.c
...
#include "proc.h"
#include "sysinfo.h"
...
uint64
sys_info(void)
{
uint64 addr;
argaddr(0, &addr);
struct sysinfo sinfo;
sinfo.freemem = freemem_size();
sinfo.nproc = count_proc();
if (copyout(myproc()->pagetable, addr, (char *)&sinfo, sizeof(sinfo)) < 0)
return -1;
return 0;
}
- Update the array of syscall names to index into in
kernel/syscall.c
.
// Prototypes for the functions that handle system calls.
...
extern uint64 sys_trace(void);
extern uint64 sys_info(void);
// An array mapping syscall numbers from syscall.h
// to the function that handles the system call.
static uint64 (*syscalls[])(void) = {
...
[SYS_trace] sys_trace,
[SYS_sysinfo] sys_info,
};
const char *syscall_names[] = {
...
[SYS_trace] "trace",
[SYS_sysinfo] "sysinfo",
};
- Add a function to
kernel/kalloc.c
to collect the amount of free memory.
uint64
freemem_size(void)
{
acquire(&kmem.lock); // prevent race condition
uint64 size = 0;
struct run *r = kmem.freelist;
while (r) {
size++;
r = r->next;
}
release(&kmem.lock);
return size * PGSIZE;
}
- Add a function to
kernel/proc.c
to collect the number of processes.
uint64
count_proc(void)
{
uint64 cnt = 0;
for (int i = 0; i < NPROC; ++i) {
if (proc[i].state != UNUSED) {
cnt++;
}
}
return cnt;
}
Remember to add the prototypes for the helper functions to kernel/defs.h
:
// kalloc.c
...
void kinit(void);
uint64 freemem_size(void);
...
// proc.c
...
uint64 count_proc(void);