..

[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: look user/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 of scause 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:

  1. Add $U/_trace to UPROGS in Makefile.
UPROGS=\
	...
	$U/_zombie\
	$U/_trace\
  1. 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.
// 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
  1. 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 in kernel/proc.h. (While sys_trace can technically be located in any file, placing it in kernel/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;
}
  1. Modify fork in kernel/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;
  ...
}
  1. Modify the syscall function in kernel/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.

  1. Add $U/_sysinfotest to UPROGS in Makefile.
UPROGS=\
	...
	$U/_trace\
	$U/_sysinfotest\
  1. 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.
// 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
  1. Add a sys_sysinfo function in kernel/sysproc.c that copies a struct 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;
}
  1. 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",
};

  1. 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;
}
  1. 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);