Lab alarm: System calls & upcalls

This lab will familiarize you with the implementation of system calls and upcalls. In particular, you will implement new system calls (sigalarm and sigreturn).

Trap handling in the xv6 book

Before you start coding, read §4, Traps and system calls of the xv6 book, and related source files:

  • kernel/trampoline.S: the assembly involved in changing from user space to kernel space and back
  • kernel/trap.c: code handling all interrupts

To start the lab, update your repository and create a new branch for your solution:

$ git fetch origin
$ git checkout -b alarm origin/xv6-21au
$ make clean

RISC-V assembly

It will be important to understand a bit of RISC-V assembly. There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program in user/call.asm.

Read the code in user/call.asm for the functions g, f, and main. The instruction manual for RISC-V is on the readings page. Here are some questions. You don’t need to submit answers to the questions in this lab. Do answer them for yourself though!

Question

  • Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
  • Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)
  • At what address is the function printf located?
  • What value is in the register ra just after the jalr to printf in main?

Question

Run the following code.

unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

What is the output? Here’s an ASCII table that maps bytes to characters.

The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian, what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

Here’s a description of little- and big-endian and a more whimsical description.

Question

In the following code, what is going to be printed after y=? (note: the answer is not a specific value.) Why does this happen?

printf("x=%d y=%d", 3);

Question

For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred. This is implemented by the backtrace() function in kernel/printf.c.

Suppose that we insert a call to backtrace() in sys_sleep (kernel/sysproc.c) and run sleep from your lab util solution, which calls sys_sleep. Your output should be as follows:

[<0x0000000080002df0>]
[<0x0000000080002cca>]
[<0x0000000080002996>]

If you run make qemu-trace instead of make qemu, you should see something that looks like a Linux kernel panic:

[<0x0000000080002df0>] sys_sleep+0x16/0xb4
[<0x0000000080002cca>] syscall+0x3e/0x6c
[<0x0000000080002996>] usertrap+0x60/0x108

Read the source code of backtrace(). How does this function walk up the stack and print the saved return address in each stack frame?

Hints:

  • The GCC compiler stores the frame pointer of the currently executing function in the register s0/fp.
  • The return address lives at a fixed offset (-8) from the frame pointer of a stack frame, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
  • Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. One can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) (see kernel/riscv.h). These number are helpful for backtrace() to terminate its loop.

Alarm

In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.

You should add a new sigalarm(interval, handler) system call. If an application calls sigalarm(n, fn), then after every n “ticks” of CPU time that the program consumes, the kernel should cause application function fn to be called. When fn returns, the application should resume where it left off. A tick is a fairly arbitrary unit of time in xv6, determined by how often a hardware timer generates interrupts. If an application calls sigalarm(0, 0), the kernel should stop generating periodic alarm calls.

You’ll find a file user/alarmtest.c in your xv6 repository. Add it to the Makefile. It won’t compile correctly until you’ve added sigalarm and sigreturn system calls (see below).

alarmtest calls sigalarm(2, periodic) in test0 to ask the kernel to force a call to periodic() every 2 ticks, and then spins for a while. You can see the assembly code for alarmtest in user/alarmtest.asm, which may be handy for debugging. Your solution is correct, when alarmtest produces output like this and usertests also runs correctly:

$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$

When you’re done, your solution will be only a few lines of code, but it may be tricky to get it right. We’ll test your code with the version of alarmtest.c in the original repository. You can modify alarmtest.c to help you debug, but make sure to revert to the original alarmtest.c before submitting your solution.

test0: invoke handler

Get started by modifying the kernel to jump to the alarm handler in user space, which will cause test0 to print “alarm!”. Don’t worry yet what happens after the “alarm!” output; it’s OK for now if your program crashes after printing “alarm!”. Here are some hints:

    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
    if(which_dev == 2) ...

test1/test2: resume interrupted code

Chances are that alarmtest crashes in test0 or test1 after it prints “alarm!”, or that alarmtest (eventually) prints “test1 failed”, or that alarmtest exits without printing “test1 passed”. To fix this, you must ensure that, when the alarm handler is done, control returns to the instruction at which the user program was originally interrupted by the timer interrupt. You must ensure that the register contents are restored to the values they held at the time of the interrupt, so that the user program can continue undisturbed after the alarm. Finally, you should “re-arm” the alarm counter after each time it goes off, so that the handler is called periodically.

As a starting point, we’ve made a design decision for you: user alarm handlers are required to call the sigreturn system call when they have finished. Have a look at periodic in alarmtest.c for an example. This means that you can add code to usertrap and sys_sigreturn that cooperate to cause the user process to resume properly after it has handled the alarm.

Some hints:

Once you pass test0, test1, and test2, run usertests to make sure you didn’t break any other parts of the kernel.

Optional challenges

Challenge: backtrace

The backtrace() function prints out numerical addresses. It relies on an external script (stacktrace in your xv6 repo) to translate numerical addresses to function names and offsets. Extend backtrace() to print out function names and offsets directly without using an external script. You may print out additional source information (e.g., file names and line numbers).

Challenge: user-level trap handling

Your solution requires multiple user-kernel transitions and state save/restore in the kernel. This is not necessary. Implement a more efficient mechanism, such as user-level exception handling described in Hardware and Software Support for Efficient Exception Handling.

Additionally, the “N” extension of RISC-V (see the RISC-V instruction set manual) provides hardware support for user-level trap handling. Implement the “N” extension in M- or S- mode, or in QEMU, and modify your xv6 to use it.

Challenge: BPF

Another approach to reduce the number of user-kernel transitions is downloading code into the kernel. For example, on Linux one can use extended BPF for tracing and profiling. Add BPF support to xv6 and use it to implement alarm.

This completes the lab. In the lab directory, commit your changes, type make tarball, and submit the tarball through Canvas.