Lab: system calls

In the last lab you used system calls to write a few utilities. In this lab you will add some new system calls to xv6, which will help you understand how they work and will expose you to some of the internals of the xv6 kernel. You will add more system calls in later labs.

Before you begin lab system calls, read Chapter 2 of the xv6 book, and Sections 4.3 and 4.4 of Chapter 4, and related source files:

In this lab, there are several questions for you to answer. Questions are in boxes with a light orange background. Write each question and its answer in your notebook. Take photo(s) of your questions/answers and submit the photo(s) on Canvas.

In the directory of your xv6-labs, create two files: answers-syscall.txt and time.txt that the grading script looks for. You can create these files by the following:

$ echo > answers-syscall.txt
$ echo 10 > time.txt
I use the information in your photo files and your lab-syscall-handin.txt file that you submit on Canvas. I have retained these files and this grading script approach in case I want to use it in the future.

Xv6 has a small collection of system calls, which are used to create utility programs. Using the system calls, you can create programs that open, close, and read files; create and delete files and directories; create processes; communicate between processes using pipes, read, and write; and execute programs. The API for Xv6 system calls are shown in yellow in the following table. In this lab you create the Xv6 system calls shown in red.

Xv6 System CallDescription
int fork()
Create a process, return child's PID.
int exit(int status)
Terminate the current process; status reported to wait(). No return. Wait for a child to exit; exit status in *status; returns child PID. Terminate process PID. Returns 0, or -1 for error.
int wait(int *status)
Wait for a child to exit; exit status in *status; returns child PID.
int kill(int pid)
Terminate process PID. Returns 0, or -1 for error.
int getpid()
Return the current process's PID.
int sleep(int n)
Pause for n clock ticks.
int exec(char *file, char *argv[])
Load a file and execute it with arguments; only returns if error.
char *sbrk(int n)
Grow process's memory by n bytes. Returns start of new memory.
int open(char *file, int flags)
Open a file; flags indicate read/write; returns an fd (file descriptor).
int write(int fd, char *buf, int n)
Write n bytes from buf to file descriptor fd; returns n.
int read(int fd, char *buf, int n)
Read n bytes into buf; returns number read; or 0 if end of file. Release open file fd.
int close(int fd)
Release open file fd.
int dup(int fd)
Return a new file descriptor referring to the same file as fd.
int pipe(int p[])
Create a pipe, put read/write file descriptors in p[0] and p[1]. Change the current directory.
int chdir(char *dir)
Change the current directory.
int mkdir(char *dir)
Create a new directory.
int mknod(char *file, int, int)
Create a device file.
int fstat(int fd, struct stat *st)
Place info about an open file into *st.
int stat(char *file, struct stat *st)
Place info about a named file into *st.
int link(char *file1, char *file2)
Create another name (file2) for the file file1.
int unlink(char *file)
Remove a file.
int rseed(int seed)
Seeds the random integer generator rinter.
int rinter(int max)
Returns a random integer between 0 and max.
int trace(int mask)
Enables tracing of Xv6 system calls which numbers match bits in mask.
int sysinfo(struct sysinfo *info)
Return system information - number of free bytes and number of procs.

Reference: Xv6 System Calls

This section provides a reference of steps to add system calls to Xv6. You do not have to study these steps prior to performing the lab, becuase the individual portions of the lab, contain instructions for performing these steps. In later labs, when you have to add a system call, you can refer to these steps. There is not a specific order to these steps; however, they all must be completed in order to run Xv6 with the newly added system call. In the following steps, a ficticious system call dosys is added.

Starting the Lab

To start the lab, switch to the syscall branch:

  $ git fetch
  $ git checkout syscall
  $ make clean
  

If you run make grade you will see that the grading script cannot exec trace and sysinfotest. Your job is to add the necessary system calls and stubs to make them work.

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 the following.

To help you become familiar with gdb, run make qemu-gdb and then fire up gdb in another window (see GDB Sample Sessions). 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.

1. 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.

You will notice that a proc struct has a member trapframe which contains the proc's registers when a trap is processed. The value that p /x *p prints is the address of the trapframe member. To view the contents of trapframe, you must enter p /x *p->trapframe. This will show you the hex values of the registers saved on the trapframe. Alternatively, you can view the value of register a7, by entering p /x (*p->trapframe)->a7. Examine the use of parentheses in the command to disply the value of the register a7 in the trapframe structure. You may want to try entering p /x *p->trapframe->a7 to help you better understand dereferencing C pointers.

2. 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
  

3. 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=0x000000008000207e stval=0x0000000000000000
panic: kerneltrap
  
Quit out of qemu.

The privileged register scause contains the cause of the trap. When a trap is taken into S-mode, scause is written with a code indicating the event that caused the trap. Lookup what the value of 0xd (13) in scause means (see RISC-V privileged instructions for a description).

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.

4. 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. Note, be sure the use the address you saw displayed in the panic: kerneltrap.

(gdb) b *0x000000008000207e
Breakpoint 1 at 0x8000207e: 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
(gdb) p /x $scause

5. Confirm that the faulting assembly instruction is the same as the one you found above.

6. Why does the kernel crash? Hint: look at figure 3-3 in the text; is address 0 mapped in the kernel address space? What is the value in scause above? Does this value confirn why the kernel crashed. (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
  

7. 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.

This section and the links at the top of this section contain helpful information on gdb.

System calls for Random Integers

In this assignment you will add system calls rseed and rinter that seed a random integer generator and generate random integers. This assignment introduces you to the Xv6 system call mechanism. You'll create a new rseed system call that will seed the random integer generator. It shall take one argument, an integer "seed", that establishes the starting seed for the Pseudo Random Number Generator (PRNG). You'll create a new rinter system call that will return an pseudo random integer. It shall take one argument, an integer max such that the random number returned is between 0 and max minus one. Both rseed and rinter shall return -1 on an error.

The following are your requirements for rseed and rinter

Some Hints

8. The system calls for random numbers were a nice introduction to system calls. You learned how to add OS system calls to the Xv6 OS. Do they have to be system calls? Examine the files kernel/string.c and user/umalloc.c and how they fit into Xv6 before answering this question.

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.

Let's first consider the int trace(int) function prototype that is added to user.h. The return code is 0 for success and -1 for failure. The argument is an integer, which is a 32-bit quantity. Each bit corresponds to a system call number, which is defined in syscall.h. For example, bit 1 specifies to trace fork system calls because fork has number 1 as its system call number. This means if you make a call trace(2), you are tracing fork system calls because 2 has bit 1 set. Another example is trace(0x20) traces read system calls because read has number 5 as its system call number and 0x20 has bit 5 set. The C expression 1<<5 results in the number 0x20, because 1 is shifted 5 bits to the left. If you want to trace both fork and read system calls, you would call trace(0x22) which sets bits 1 and 5.

When calling the trace user program from the command line, the examples show the first parameter as in decimal integer. The samples provided discuss translating this decimal integar parameter to specific system calls.

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:

Some Hints

Many of these hints are similar (but maybe briefer) to the hints for the system calls for random numbers.

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

9. Why must the kernel call copyout?

Submit the lab

This completes the lab. Make sure you pass all of the make grade tests. Read Lab Submissions for instructions on how to submit your lab.