xv6, line by line
lab 2

Extension labs · lab 2 · The system-call and trap path · ★☆☆☆☆

A kernel backtrace on panic

When this kernel panics it prints one line, such as panic: sched locks, and stops. The line names the check that failed, not the path that led to it. In this lab you add backtrace(), which prints the return address of every function on the current kernel call chain, and make panic call it. On the host, addr2line turns the addresses into file names and line numbers.

The walk itself is a five-line loop over the frame pointers the compiler already maintains. The interesting part is where the loop must stop, and that depends on which stack it is walking. A process’s kernel stack is one page, so its top is a page boundary. A hart’s scheduler stack is a 4096-byte slice of stack0, which is only 16-byte aligned, so its top is in the middle of a page, and its top 32 bytes hold two frames left over from boot. And when an interrupt arrives in the middle of a call chain, kernelvec pushes a 256-byte kernelvec frame that the walk has to step across. You will see all three, in real output from three harts, and you will see what goes wrong, in recorded runs, when the walk does not stop where it should.

Read first: Tour 5: Life of a system call, Tour 8: Traps taken inside the kernel, Tour 12: One scheduler per hart, Tour 42: One hart's stacks, from power-on to the first user instruction, Tour 44: One interrupt, three landing sites · The stacks of xv6

What this lab teaches

  • How a RISC-V function’s prologue links its frame to its caller’s: with -fno-omit-frame-pointer, s0 points just above a two-word record holding the return address (at s0-8) and the caller’s s0 (at s0-16), so the frames form a linked list through the stack.
  • What sits at the top of each kind of kernel stack: on a process’s kernel stack, usertrap's record, holding the address after uservec’s jalr (the first instruction of userret) and the user’s s0; on a scheduler stack, start’s and main’s frames, never popped, with start’s record holding the ra and s0 of power-on.
  • Why stack0 slices are not page-aligned, and what PGROUNDUP(fp) does on them: it stops the walk too late (printing start’s power-on record, and, without an upward check, following its s0 of 0) or too early (cutting the chain at a page boundary in the middle of the slice). Both happen in one recorded run.
  • What the walk sees across a trap: kernelvec saves ra but not s0 and builds no record, so the chain goes from kerneltrap straight to the interrupted function’s frame, and the interrupted function’s own name never appears (nor its caller’s, if the trap strikes in a prologue or epilogue); its address is in sepc.
  • How to turn return addresses into lines with addr2line, and why you should look up ra - 1: with optimization, the instruction after a call can belong to another line, or to another block of code entirely.
  • Why a backtrace in panic must run before panicked is set and must never fault: a fault inside it re-enters kerneltrap, which panics again. A recorded run of a walk without bounds went down through every kernel stack below its own and their guard pages, then through the top of physical memory, before it froze.

The reference branch

ext/02-backtrace in ShowMeTheStack/xv6-riscv-labs, branched from the frozen commit 06aad25; 8 commits.

git clone https://github.com/ShowMeTheStack/xv6-riscv-labs
cd xv6-riscv-labs
git checkout -b my-backtrace 06aad25   # start your own
git diff 06aad25 origin/ext/02-backtrace   # only when you want the answer

1. The spec

The function. void backtrace(void) prints backtrace: and then, one per line, the return addresses of the current kernel call chain, newest first, in the form printk’s %p produces (0x0000000080002c46). It must stop at the top of the stack it started on, whichever kind of kernel stack that is, and it must never read outside that stack: it is about to be called from panic, where a fault would start another panic.

panic. panic prints its message, then the backtrace, then stops as before.

A way to try it from user space. A system call int kbacktrace(int where) makes the kernel print a backtrace from one of three places:

where where the backtrace is taken stack it walks
0 inside the system call the calling process’s kernel stack
1 in kerneltrap, at the next timer interrupt that arrives while this system call is running the same kernel stack, with a kernelvec frame in the middle
2 in kerneltrap, at the next timer interrupt that some idle hart takes in scheduler that hart’s slice of stack0

It returns 0 when the backtrace has been printed, and -1 for any other where or if no hart took a where 2 request within 10 ticks.

The test. bttest calls kbacktrace with 0, 1, 2 and 3 and checks the return values (0, 0, 0, -1). It cannot read the console, so the addresses themselves are checked by eye, by symbolizing them on the host; its last line says so:

$ bttest
bttest: from a system call, on this process's kernel stack:
backtrace:
0x0000000080002c46
0x00000000800029f2
0x0000000080002736
bttest: kbacktrace(0) returned 0: OK
bttest: from a timer interrupt during a system call:
backtrace:
...
bttest: kbacktrace(3) returned -1: OK
bttest: 4 checks OK; the addresses can only be checked by symbolizing them on the host with addr2line

bttest N runs only case N, to look at one stack at a time.

Constraints. Nothing else may change: no new output from any other program, and usertests -q must still print ALL TESTS PASSED on three harts. Do not change the compiler flags; the kernel is already built with -fno-omit-frame-pointer.

2. Think first

Answer each question in your head (or on paper) before opening a hint. Hints get more specific; the reference answer comes last.

1What does a function leave behind that leads to its caller?

To print the chain of calls that led to the current function, you need a way to get from a function to its caller, then to the caller’s caller, and so on. When a C function in this kernel is called, what does it store on the stack that would let you do that, and where exactly? Look at a few real prologues before you answer.

Check yourself

1warm-upType a number

usertrap begins addi sp,sp,-32; sd ra,24(sp); sd s0,16(sp); sd s1,8(sp); sd s2,0(sp); addi s0,sp,32. At what offset from the new s0 is the caller’s s0 saved? (Write a negative number.)

decimal, 0x hex or 0b binary
2solidDecode the bits

A copy of the kernel built without -fno-omit-frame-pointer printed, from a backtrace taken in kerneltrap, fp 0x0000000200000120 is not on a kernel stack: s0 held some variable of kerneltrap’s. Decode the value as the register it really came from, sstatus.

Value: 0x200000120

2How does C code get hold of the first link?

The walk starts from the current function’s frame pointer. C has no way to name a register. How will your walker read s0, and whose frame pointer will it get? Would reading sp do as well?

Check yourself

1solidChoose one

Why is the register reader declared static inline rather than as an ordinary function in some .c file?

3Where does the chain end on a process's kernel stack?

Start from a system-call handler and follow the saved frame pointers up. Every record leads to the caller’s record, until you reach the outermost frame of the kernel stack. What is stored in that frame’s record, and what happens if the walk follows it? How will the walker know where to stop?

Check yourself

1warm-upType a number

The walk starts with fp = 0x3fffff9f90 on a process’s kernel stack. What is PGROUNDUP(fp), the address where it stops? (Hex is fine.)

decimal, 0x hex or 0b binary
2solidTrue or false, and why

True or false: the record of usertrap’s frame, the outermost one on a process’s kernel stack, holds a saved frame pointer that points to another frame on the same kernel stack.

Why?

4How do you turn the printed numbers into names?

The walk prints numbers like 0x0000000080002736. The running kernel has no symbol table to look them up in. How do you find the function and line for each one, and is the line you get the line of the call?

Check yourself

1warm-upChoose one

Case 0 printed 0x0000000080002736. ${TOOLPREFIX}addr2line -e kernel/kernel -f -p 0x80002736 says usertrap at ./kernel/trap.c:88, the if (killed(p)) after the system call. Which line actually made the call that this address returns from?

5What does the walk see across a trap?

Suppose a timer interrupt arrives while some system-call handler f is running in the kernel, and the walk starts inside the interrupt handler. kernelvec pushes 256 bytes onto the same stack and calls kerneltrap. Which return addresses will the walk print, and which function on the path will not appear at all?

Check yourself

1solidPut in order

In case 1, a timer interrupt hits sys_kbacktrace’s spin loop and kerneltrap calls backtrace(). Put the functions named by the printed lines in the order they are printed.

  1. kernelvec
  2. kerneltrap
  3. usertrap
  4. syscall
2deepChoose one

Why can the frame-pointer walk continue past kernelvec at all, when kernelvec builds no frame record?

6Where is the top of a scheduler stack?

The same walk must work when the interrupted code is the scheduler loop on an idle hart. That stack is not a process’s kernel stack: it is the hart’s slice of stack0, the same memory it booted on. Is “the next page boundary above fp” still its top? If not, what happens when the walk uses it, and how should the walker find the real top?

Check yourself

1solidChoose one

A backtrace contains 0x0000000080000f66, and addr2line reports main at ./kernel/main.c:14, the line consoleinit();. Yet the stack belongs to a hart that has been in the scheduler for minutes. What is going on?

2solidType a number

In the unmodified kernel stack0 is at 0x80007890 and each hart’s slice is 4096 bytes. What is the address of the top of hart 2’s slice, where _entry points hart 2’s first sp? (Hex is fine.)

decimal, 0x hex or 0b binary
3solidFill in the machine state

Case 2: an idle hart takes a timer interrupt in the scheduler loop’s intr_on(); intr_off(); window, and kerneltrap calls backtrace(). Fill in the machine state on that hart as backtrace starts.

7How can a user program make the kernel walk from each place?

To test the walk you want a backtrace (a) from inside a system call, (b) from the interrupt handler while a system call is the interrupted code, and © from the interrupt handler on an idle hart’s scheduler stack. (a) is a system call that calls the walker. Design (b) and ©: how does the interrupt handler know it should print, how do you make sure exactly one hart prints, and when may the system call return?

Check yourself

1deepChoose all that apply

Which statements about the request variable btwant in the reference are true?

8What must panic do, and in what order?

Now make panic print a backtrace. Where in panic does the call go, and what must the walker itself never do when it is called from there?

Check yourself

1solidTrue or false, and why

True or false: if backtrace() were called after panicked = 1, it would still print, because panicked is meant to freeze the other harts’ output.

Why?

3. Build it

Start from the frozen commit, in your own clone of xv6 (not this site’s xv6/):

git checkout -b my-backtrace 06aad25

Milestones.

  1. Read s0. Add r_fp() to kernel/riscv.h. Nothing uses it yet; make must still build (-Werror is on, so an unused static inline is fine but an unused static function is not).
  2. The walk, for kernel stacks. backtrace() in kernel/printk.c, bounded by PGROUNDUP of the first fp, with the “saved fp must be higher” check, and its prototype in kernel/defs.h. Nothing calls it yet; milestone 3 is its first test.
  3. A system call. kbacktrace(0): the number in kernel/syscall.h, the table entry and extern in kernel/syscall.c, entry("kbacktrace") in user/usys.pl, the prototype in user/user.h (lab 1, trace, explains each one), and a three-line test program. You should see three addresses; symbolize them (${TOOLPREFIX}addr2line -e kernel/kernel -f -p ADDR-1) and check that they are sys_kbacktrace, syscall, usertrap.
  4. Across a trap. The btwant request, the spin in sys_kbacktrace, and the check in kerneltrap. Expect kerneltrap, kernelvec, syscall, usertrap.
  5. The scheduler stack. Write stacktop() first, then the case-2 request with the compare-and-swap and the sleeping wait. Expect kerneltrap, kernelvec, main, start.
  6. panic. One line. To see it work, break something on purpose in a scratch copy (the clinic has several ideas) and check the backtrace against the panic message.
  7. The test, then usertests -q, then the test again.

Debugging. Run QEMU on three harts (make qemu uses CPUS := 3). To see the frames the walk should find, use gdb: make qemu-gdb in one terminal, ${TOOLPREFIX}gdb kernel/kernel and target remote with the port make qemu-gdb chose in another, break backtrace before the first continue (in the recorded runs, breakpoints set after attaching to an already-running QEMU did not fire), then at the breakpoint p/x $sp, p/x $s0, and x/2gx $s0-16 to see a record. Walk it by hand with x/2gx on each saved s0. info symbol ADDR names a return address. If your walk prints one strange line and stops, compare the line with $s0: an address that looks like a stack address means you printed the saved fp instead of the return address (clinic 1).

4. Debugging clinic

Each of these bugs was put into the reference solution on purpose and run on three harts. The symptom is exactly what happened. Try to explain it before revealing why.

1The two words of the record swapped

The return address and the saved frame pointer are read from each other’s slots:

   while (fp < top) {
-    printk("%p\n", (void *)*(uint64 *)(fp - 8));
-    uint64 prev = *(uint64 *)(fp - 16);
+    printk("%p\n", (void *)*(uint64 *)(fp - 16));
+    uint64 prev = *(uint64 *)(fp - 8);
     if (prev <= fp)
       break; // a caller's frame is always higher: the chain is broken

What happened when we ran it

$ bttest
bttest: from a system call, on this process's kernel stack:
backtrace:
0x0000003fffff9fc0
bttest: kbacktrace(0) returned 0: OK
bttest: from a timer interrupt during a system call:
backtrace:
0x0000003fffff9e90
bttest: kbacktrace(1) returned 0: OK
bttest: from a timer interrupt on a scheduler stack:
backtrace:
0x000000008000a750
bttest: kbacktrace(2) returned 0: OK
bttest: kbacktrace(3) returned -1: OK
bttest: 4 checks OK; the addresses can only be checked by symbolizing them on the host with addr2line

2No bound at all, stop when fp is 0

A walk with no idea where the stack ends, stopping only at a null frame pointer:

   uint64 fp = r_fp();
-  uint64 top = stacktop(fp);

   printk("backtrace:\n");
-  if (top == 0)
-    printk("fp %p is not on a kernel stack\n", (void *)fp);
-  while (fp < top) {
+  while (fp != 0) {
     printk("%p\n", (void *)*(uint64 *)(fp - 8));
-    uint64 prev = *(uint64 *)(fp - 16);
-    if (prev <= fp)
-      break; // a caller's frame is always higher: the chain is broken
-    fp = prev;
+    fp = *(uint64 *)(fp - 16);
   }

(stacktop is deleted too; -Werror rejects an unused static function.)

What happened when we ran it

(on a scheduler stack, three runs of bttest 2 on one boot, all like this one:)
$ bttest 2
bttest: from a timer interrupt on a scheduler stack:
backtrace:
0x00000000800027e0
0x0000000080005768
0x0000000080000ee4
0x00000000800000c2
0x000000008000001a
bttest: kbacktrace(2) returned 0: OK
bttest: 1 checks OK; the addresses can only be checked by symbolizing them on the host with addr2line

(on a process's kernel stack, a fresh boot:)
$ bttest 0
bttest: from a system call, on this process's kernel stack:
backtrace:
0x0000000080002bc4
0x0000000080002970
0x00000000800026b4
0x0000003ffffff09c
scause=0xd sepc=0x80000858 stval=0x3fa8
panic: kerneltrap
backtrace:
0x00000000800008aa
0x000000008000279c
0x0000000080005768
0x0000000080002bc4
0x0000000080002970
0x00000000800026b4
0x0000003ffffff09c
scause=0xd sepc=0x80000858 stval=0x3fa8
panic: kerneltrap
backtrace:
[...]
(the same scause line ten times in all, the backtrace three lines longer each time; then:)
scause=0xf sepc=0x80005746 stval=0x3fffff8000
panic: kerneltrap
[...]
scause=0xf sepc=0x80005756 stval=0x3fffff6000
[...]
scause=0xf sepc=0x80005746 stval=0x3ffff80000
[...]
scause=0xf sepc=0x80005756 stval=0x88000000
[...]
scause=0xf sepc=0x80005746 stval=0x87f43000
[...]
scause=0xf sepc=0x80005756 stval=0x80011000
[...]
scause=0xd sepc=0x800006cc stval=0x1
[...]
scause=0xf sepc=0x80005744 stval=0x80008000
[...]
0x00000000800026b4
0x0000003ffffff09c

(about four minutes later, gdb attached:)
* 1    Thread 1.1 (CPU#0 [running]) uartputc_sync (c=115) at kernel/uart.c:109
  2    Thread 1.2 (CPU#1 [running]) acquire (lk=0x80010d68 <proc+3960>) at kernel/spinlock.c:37
  3    Thread 1.3 (CPU#2 [running]) acquire (lk=0x80010d68 <proc+3960>) at kernel/spinlock.c:37
[...]
Thread 1 (Thread 1.1 (CPU#0 [running])):
$6 = 0x80007810

3PGROUNDUP as the top, on stack0 too

The bound of commit 2 (PGROUNDUP of the first fp), kept for every stack, with the plain loop that many published solutions use (no upward check):

   uint64 fp = r_fp();
-  uint64 top = stacktop(fp);
+  uint64 top = PGROUNDUP(fp);

   printk("backtrace:\n");
-  if (top == 0)
-    printk("fp %p is not on a kernel stack\n", (void *)fp);
   while (fp < top) {
     printk("%p\n", (void *)*(uint64 *)(fp - 8));
-    uint64 prev = *(uint64 *)(fp - 16);
-    if (prev <= fp)
-      break; // a caller's frame is always higher: the chain is broken
-    fp = prev;
+    fp = *(uint64 *)(fp - 16);
   }

In this copy stack0 is at 0x800078b0, so hart 0’s slice is 0x800078b0 to 0x800088b0.

What happened when we ran it

$ bttest 2
bttest: from a timer interrupt on a scheduler stack:
backtrace:
0x00000000800027f4
0x0000000080005778
0x0000000080000ef8
0x00000000800000c2
0x000000008000001a
scause=0xd sepc=0x80000868 stval=0xfffffffffffffff8
panic: kerneltrap
backtrace:
0x00000000800008be
0x00000000800027b0
0x0000000080005778
0x00000000800027f4
0x0000000080005778
0x0000000080000ef8
0x00000000800000c2
0x000000008000001a
scause=0xd sepc=0x80000868 stval=0xfffffffffffffff8
panic: kerneltrap
[...]
(five panics in all; the first four backtraces each three lines longer than the one before; the fifth:)
scause=0xd sepc=0x80000868 stval=0xfffffffffffffff8
panic: kerneltrap
backtrace:
0x00000000800008be
0x00000000800027b0
0x0000000080005778

(no more output; 15 seconds later, gdb attached:)
* 1    Thread 1.1 (CPU#0 [running]) panic (s=s@entry=0x80007390 "kerneltrap") at kernel/printk.c:164
  2    Thread 1.2 (CPU#1 [halted ]) s_sstatus (x=2) at kernel/riscv.h:67
  3    Thread 1.3 (CPU#2 [halted ]) s_sstatus (x=2) at kernel/riscv.h:67
[...]
Thread 1 (Thread 1.1 (CPU#0 [running])):
$6 = 0x80007f80
[...]
Thread 1 (Thread 1.1 (CPU#0 [running])):
#0  panic (s=s@entry=0x80007390 "kerneltrap") at kernel/printk.c:164
#1  0x00000000800027b0 in kerneltrap () at kernel/trap.c:160
#2  0x0000000080005778 in kernelvec () at kernel/kernelvec.S:38
Backtrace stopped: frame did not save the PC

4Built without frame pointers

-fno-omit-frame-pointer removed from CFLAGS in the Makefile (as when you copy flags from another project); the code is the reference:

-CFLAGS = -Wall -Werror -Wno-unknown-attributes -O -fno-omit-frame-pointer -ggdb -gdwarf-2
+CFLAGS = -Wall -Werror -Wno-unknown-attributes -O -ggdb -gdwarf-2

What happened when we ran it

$ bttest
bttest: from a system call, on this process's kernel stack:
backtrace:
fp 0x00000000800100e0 is not on a kernel stack
bttest: kbacktrace(0) returned 0: OK
bttest: from a timer interrupt during a system call:
backtrace:
fp 0x0000000200000120 is not on a kernel stack
bttest: kbacktrace(1) returned 0: OK
bttest: from a timer interrupt on a scheduler stack:
backtrace:
fp 0x0000000200000120 is not on a kernel stack
bttest: kbacktrace(2) returned 0: OK
bttest: kbacktrace(3) returned -1: OK
bttest: 4 checks OK; the addresses can only be checked by symbolizing them on the host with addr2line

5Slept while holding tickslock

In case 2’s wait, the learner forgot this tree’s rule that the condition lock is released before sleep() (there is no sleep(chan, lock) here):

     sleep_prepare(&ticks);
-    release(&tickslock);
     sleep();
-    acquire(&tickslock);
   }

What happened when we ran it

$ bttest 2
bttest: from a timer interrupt on a scheduler stack:
panic: sched locks
backtrace:
0x000000008000092c
0x0000000080001f94
0x0000000080002034
0x0000000080002ce4
0x00000000800029f2
0x0000000080002736

(symbolized on the host with addr2line, each address minus 1:)
panic at ./kernel/printk.c:183
sched at ./kernel/proc.c:488
sleep at ./kernel/proc.c:569
sys_kbacktrace at ./kernel/sysproc.c:153
syscall at ./kernel/syscall.c:148
usertrap at ./kernel/trap.c:75

(a second run under gdb, breakpoint on panic; $4 is `p cpus[1].noff`, $7 is `p cpus[1].intena`:)
Thread 2 hit Breakpoint 1, panic (s=s@entry=0x800071e0 "sched locks") at kernel/printk.c:179
[...]
$4 = 2
[...]
$7 = 1

6myproc() used without checking for 0

In kerneltrap, the learner reads the current process’s pid unconditionally, as if a timer interrupt always interrupted a process:

   if (which_dev == 2 && btwant != 0) {
-    int want = myproc() != 0 ? myproc()->pid : -1;
+    int want = myproc()->pid;

What happened when we ran it

$ bttest 2
bttest: from a timer interrupt on a scheduler stack:
scause=0xd sepc=0x80002840 stval=0x30
scause=0xd sepc=0x80002840 stval=0xpan30
panic: ic: kkerneeltrneltrrapap

bacbackktractrace:
e:
00xx0000000000000800000080902c00092c

0x000000008000281e
0x0x000000000000080005700e8
0x00000000800057e8
0x000000000800800f606
0x000002800001e
0x0000008000000c2
00800057e8
(the capture ended here)

(a second run under gdb, breakpoint on panic:)
  Id   Target Id                    Frame
  1    Thread 1.1 (CPU#0 [running]) 0x0000000080000a60 in uartputc_sync (c=115) at kernel/uart.c:114
  2    Thread 1.2 (CPU#1 [running]) acquire (lk=lk@entry=0x8000f978 <pr>) at kernel/spinlock.c:37
* 3    Thread 1.3 (CPU#2 [running]) panic (s=s@entry=0x800073b0 "kerneltrap") at kernel/printk.c:179

Thread 3 (Thread 1.3 (CPU#2 [running])):
#0  panic (s=s@entry=0x800073b0 "kerneltrap") at kernel/printk.c:179
#1  0x000000008000281e in kerneltrap () at kernel/trap.c:160
#2  0x00000000800057e8 in kernelvec () at kernel/kernelvec.S:38
Backtrace stopped: frame did not save the PC

Thread 2 (Thread 1.2 (CPU#1 [running])):
#0  acquire (lk=lk@entry=0x8000f978 <pr>) at kernel/spinlock.c:37
#1  0x000000008000057a in printk (fmt=fmt@entry=0x80007388 "scause=0x%lx sepc=0x%lx stval=0x%lx\n") at kernel/printk.c:71
#2  0x0000000080002812 in kerneltrap () at kernel/trap.c:158
#3  0x00000000800057e8 in kernelvec () at kernel/kernelvec.S:38
Backtrace stopped: frame did not save the PC

Thread 1 (Thread 1.1 (CPU#0 [running])):
#0  0x0000000080000a60 in uartputc_sync (c=115) at kernel/uart.c:114
#1  0x000000008000029e in consputc (c=<optimized out>) at kernel/console.c:43
#2  0x0000000080000580 in printk (fmt=fmt@entry=0x80007388 "scause=0x%lx sepc=0x%lx stval=0x%lx\n") at kernel/printk.c:76
[...]

5. The reference solution

Take the guided tour through the reference solution, one commit at a time, with the machine state at every step:

Open the reveal tour →

Or read the commits

  1. c3ee160 Add r_fp to read the frame pointer

    kernel/riscv.h

    @@ -358,8 +358,20 @@ r_ra()
    358358 asm volatile("mv %0, ra" : "=r"(x));
    359359 return x;
    360360}
    361361
    362// read s0, the frame pointer. the kernel is compiled with
    363// -fno-omit-frame-pointer, so every C function points s0
    364// just above a two-word record: its return address at s0-8
    365// and its caller's s0 at s0-16.
    366static inline uint64
    367r_fp()
    368{
    369 uint64 x;
    370 asm volatile("mv %0, s0" : "=r"(x));
    371 return x;
    372}
    373
    362374// flush the TLB.
    363375static inline void
    364376sfence_vma()
    365377{
  2. a7613a4 Add backtrace() that walks the frame pointers

    kernel/defs.h

    @@ -76,8 +76,9 @@ int pipewrite(struct pipe*, uint64, int);
    7676// printk.c
    7777int printk(char*, ...) __attribute__ ((format (printf, 1, 2)));
    7878void panic(char*) __attribute__((noreturn));
    7979void printkinit(void);
    80void backtrace(void);
    8081
    8182// proc.c
    8283int cpuid(void);
    8384void kexit(int);

    kernel/printk.c

    @@ -133,8 +133,29 @@ printk(char *fmt, ...)
    133133
    134134 return 0;
    135135}
    136136
    137// print the return addresses of the current kernel call chain,
    138// newest first, by following frame pointers. every C function's
    139// frame holds its return address at fp-8 and its caller's fp at
    140// fp-16. a process's kernel stack is one page, so the walk stops
    141// at the next page boundary above the first fp.
    142void
    143backtrace(void)
    144{
    145 uint64 fp = r_fp();
    146 uint64 top = PGROUNDUP(fp);
    147
    148 printk("backtrace:\n");
    149 while (fp < top) {
    150 printk("%p\n", (void *)*(uint64 *)(fp - 8));
    151 uint64 prev = *(uint64 *)(fp - 16);
    152 if (prev <= fp)
    153 break; // a caller's frame is always higher: the chain is broken
    154 fp = prev;
    155 }
    156}
    157
    137158void
    138159panic(char *s)
    139160{
    140161 panicking = 1;
  3. ab240a7 Add the kbacktrace system call

    kernel/syscall.c

    @@ -102,8 +102,9 @@ extern uint64 sys_unlink(void);
    102102extern uint64 sys_link(void);
    103103extern uint64 sys_mkdir(void);
    104104extern uint64 sys_close(void);
    105105extern uint64 sys_sync(void);
    106extern uint64 sys_kbacktrace(void);
    106107
    107108// An array mapping syscall numbers from syscall.h
    108109// to the function that handles the system call.
    109110static uint64 (*syscalls[])(void) = {
    @@ -129,8 +130,9 @@ static uint64 (*syscalls[])(void) = {
    129130 [SYS_link] = sys_link,
    130131 [SYS_mkdir] = sys_mkdir,
    131132 [SYS_close] = sys_close,
    132133 [SYS_sync] = sys_sync,
    134 [SYS_kbacktrace] = sys_kbacktrace,
    133135 // clang-format on
    134136};
    135137
    136138void

    kernel/syscall.h

    @@ -20,4 +20,5 @@
    2020#define SYS_link 19
    2121#define SYS_mkdir 20
    2222#define SYS_close 21
    2323#define SYS_sync 22
    24#define SYS_kbacktrace 23

    kernel/sysproc.c

    @@ -109,4 +109,19 @@ sys_uptime(void)
    109109 xticks = ticks;
    110110 release(&tickslock);
    111111 return xticks;
    112112}
    113
    114// print a backtrace of this system call's kernel stack,
    115// to try out backtrace() from user space.
    116uint64
    117sys_kbacktrace(void)
    118{
    119 int where;
    120
    121 argint(0, &where);
    122 if (where == 0) {
    123 backtrace();
    124 return 0;
    125 }
    126 return -1;
    127}

    user/user.h

    @@ -24,8 +24,9 @@ int getpid(void);
    2424char *sys_sbrk(int, int);
    2525int pause(int);
    2626int uptime(void);
    2727int sync(void);
    28int kbacktrace(int);
    2829
    2930// ulib.c
    3031int stat(const char *, struct stat *);
    3132char *strcpy(char *, const char *);

    user/usys.pl

    @@ -42,4 +42,5 @@ entry("getpid");
    4242entry("sbrk");
    4343entry("pause");
    4444entry("uptime");
    4545entry("sync");
    46entry("kbacktrace");
  4. 58c814e Take a backtrace from a timer interrupt in the kernel

    kernel/defs.h

    @@ -143,8 +143,9 @@ void syscall();
    143143extern uint ticks;
    144144void trapinit(void);
    145145void trapinithart(void);
    146146extern struct spinlock tickslock;
    147extern volatile int btwant;
    147148void prepare_return(void);
    148149
    149150// uart.c
    150151void uartinit(void);

    kernel/sysproc.c

    @@ -110,10 +110,12 @@ sys_uptime(void)
    110110 release(&tickslock);
    111111 return xticks;
    112112}
    113113
    114// print a backtrace of this system call's kernel stack,
    115// to try out backtrace() from user space.
    114// print a kernel backtrace, to try out backtrace() from user space.
    115// where 0: right here, in this system call.
    116// where 1: from kerneltrap(), at the next timer interrupt that
    117// arrives while this system call is running.
    116118uint64
    117119sys_kbacktrace(void)
    118120{
    119121 int where;
    @@ -122,6 +124,14 @@ sys_kbacktrace(void)
    122124 if (where == 0) {
    123125 backtrace();
    124126 return 0;
    125127 }
    128 if (where == 1) {
    129 // interrupts are on during a system call, so the next
    130 // timer interrupt on this hart will land in this loop.
    131 btwant = myproc()->pid;
    132 while (btwant != 0)
    133 ;
    134 return 0;
    135 }
    126136 return -1;
    127137}

    kernel/trap.c

    @@ -8,8 +8,13 @@
    88
    1010uint ticks;
    1111
    12// set by kbacktrace(1): the pid of a process that wants a
    13// backtrace from the next timer interrupt that interrupts it
    14// in the kernel. 0 means no request.
    15volatile int btwant;
    16
    1217extern char trampoline[], uservec[];
    1318
    1419// in kernelvec.S, calls kerneltrap().
    1520void kernelvec();
    @@ -152,8 +157,14 @@ kerneltrap()
    152157 r_stval());
    153158 panic("kerneltrap");
    154159 }
    155160
    161 // a backtrace across the trap, asked for by kbacktrace(1).
    162 if (which_dev == 2 && myproc() != 0 && btwant == myproc()->pid) {
    163 btwant = 0;
    164 backtrace();
    165 }
    166
    156167 // give up the CPU if this is a timer interrupt.
    157168 if (which_dev == 2 && myproc() != 0)
    158169 yield();
    159170
  5. 6fce4bd Find the top of the stack0 slice as well as a kernel stack

    kernel/printk.c

    @@ -133,20 +133,39 @@ printk(char *fmt, ...)
    133133
    134134 return 0;
    135135}
    136136
    137extern char stack0[]; // start.c: one 4096-byte stack per hart
    138
    139// the top of the kernel stack that contains address fp, or 0.
    140// a process's kernel stack is one page, so its top is the next
    141// page boundary. stack0 is only 16-byte aligned, so the top of
    142// a hart's slice of it need not be a page boundary.
    143static uint64
    144stacktop(uint64 fp)
    145{
    146 uint64 base = (uint64)stack0;
    147
    148 if (fp >= base && fp < base + NCPU * 4096)
    149 return base + ((fp - base) / 4096 + 1) * 4096;
    150 if (fp >= KSTACK(NPROC - 1) && fp < TRAMPOLINE)
    151 return PGROUNDUP(fp);
    152 return 0;
    153}
    154
    137155// print the return addresses of the current kernel call chain,
    138156// newest first, by following frame pointers. every C function's
    139157// frame holds its return address at fp-8 and its caller's fp at
    140// fp-16. a process's kernel stack is one page, so the walk stops
    141// at the next page boundary above the first fp.
    158// fp-16. the walk stops at the top of the stack it started on.
    142159void
    143160backtrace(void)
    144161{
    145162 uint64 fp = r_fp();
    146 uint64 top = PGROUNDUP(fp);
    163 uint64 top = stacktop(fp);
    147164
    148165 printk("backtrace:\n");
    166 if (top == 0)
    167 printk("fp %p is not on a kernel stack\n", (void *)fp);
    149168 while (fp < top) {
    150169 printk("%p\n", (void *)*(uint64 *)(fp - 8));
    151170 uint64 prev = *(uint64 *)(fp - 16);
    152171 if (prev <= fp)
  6. 2480cf5 Take a backtrace on a scheduler stack

    kernel/sysproc.c

    @@ -114,12 +114,15 @@ sys_uptime(void)
    114114// print a kernel backtrace, to try out backtrace() from user space.
    115115// where 0: right here, in this system call.
    116116// where 1: from kerneltrap(), at the next timer interrupt that
    117117// arrives while this system call is running.
    118// where 2: from kerneltrap(), at the next timer interrupt that
    119// some hart takes on its scheduler stack.
    118120uint64
    119121sys_kbacktrace(void)
    120122{
    121123 int where;
    124 uint ticks0;
    122125
    123126 argint(0, &where);
    124127 if (where == 0) {
    125128 backtrace();
    @@ -132,6 +135,27 @@ sys_kbacktrace(void)
    132135 while (btwant != 0)
    133136 ;
    134137 return 0;
    135138 }
    139 if (where == 2) {
    140 // sleep, as sys_pause does, until an idle hart has printed
    141 // the backtrace. withdraw the request if no hart has taken
    142 // it after 10 ticks.
    143 btwant = -1;
    145 ticks0 = ticks;
    146 while (btwant != 0) {
    147 if (ticks - ticks0 >= 10 &&
    148 __sync_bool_compare_and_swap(&btwant, -1, 0)) {
    150 return -1;
    151 }
    154 sleep();
    156 }
    158 return 0;
    159 }
    136160 return -1;
    137161}

    kernel/trap.c

    @@ -8,11 +8,13 @@
    88
    1010uint ticks;
    1111
    12// set by kbacktrace(1): the pid of a process that wants a
    13// backtrace from the next timer interrupt that interrupts it
    14// in the kernel. 0 means no request.
    12// a request for a backtrace from the next timer interrupt taken
    13// in the kernel: the pid of a process that wants one while it is
    14// in a system call (kbacktrace(1)), or -1 for a hart that is on
    15// its scheduler stack (kbacktrace(2)). -2 while one is printing,
    16// 0 when there is no request.
    1517volatile int btwant;
    1618
    1719extern char trampoline[], uservec[];
    1820
    @@ -157,12 +159,17 @@ kerneltrap()
    157159 r_stval());
    158160 panic("kerneltrap");
    159161 }
    160162
    161 // a backtrace across the trap, asked for by kbacktrace(1).
    162 if (which_dev == 2 && myproc() != 0 && btwant == myproc()->pid) {
    163 btwant = 0;
    164 backtrace();
    163 // a backtrace across the trap, asked for by kbacktrace().
    164 if (which_dev == 2 && btwant != 0) {
    165 int want = myproc() != 0 ? myproc()->pid : -1;
    166 // two idle harts may both see -1: an atomic
    167 // compare-and-swap lets exactly one of them print.
    168 if (__sync_bool_compare_and_swap(&btwant, want, -2)) {
    169 backtrace();
    170 btwant = 0; // only now may the requester go on
    171 }
    165172 }
    166173
    167174 // give up the CPU if this is a timer interrupt.
    168175 if (which_dev == 2 && myproc() != 0)
  7. 2525819 Print a backtrace when the kernel panics

    kernel/printk.c

    @@ -179,8 +179,9 @@ panic(char *s)
    179179{
    180180 panicking = 1;
    181181 printk("panic: ");
    182182 printk("%s\n", s);
    183 backtrace(); // before panicked, which would freeze this hart's output too
    183184 panicked = 1; // freeze uart output from other CPUs
    184185 for (;;)
    185186 ;
    186187}
  8. ac4dbc3 Add bttest

    Makefile

    @@ -149,8 +149,9 @@ UPROGS=\
    149149 $U/_logstress\
    150150 $U/_forphan\
    151151 $U/_dorphan\
    152152 $U/_sync\
    153 $U/_bttest\
    153154
    154155fs.img: mkfs/mkfs README $(UPROGS)
    155156 mkfs/mkfs fs.img README $(UPROGS)
    156157

    user/bttest.c

    @@ -0,0 +1,49 @@
    1// test kbacktrace(): the kernel prints three backtraces, one from
    2// a system call, one across a trap, one on a scheduler stack.
    3
    4#include "kernel/types.h"
    5#include "user/user.h"
    6
    7int failed = 0;
    8
    9void
    10check(int where, int got, int want)
    11{
    12 printf("bttest: kbacktrace(%d) returned %d: %s\n", where, got,
    13 got == want ? "OK" : "FAIL");
    14 if (got != want)
    15 failed++;
    16}
    17
    18char *what[] = {
    19 "from a system call, on this process's kernel stack",
    20 "from a timer interrupt during a system call",
    21 "from a timer interrupt on a scheduler stack",
    22};
    23
    24int
    25main(int argc, char *argv[])
    26{
    27 int where, n = 0;
    28
    29 // "bttest 2" runs only that case, to look at one stack at a time.
    30 for (where = 0; where < 3; where++) {
    31 if (argc > 1 && atoi(argv[1]) != where)
    32 continue;
    33 printf("bttest: %s:\n", what[where]);
    34 check(where, kbacktrace(where), 0);
    35 n++;
    36 }
    37 if (argc == 1) {
    38 check(3, kbacktrace(3), -1);
    39 n++;
    40 }
    41
    42 if (failed) {
    43 printf("bttest: %d FAILED\n", failed);
    44 exit(1);
    45 }
    46 printf("bttest: %d checks OK; the addresses can only be checked by "
    47 "symbolizing them on the host with addr2line\n", n);
    48 exit(0);
    49}

6. Verify and measure

On the branch, on three harts, before usertests (the gdb run that the reveal follows printed exactly the same lines):

$ bttest
bttest: from a system call, on this process's kernel stack:
backtrace:
0x0000000080002c46
0x00000000800029f2
0x0000000080002736
bttest: kbacktrace(0) returned 0: OK
bttest: from a timer interrupt during a system call:
backtrace:
0x0000000080002862
0x00000000800057e8
0x00000000800029f2
0x0000000080002736
bttest: kbacktrace(1) returned 0: OK
bttest: from a timer interrupt on a scheduler stack:
backtrace:
0x0000000080002862
0x00000000800057e8
0x0000000080000f66
0x00000000800000c2
bttest: kbacktrace(2) returned 0: OK
bttest: kbacktrace(3) returned -1: OK
bttest: 4 checks OK; the addresses can only be checked by symbolizing them on the host with addr2line

Symbolized on the host with ${TOOLPREFIX}addr2line -e kernel/kernel -f -p, each address minus one:

case lines
0: system call sys_kbacktrace sysproc.c:128, syscall syscall.c:148, usertrap trap.c:75
1: across a trap kerneltrap trap.c:169, kernelvec kernelvec.S:38, syscall syscall.c:148, usertrap trap.c:75
2: scheduler stack kerneltrap trap.c:169, kernelvec kernelvec.S:38, main main.c:44, start start.c:44

Each is the expected chain: case 0 stops at the top of the kernel stack; case 1 crosses the kernelvec frame and (as it must) has no line for the interrupted sys_kbacktrace; case 2 stops at the top of the idle hart’s stack0 slice, after main and the boot-time return address into start, without reading start’s record.

The rest of the kernel is unharmed, in the same boot:

$ usertests -q
usertests starting
[...]
ALL TESTS PASSED

bttest run again after usertests printed the same eleven addresses and 4 checks OK. No other program calls kbacktrace, so the only cost to an ordinary kernel is one load of btwant per timer interrupt taken in the kernel.

How deep are the stacks the walk climbs? Measured with gdb on the branch (frame pointers at the moment backtrace starts, against each stack’s top):

case stack top backtrace’s fp depth
0 bttest’s kernel stack 0x3fffffa000 0x3fffff9f90 112 bytes, 3 frames
1 the same, with a kernelvec frame 0x3fffffa000 0x3fffff9e60 416 bytes (256 of them kernelvec’s)
2 hart 1’s slice of stack0 0x800098d0 0x80009720 432 bytes

How much of the 4096 bytes does real work use? kalloc fills every page with the byte 5, including the 64 kernel stacks that proc_mapstacks allocates once at boot, and stack0 starts out zero (QEMU’s RAM is zeroed, and nothing else writes it). So after a run, the lowest byte of a stack that no longer holds its initial value marks the deepest point that stack ever reached (to within a few bytes, if a stored value happened to end in that byte). Read with gdb after one boot running usertests -q and then bttest, on the branch:

So the deepest normal path leaves about 2.4 KiB of each 4 KiB kernel stack unused, and the guard page is never approached. What fills a stack is recursion that should not happen: in clinic 2, each round of fault, kerneltrap, panic and walk took 368 bytes, and in the eleventh round kerneltrap’s frame reached the guard page.

What PGROUNDUP gets wrong on stack0, in numbers. In this build the slice tops are at 0x...8d0: 2256 bytes above a page boundary and 1840 bytes below the next one. A walk that starts in the top 2256 bytes of a slice (every scheduler-stack walk in the recorded runs of a healthy kernel: the deepest scheduler-stack use measured was 767 bytes) gets a bound 1840 bytes too high and reads start’s record; one that starts deeper gets a bound inside the slice and stops early. Clinic 3 recorded both in one run: five walks too long, then one too short.

7. Go further