Appearance
Lab: traps
This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.
Before you start coding, read Chapter 4 of the xv6 book, and related source files:
kernel/trampoline.S
: the assembly involved in changing from user space to kernel space and backkernel/trap.c
: code handling all interrupts
To start the lab, switch to the trap branch:
$ git fetch
$ git checkout traps
$ make clean
RISC-V assembly
It will be important to understand a bit of RISC-V assembly, which you were exposed to in 6.1910 (6.004). 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 call.asm for the functions g
, f
, and main
. The instruction manual for RISC-V is on the reference page. Answer the following questions in answers-traps.txt
:
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 tog
? (Hint: the compiler may inline functions.)
At what address is the function
printf
located?
What value is in the register
ra
just after thejalr
toprintf
inmain
?
Run the following code.
unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, (char *) &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 change57616
to a different value?Here's a description of little- and big-endian and a more whimsical description.
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);
Backtrace
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. To help with backtraces, the compiler generates machine code that maintains a stack frame on the stack corresponding to each function in the current call chain. Each stack frame consists of the return address and a "frame pointer" to the caller's stack frame. Register s0
contains a pointer to the current stack frame (it actually points to the the address of the saved return address on the stack plus 8). Your backtrace
should use the frame pointers to walk up the stack and print the saved return address in each stack frame.
Implement a
backtrace()
function inkernel/printf.c
. Insert a call to this function insys_sleep
, and then runbttest
, which callssys_sleep
. Your output should be a list of return addresses with this form (but the numbers will likely be different):backtrace: 0x0000000080002cda 0x0000000080002bb6 0x0000000080002898
After
bttest
exit qemu. In a terminal window: runaddr2line -e kernel/kernel
(orriscv64-unknown-elf-addr2line -e kernel/kernel
) and cut-and-paste the addresses from your backtrace, like this:$ addr2line -e kernel/kernel 0x0000000080002de2 0x0000000080002f4a 0x0000000080002bfc Ctrl-D
You should see something like this:
kernel/sysproc.c:74 kernel/syscall.c:224 kernel/trap.c:85
Some hints:
- Add the prototype for your
backtrace()
tokernel/defs.h
so that you can invokebacktrace
insys_sleep
. - The GCC compiler stores the frame pointer of the currently executing function in the register
s0
. In the section marked by #ifndef ASSEMBLER ... #endif, add the following function tokernel/riscv.h
:and call this function instatic inline uint64 r_fp() { uint64 x; asm volatile("mv %0, s0" : "=r" (x) ); return x; }
backtrace
to read the current frame pointer.r_fp()
uses in-line assembly to reads0
. - These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
- Your
backtrace()
will need a way to recognize that it has seen the last stack frame, and should stop. A useful fact is that the memory allocated for each kernel stack consists of a single page-aligned page, so that all the stack frames for a given stack are on the same page. You can usePGROUNDDOWN(fp)
(seekernel/riscv.h
) to identify the page that a frame pointer refers to.
Once your backtrace is working, call it from panic
in kernel/printf.c
so that you see the kernel's backtrace when it panics.
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 -q'
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 -q 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
test3 start
test3 passed
$ usertest -q
...
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 the original alarmtest says that all the tests pass.
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:
You'll need to modify the Makefile to cause
alarmtest.c
to be compiled as an xv6 user program.The right declarations to put in
user/user.h
are:int sigalarm(int ticks, void (*handler)()); int sigreturn(void);
Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow
alarmtest
to invoke the sigalarm and sigreturn system calls.For now, your
sys_sigreturn
should just return zero.Your
sys_sigalarm()
should store the alarm interval and the pointer to the handler function in new fields in theproc
structure (inkernel/proc.h
).You'll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process's alarm handler; you'll need a new field in
struct proc
for this too. You can initializeproc
fields inallocproc()
inproc.c
.Every tick, the hardware clock forces an interrupt, which is handled in
usertrap()
inkernel/trap.c
.You only want to manipulate a process's alarm ticks if there's a timer interrupt; you want something like
if(which_dev == 2) ...
Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user's alarm function might be 0 (e.g., in user/alarmtest.asm,
periodic
is at address 0).You'll need to modify
usertrap()
so that when a process's alarm interval expires, the user process executes the handler function. When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?It will be easier to look at traps with gdb if you tell qemu to use only one CPU, which you can do by running
make CPUS=1 qemu-gdb
You've succeeded if alarmtest prints "alarm!".
test1/test2()/test3(): 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:
Your solution will require you to save and restore registers---what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).
Have
usertrap
save enough state instruct proc
when the timer goes off thatsigreturn
can correctly return to the interrupted user code.Prevent re-entrant calls to the handler----if a handler hasn't returned yet, the kernel shouldn't call it again.
test2
tests this.Make sure to restore a0.
sigreturn
is a system call, and its return value is stored in a0.
Once you pass test0
, test1
, test2
, and test3
run usertests -q
to make sure you didn't break any other parts of the kernel.
Submit the lab
Time spent
Create a new file, time.txt
, and put in a single integer, the number of hours you spent on the lab. git add
and git commit
the file.
Answers
If this lab had questions, write up your answers in answers-*.txt
. git add
and git commit
these files.
Submit
Assignment submissions are handled by Gradescope. You will need an MIT gradescope account. See Piazza for the entry code to join the class. Use this link if you need more help joining.
When you're ready to submit, run make zipball
, which will generate lab.zip
. Upload this zip file to the corresponding Gradescope assignment.
If you run make zipball
and you have either uncomitted changes or untracked files, you will see output similar to the following:
M hello.c
?? bar.c
?? foo.pyc
Untracked files will not be handed in. Continue? [y/N]
Inspect the above lines and make sure all files that your lab solution needs are tracked, i.e., not listed in a line that begins with ??
. You can cause git
to track a new file that you create using git add {filename}
.
Warning
- Please run
make grade
to ensure that your code passes all of the tests. The Gradescope autograder will use the same grading program to assign your submission a grade.- Commit any modified source code before running
make zipball
.- You can inspect the status of your submission and download the submitted code at Gradescope. The Gradescope lab grade is your final lab grade.
Optional challenge exercises
- Print the names of the functions and line numbers in
backtrace()
instead of numerical addresses.