16 When it does not boot: debugging a kernel

The first edition of this book ended Part III with a list of chapter titles, and the mail it received over the years was not about the chapters that existed. It was about kernels that did not boot. Readers had understood the GDT, the IDT and paging perfectly well; what stopped them was a QEMU window that flickered, or a serial console that stayed empty, or a program that died at its first instruction, with nothing to tell them why. A hosted program that crashes leaves a core dump, a message from the C library, at worst a Segmentation fault. A kernel leaves nothing. It is the lowest layer of software, and when it faults there is no layer below to print an error message: the processor either runs a handler the kernel installed, or it resets.

That is the whole difficulty, and it is not a difficulty of knowledge. Every reader who wrote to us could have found their bug in a few minutes with the right question asked in the right order. This chapter is about the order. It gives a method, cheapest step first; a table that maps what you see to what is probably wrong and to the one command that confirms it; and three bugs that we introduced on purpose in the kernel of chapter 14, File system, and hunted down for real, with the transcripts. Nothing here is new: every tool was used at least once in chapters 7 to 14. What is new is putting them together into a procedure that you can follow when the machine says nothing.

Running this chapter’s code

This chapter adds no code. Its experiments are made on copies of the chapter 13 and 14 directories, so that the originals stay known-good, which is the point of section 16.1.5 below. Copy a chapter next to a copy of the tools/ directory, so that the relative path to the test scripts in the Makefiles still works, and mount the copy in the container as a second volume:

$ mkdir -p /path/to/scratch/code
$ cp -r code/chapter14 /path/to/scratch/code/
$ cp -r tools /path/to/scratch/
$ docker run --rm -it --user "$(id -u):$(id -g)" --security-opt seccomp=unconfined \
      -v "$PWD":/work -v /path/to/scratch:/scratch -w /scratch/code/chapter14/os os01

Inside, make, make qemu, make gdb and make test work as in chapter 14. Every transcript below was produced this way; the QEMU command lines are the chapter 14 ones, with options added as explained.

16.1 A method, in order of cost

The order matters more than the tools. Each step below is cheaper than the next, in time and in the amount of machinery you must understand to read its output, and each one answers a question that the next one would otherwise have to answer too. Start at the top, stop as soon as you know where the kernel dies; the symptom table and the worked cases take it from there.

The cheapest instrument is the one you have had since chapter 10, Talking to devices: the serial port and the VGA text console. serial_putc depends on nothing, no interrupts, no paging, no heap, no stack beyond one call, and QEMU writes what it receives into a file on the host. A kprintf is therefore the first thing to add when a kernel dies silently, and the question it answers is the only one that matters at that point: how far did we get?

The technique is bracketing. Put a line before and after each step of kmain and look at which one is the last to appear:

    kprintf("init: paging\n");
    paging_init();
    kprintf("init: heap\n");
    heap_init();
    kprintf("init: tss\n");
    tss_init();

If init: heap is the last line, the bug is in heap_init or in something that heap_init is the first to rely on, and the next round of prints goes inside it. Three rounds usually narrow a silent death to a dozen lines. Then print the values: a pointer, a selector, an entry of the page directory, the status byte a device returned. The kernel of chapter 14 prints the LBA of a failed read and the status that came with it, the error code of a page fault with its bits decoded, the vector and registers of an unhandled exception; those lines are the residue of exactly this process, left in the code because they cost nothing and because the next bug will need them.

Two habits make the prints useful. Capture the port to a file, -serial file:serial.txt, rather than watching the terminal: a kernel that reboots in a loop scrolls the evidence off the screen, a file keeps it. And put the serial driver first in kmain, before the GDT, before the console: in chapter 10 we explained why serial_init can run before anything else, and a kernel that cannot print cannot be debugged by printing.

Why not the VGA screen? Three reasons. It scrolls: 25 lines, and the message you need is the twenty-sixth. It is erased by a reset: a triple fault clears it before you can read it, and in the make test configuration there is no screen at all. And after chapter 12 it is memory that must be mapped before a store to 0xB8000 works, so the one bug that unmaps it also silences it. The serial port is a port, it is never unmapped, and its output outlives the machine.

A print has one limit: it needs the kernel to be running C code with a valid stack. The bugs that strike between an event and the first line of C, in the processor’s own handling of an interrupt, or in the bootloader before there is a kernel, print nothing. For those we need the machine itself to talk.

16.1.2 Make the machine tell you: QEMU’s logs and monitor

QEMU knows everything the processor does, and will say it if asked. The options that matter, all of them already met in chapter 11, Interrupts, and chapter 12, Memory management:

The monitor is QEMU’s own console, reachable with -monitor stdio or, from gdb, with the monitor prefix. Four of its commands cover most of a kernel’s needs, and each prints something gdb cannot know:

Here is what a clean run of the chapter 14 kernel looks like through -d int, booted headless for eight seconds and then interrupted; it is the baseline against which every log in this chapter is read:

$ qemu-system-i386 -machine q35 -device piix3-ide,id=ide \
      -drive id=disk,format=raw,file=build/disk.img,if=none -device ide-hd,drive=disk,bus=ide.0 \
      -display none -serial file:serial.txt -d int,guest_errors -D qemu.log -no-reboot
$ grep -o "v=[0-9a-f]*" qemu.log | sort | uniq -c
    792 v=20
     14 v=80
$ grep -c check_exception qemu.log
0

Seven hundred and ninety-two timer ticks, one every ten milliseconds, fourteen system calls (hello makes three: getpid, write, exit; count makes eleven) and not one exception: a correct kernel of this size never faults, and a v=0d or v=0e in the log of a boot that looks fine is a bug waiting to be understood. The first system call entry shows what the log says about a ring 3 event:

$ grep -n -m1 -A3 "v=80" qemu.log
619:    10: v=80 e=0000 i=1 cpl=3 IP=001b:080480a3 pc=080480a3 SP=0023:7fffffa4 env->regs[R_EAX]=00000004
620-EAX=00000004 EBX=00000000 ECX=00000000 EDX=00000000
621-ESI=00000000 EDI=00000000 EBP=7fffffb8 ESP=7fffffa4
622-EIP=080480a3 EFL=00000202 [-------] CPL=3 II=0 A20=1 SMM=0 HLT=0

i=1 for a software interrupt, cpl=3, CS = 0x1b, the user stack at 0x7fffffa4 and EAX = 4, SYS_GETPID: everything chapter 13 said about the system call path, in one line, for free.

16.1.3 gdb on a frozen machine

When the prints and the log have told you where, gdb tells you what. make qemu and make gdb as in every chapter since 7; the recipes below are the ones that matter when the kernel is broken rather than being studied.

Attach after the fact. gdb does not have to be there from the start. With -no-reboot -no-shutdown on the QEMU command line and the gdb stub enabled (-gdb tcp::26000 without -S), you can let the kernel run, crash or hang, and then run make gdb: the .gdbinit connects, and the first thing gdb prints is where the processor stopped. For a hang it is the instruction being executed, usually a hlt or a jmp to itself; for a triple fault it is the instruction the processor was trying to interrupt. Case 1 does exactly this.

Break on the handlers, not on the bug. You rarely know where the bug is; you always know where its consequence arrives. b isr_dispatch catches every interrupt and exception, b page_fault_handler every page fault, b panic every panic, and b isr13 or b isr14, the assembly stubs, catch the exception before the kernel has touched anything. The stubs are the place to read the raw frame the processor pushed. At isr14 for a fault from ring 3 (taken from case 2 below):

(gdb) b isr14
Breakpoint 2 at 0x10194: file isr_stubs.asm, line 37.
(gdb) c

Breakpoint 2, isr14 () at isr_stubs.asm:37
37      push    dword %1
(gdb) x/6wx $esp
0xc0001ff8: 0x00000007  0x08048080  0x0000001b  0x00010202
0xc0002008: 0x80000000  0x00000023
(gdb) p/x $cr2
$1 = 0x7ffffffc

Six words, exactly the figure “Stack Usage on Transfers to Interrupt and Exception-Handling Routines” of Intel SDM Volume 3A, section 7.12.1, read bottom-up: the error code 7, the saved EIP 0x08048080, CS 0x1b, EFLAGS 0x10202, and, because the fault came from ring 3, the user ESP 0x80000000 and SS 0x23. Note where ESP itself is: 0xc0001ff8, at the top of a kernel stack in the heap, which is the stack switch of chapter 13 having happened. For a fault from ring 0 the last two words are absent: an exception that pushes an error code leaves four words at the stub, and $esp is where the interrupted kernel code had it, minus 16. The same frame reached through the C structure, once isr_common has run, is p/x *regs at a breakpoint on isr_dispatch, as in chapter 11.

Decode the error code before anything else. Two formats, both in Volume 3A. For #GP, #NP, #TS and #SS (section 7.13 “Error Code”): bit 0 EXT, bit 1 IDT, bit 2 TI, bits 15:3 the index of the descriptor at fault. 0x102 is 1 0000 0010 in binary: IDT set, index 32, “gate 32 is bad”. 0x402 is index 128, gate 0x80. A zero error code with #GP means the fault was not about a descriptor: a privileged instruction in ring 3, a bad iret frame, a segment limit. For #PF (section 5.7 “Page-Fault Exceptions”): bit 0 P, bit 1 W/R, bit 2 U/S, bit 4 I/D, and CR2 holds the address. 0x7 is “the page is present, the access was a write, from ring 3”, which can only mean a permission bit; 0x6 is the same access to a page that is not there at all. Chapter 12’s handler prints the decoding, chapter 11’s example 11.1 does it by hand; do it by hand once more each time, it takes ten seconds and it is where most wrong guesses are avoided.

Look at the instruction, then at the backtrace, then doubt the backtrace. x/i $pc or x/i regs->eip shows the faulting instruction; x/5i regs->eip - 12 the ones before it, which is where the operands were computed. bt is the next step, and chapter 11 warned you about it: the assembly stubs have no frame pointer, so gdb’s unwinder invents a frame or two (#3 0xc0001fc4 in ?? ()) and may stop with Backtrace stopped: previous frame inner to this frame (corrupt stack?). Read a backtrace up to the first isr_common and ignore what follows; across iret, across context_switch, and in a user program whose _start has no frame, gdb cannot see, and that is not a bug in your kernel. When you need the real caller, read the return address off the stack yourself, info symbol *(uint32_t *)($esp + 20), as chapter 13 did for context_switch.

Change the machine from gdb. set $pc = address makes the processor continue somewhere else, set var x = 0 changes a variable, jump *address combines both; the use is to skip a faulting instruction to see whether the rest works, or to force a code path. finish runs to the end of the current function and prints its return value, which is the fastest way to see what a driver returned without adding a print.

Hardware breakpoints and watchpoints. hbreak sets a breakpoint through the processor’s debug registers instead of QEMU’s address list; the two behave the same under QEMU, but hbreak is what you will need on real hardware and under hypervisors that patch memory. watch variable stops when a variable changes, and tells you the old and new value and the line that wrote it. On the chapter 14 kernel:

(gdb) watch current_task
Hardware watchpoint 2: current_task
(gdb) c

Hardware watchpoint 2: current_task

Old value = (struct task *) 0x0
New value = (struct task *) 0x1e060 <idle_task>
task_init () at task.c:49
49  }
(gdb) c

Hardware watchpoint 2: current_task

Old value = (struct task *) 0x1e060 <idle_task>
New value = (struct task *) 0xc0000010
schedule () at task.c:146
146     tss_set_kernel_stack((uint32_t)next->kernel_stack + KERNEL_STACK_SIZE);
(gdb) p current_task->name
$1 = 0x15ab2 "/bin/hello"

Two writes, from task_init and from schedule, each reported with the line that did it. This is the instrument for “who corrupted this”: a watchpoint on the word that changes by itself finds the writer, however far away it is. The processor has four debug registers, which bounds the number of hardware watchpoints on real hardware; watch *(uint32_t *)0x1d000 works on a raw address when there is no variable.

The 16-bit quirk. Chapters 7 and 9 explained it and it is worth one more line: with QEMU 10 and gdb 16, gdb disassembles real-mode code as 32-bit even after set architecture i8086, because the stub describes the processor as an i386. Breakpoints, stepping and registers are right; only the mnemonics are wrong. Read bytes with x/8xb $cs*16+$eip and compare with nasm -l when debugging the bootloader, and trust the disassembly again from the far jump on.

16.1.4 From an address to a line

Every tool above hands you addresses. Three programs of binutils turn them back into source, and all three read the DWARF information that -g and nasm -g put in the kernel ELF, which is why the file is 124772 bytes for 20 KiB of code.

addr2line is the direct one. The EIP at which the kernel of case 1 died was 0x1313b:

$ addr2line -e build/os/os 0x1313b
/scratch/code/chapter14/os/os/kernel.c:100
$ addr2line -f -p -e build/os/os 0x1313b 0x134ca 0x8048080
kmain at /scratch/code/chapter14/os/os/kernel.c:100
page_fault_handler at /scratch/code/chapter14/os/os/paging.c:104
?? ??:0

-f adds the function, -p makes it one line per address. The third address is a user-space one, which the kernel’s ELF knows nothing about: ask the right file,

$ addr2line -f -p -e build/user/hello 0x8048080
_start at /scratch/code/chapter14/os/user//crt0.asm:17

and remember that with several address spaces the same number means a different line in each.

nm -n lists symbols sorted by address, and finds which function an address falls in when there is no line information (an address in the middle of an assembly stub, or a kernel built without -g): the last symbol below the address is the function,

$ nm -n build/os/os | awk '$1 <= "0001313b"' | tail -2
00012f91 t log_boot
00013108 T kmain

so 0x1313b is kmain + 0x33. gdb’s info symbol 0x1313b does the same from inside a session. nm -n is also the quickest answer to “where is this variable”: the chapter 12 listing of __bss_start, kernel_directory and bitmap was made with it, and nm -n build/os/os | grep ticks tells you what address to watch.

objdump -d with --start-address and --stop-address disassembles a window around the address without dumping the whole kernel:

$ objdump -d -M intel --start-address=0x13137 --stop-address=0x13145 build/os/os | tail -4
   13137:   fb                      sti
   13138:   83 ec 0c                sub    esp,0xc
   1313b:   68 da 59 01 00          push   0x159da
   13140:   e8 04 0f 00 00          call   14049 <kprintf>

The faulting instruction is the push of the first kprintf argument, right after sti. That is the one-instruction delay of chapter 11: the interrupt that sti let through was delivered after the next instruction, and EIP is where the processor would have resumed. Finally, readelf -S and readelf -l tell you where things were linked, which is the question when an address is in no function at all: an EIP of 0x18000 in the chapter 14 kernel is in .bss, so the processor is executing data, and the return address it popped was overwritten; an EIP below 0x10100 is before .text, in the ELF headers.

16.1.5 Bisect: the last known-good kernel

The method so far assumes one bug in a kernel that mostly works. When everything is broken, or when you do not know what you changed, the cheapest instrument is a copy that works. The repository gives you one per chapter, each a superset of the previous, and diff shows exactly what a chapter added:

$ diff -rq chapter13/os/os chapter14/os/os
Only in chapter14/os/os: ata.c
Only in chapter14/os/os: ata.h
Only in chapter14/os/os: elf.c
Only in chapter14/os/os: elf.h
Only in chapter14/os/os: ext2.c
Only in chapter14/os/os: ext2.h
Files chapter13/os/os/io.h and chapter14/os/os/io.h differ
Files chapter13/os/os/kernel.c and chapter14/os/os/kernel.c differ
Files chapter13/os/os/paging.c and chapter14/os/os/paging.c differ
Files chapter13/os/os/paging.h and chapter14/os/os/paging.h differ
Files chapter13/os/os/string.c and chapter14/os/os/string.c differ
Files chapter13/os/os/string.h and chapter14/os/os/string.h differ
Files chapter13/os/os/syscall.c and chapter14/os/os/syscall.c differ
Files chapter13/os/os/syscall.h and chapter14/os/os/syscall.h differ
Files chapter13/os/os/task.c and chapter14/os/os/task.c differ
Files chapter13/os/os/task.h and chapter14/os/os/task.h differ

Your own kernel deserves the same: commit it every time make test passes. Then a regression is a search over commits rather than over code, and git bisect automates the search: git bisect start, git bisect bad on the broken revision, git bisect good on the last one that booted, and git checks out the middle one; make test says good or bad, and after log2(n) rounds git names the commit that introduced the bug. The test scripts in tools/ are what make this mechanical: serial-test.sh boots headless and waits for a string, boot-test.sh waits for an address under gdb, and a one-line make test target with the right expected string is a regression test for every chapter’s feature. Run it after every change, not only before a commit; the ten seconds it takes are cheaper than any step above.

When the bug is in a single change, diff -u between the copy and the original is the bisection, and a diff of one line is the usual outcome. All three cases below start with one.

16.2 A symptom table

The table below is the method applied in reverse: from what you see to what is probably wrong, with the command that confirms it before you change anything. The chapter column says where the mechanism is explained. Confirm first; most hours lost on a kernel were spent fixing the wrong bug.

What you see Most likely cause How to confirm Chapter
The machine reboots in a loop: the QEMU window flickers, the serial output starts again from the first line Triple fault: an exception that the IDT cannot deliver, usually at the first interrupt after sti Run with -d int -D qemu.log -no-reboot and read the last check_exception lines of the log; without -no-reboot, grep -c 'Triple fault' qemu.log 11, 16
Nothing after the bootloader; it hangs or resets right after lgdt or the far jump A wrong selector in the far jump, or a GDT pseudo-descriptor with a wrong base or limit b *0x7c00, si up to the far jump, then monitor info registers: compare the GDT= line with the table’s address in the nasm -l listing, and x/3xg the base 9
Nothing on the serial port, ever serial_init not called or a wrong port; or the kernel was never loaded: KERNEL_SECTORS too small, a wrong load address, dd seek= off by one x/4xb 0x10000 must show 7f 45 4c 46 and x/xw 0x10018 the entry point; monitor i /b 0x3fb must read 0x03 (DLAB clear); monitor i /b 0x3fd should read 0x60 9, 10
Garbage or nothing on the VGA screen while the serial port is fine 0xB8000 not mapped after paging was turned on, or an attribute byte of 0x00, black on black x/8xh 0xb8000: the attribute is the high byte of each cell; monitor info mem must list the first megabyte as mapped 10, 12
Reboot the moment sti runs PIC not remapped (the timer lands on vector 8), the gate for vector 0x20 missing or outside the IDT limit, or lidt never executed -d int: a v=20 or v=08 immediately followed by check_exception; monitor info registers and read the IDT= base and limit 11, 16
Exception 13: General Protection with a non-zero error code A bad descriptor; the error code names it: bit 1 set means an IDT gate, clear means a GDT selector, and bits 15:3 are the index p regs->err_code >> 3 for the index; x/2xw &idt[index] or x/xg &gdt[index]; compare with monitor info registers 9, 11
Page fault at <address> An unmapped page (P clear), a kernel page touched from ring 3 (P and U set), a write to a read-only page (W set); an address just below a stack is an overflow p/x $cr2, then walk the tables by hand: p/x dir[cr2 >> 22], then the table entry; monitor info mem prints u only for user pages 12, 13
int 0x80 from a user program gives #GP with error code 0x402, or a triple fault The gate for vector 0x80 has DPL 0: ring 3 is not allowed to use it p/x idt[0x80]: type_attr must be 0xee, not 0x8e; -d int shows v=80 ... cpl=3 followed by v=0d e=0402 13
A user program dies at its first instruction Wrong entry address, no _start, user stack not mapped or without PAGE_USER, data selectors still the kernel’s b isr14 (or isr13) and x/6wx $esp: the saved EIP, CS, ESP; readelf -h of the program for e_entry; monitor info mem for a u line at the stack 13, 14
Exception 8: Double Fault, or v=0e then v=08 in the log The kernel stack is gone: TSS.esp0 wrong or stale, a kernel stack that overflowed into an unmapped page; or an IRQ on vector 8 (PIC not remapped) p/x tss.esp0 against current_task->kernel_stack + KERNEL_STACK_SIZE; CR2 in the log close to esp0; an “error code” that looks like an address is a hardware interrupt 11, 13
The timer fires once, then never EOI not sent, or IRQ 0 masked again monitor info pic: isr=01 means IRQ 0 still in service, bit 0 of imr set means masked; p ticks stays at 1 11, 13
The keyboard works for one key, then never The scancode is not read from port 0x60 (the controller waits), or EOI not sent monitor info pic as above; monitor i /b 0x64 with bit 0 set: a byte is still waiting in the controller 11
Every disk register reads 0xFF No controller answers at 0x1F0: on q35 the disk is on AHCI unless -device piix3-ide is given monitor i /b 0x1f7 reads 0xff; check QEMU_DRIVE_ARGS in the Makefile 14
Disk reads succeed but the data is wrong: bad magic, not a directory, an ELF that is not one LBA or sector count off by one, or the partition offset Print the LBA in read_block; compare with xxd -s $((lba*512)) -l 32 build/disk.img and debugfs -R stats 14, 16
The ELF loads, then the program faults at address 0 or executes zeros A PT_LOAD segment skipped, p_offset confused with p_vaddr, .bss not zeroed, the entry point outside the bytes copied b elf.c:117 (after the copy), walk the new directory to the frame, x/4xb frame must be 7f 45 4c 46, then x/4i frame + (e_entry & 0xfff) 14
Random crashes after a while; variables that change by themselves; kfree panics Heap corruption: a write past a kmalloc block, a stale pointer, an interrupt handler touching a structure in the middle of an update watch *(uint32_t *)addr on the word that changes; p *head on the heap list; irq_save around the suspect 12, 13
The panic names a line that cannot fault, or addr2line disagrees with the source Stale build: an object not rebuilt after a header changed, or symbol-file on an old ELF make clean && make; ls -l build/os/os against the sources; addr2line on the faulting EIP again 16

16.3 Three worked cases

Each case starts from a clean copy of the chapter 14 kernel, introduces one small bug of the kind readers reported, and shows the hunt as it happened, transcripts included. The bugs are shown as diffs against the chapter’s code; the hunts use only the tools above.

16.3.1 Case 1: the machine reboots at the first tick

The bug is in idt.c. The limit of the IDTR is the size of the table minus one, and the chapter 11 code says so; this copy says the size of an entry minus one, the kind of slip that happens when a * IDT_ENTRIES is lost in an edit:

--- chapter14/os/os/idt.c
+++ i_limit/os/os/idt.c
@@ -38,7 +38,7 @@
         idt_set_gate(i, (uint32_t)isr_stub_table[i], GDT_KERNEL_CODE,
                      IDT_INTERRUPT_GATE_RING0);
 
-    idt_pointer.limit = sizeof(idt) - 1;
+    idt_pointer.limit = sizeof(struct idt_entry) - 1;
     idt_pointer.base  = (uint32_t)&idt;
     asm volatile("lidt %0" : : "m"(idt_pointer));
 }

It compiles without a warning, and make qemu shows a window that flickers. The serial output is empty, which is itself a clue: the kernel dies before its first kprintf, and kmain executes sti just before it. Step one, print, has nothing to add here, so we go to step two and ask QEMU:

$ qemu-system-i386 -machine q35 -device piix3-ide,id=ide \
      -drive id=disk,format=raw,file=build/disk.img,if=none -device ide-hd,drive=disk,bus=ide.0 \
      -display none -serial file:serial.txt -d int,cpu_reset -D qemu.log -no-reboot -no-shutdown

QEMU stops at the triple fault and waits; after a few seconds we interrupt it with Ctrl-C and read:

$ wc -c serial.txt
0 serial.txt
$ grep -n "check_exception\|v=\|Triple" qemu.log
476:     0: v=20 e=0000 i=0 cpl=0 IP=0008:0001313b pc=0001313b SP=0010:0008ffc4 env->regs[R_EAX]=000000fc
495:check_exception old: 0xffffffff new 0xd
496:     1: v=0d e=0102 i=0 cpl=0 IP=0008:0001313b pc=0001313b SP=0010:0008ffc4 env->regs[R_EAX]=000000fc
515:check_exception old: 0xd new 0xd
516:     2: v=08 e=0000 i=0 cpl=0 IP=0008:0001313b pc=0001313b SP=0010:0008ffc4 env->regs[R_EAX]=000000fc
535:check_exception old: 0x8 new 0xd
536:Triple fault

Read it as chapter 11 read its log. The first interrupt the kernel ever receives is v=20, IRQ 0, the timer, a hardware interrupt (i=0) in ring 0 at EIP = 0x1313b. Delivering it raises #GP (new 0xd) with error code 0x102. Delivering the #GP raises #GP again, which is a double fault, v=08; delivering the double fault raises a third, and that is the triple fault. The error code says which descriptor was refused: 0x102 is binary 1 0000 0010, IDT bit set, index 0x20: “gate 32 is bad”. And the processor could not deliver #GP through gate 13 either, nor #DF through gate 8, so the whole table is unusable, not one entry. A table whose entries were filled by the loop we can read in idt.c and that is unusable as a whole has a wrong base or a wrong limit.

Without -no-reboot the same log is a loop. We let it run for three seconds:

$ grep -c "Triple fault" qemu.log
22
$ grep -A3 "Triple fault" qemu.log | head -5
Triple fault
CPU Reset (CPU 0)
EAX=000000fc EBX=00000000 ECX=00000001 EDX=00000021
ESI=00007cd8 EDI=0001e110 EBP=0008fff8 ESP=0008ffc4
--

Twenty-two boots in three seconds, each ending at the same place; -d cpu_reset records the registers at each reset and EIP is 0x1313b in the block that follows every Triple fault. That is the flicker.

Step three, gdb, confirms the diagnosis without guessing. We start QEMU as above, with -gdb tcp::26000 added and no -S, wait for the fault, and attach:

$ make gdb
...output omitted...
0x0001313b in kmain () at kernel.c:100
100     kprintf("Hello World from the kernel!\n");
Breakpoint 1 at 0x13111: file kernel.c, line 92.
(gdb) info registers eip esp eflags
eip            0x1313b             0x1313b <kmain+51>
esp            0x8ffc4             0x8ffc4
eflags         0x212               [ IOPL=0 IF AF ]
(gdb) x/i $pc
=> 0x1313b <kmain+51>:  push   0x159da
(gdb) monitor info registers
...output omitted...
GDT=     000194e0 0000002f
IDT=     00019520 00000007
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
...output omitted...
(gdb) p idt_pointer
$1 = {limit = 7, base = 103712}
(gdb) p sizeof(idt)
$2 = 2048
(gdb) p/x 0x20 * 8 + 7
$3 = 0x107

The processor is frozen on the push after sti, with IF set: the first instruction at which an interrupt could be taken, and the one at which it was. monitor info registers shows the IDTR: base 0x19520, which nm -n confirms is idt, and limit 7. One entry. Gate 32 ends at byte 0x107, far outside; so does gate 13 at 0x6f and gate 8 at 0x47, which is why the fault escalated instead of being reported. p idt_pointer shows the C variable with the same 7, and sizeof(idt) shows what it should have been. The fix is the line in the diff, reversed. make test passes, and the log of the fixed kernel is the clean baseline of section 16.1.2.

Two close relatives of this bug deserve a look, because they produce different symptoms from almost the same mistake. The first is the limit counted in entries rather than bytes, IDT_ENTRIES - 1 = 255. Entries 0 to 31 then fit in the limit, entry 32 does not, and the kernel halts with a message instead of rebooting:

Exception 13: General Protection (error code 0x102)
eax=0x000000fc ebx=0x00000000 ecx=0x00000001 edx=0x00000021
esi=0x00007cd8 edi=0x0001e110 ebp=0x0008fff8 esp=0x0008ffc4
eip=0x0001313b cs=8 ds=10 eflags=0x00010212
System halted.

Same error code, same EIP, but gate 13 was reachable, so the processor could report. Under gdb with a breakpoint on the stub, the raw frame of a ring 0 fault is four words:

(gdb) b isr13
Breakpoint 2 at 0x1018d: file isr_stubs.asm, line 37.
(gdb) c

Breakpoint 2, isr13 () at isr_stubs.asm:37
37      push    dword %1
(gdb) x/4wx $esp
0x8ffb4:    0x00000102  0x0001313b  0x00000008  0x00010212
(gdb) x/i *(uint32_t *)($esp + 4)
   0x1313b <kmain+51>:  push   0x159da
(gdb) p/x $eflags
$1 = 0x12
(gdb) p *(uint32_t *)$esp >> 3
$2 = 32
(gdb) p idt_pointer
$3 = {limit = 255, base = 103712}

Error code, EIP, CS, EFLAGS with RF set; $eflags inside the stub is 0x12, IF clear, because the gate is an interrupt gate; and the error code shifted right by three is the vector of the offending gate. The second relative is the unremapped PIC, pic_init() left out. The IDT is fine; the timer arrives on vector 8, which has a handler, and the kernel reports a double fault that is not one:

Exception 8: Double Fault (error code 0x13136)
eax=0x000000b8 ebx=0x00000000 ecx=0x00000001 edx=0x00000021
esi=0x00007cd8 edi=0x0001e110 ebp=0x0008fff8 esp=0x0008ffc8
eip=0x00000008 cs=212 ds=10 eflags=0x000000c0
System halted.

Everything in the last two lines is shifted by one word. A real #DF pushes an error code, so isr8 is an ISR_ERRCODE stub and pushes no dummy one; a hardware interrupt on vector 8 pushes no error code either, so struct registers is read four bytes off: the “error code” 0x13136 is the saved EIP, eip=8 is the saved CS, cs=212 is the saved EFLAGS. An error code that looks like an address is the signature of this bug, and -d int settles it, v=08 e=0000 i=0 cpl=0 IP=0008:00013136: a hardware interrupt, i=0, with no error code, at the same push after sti. addr2line -e build/os/os 0x13136 says kernel.c:100 again. Whenever the first interrupt after sti goes wrong, the IDT limit, the PIC offsets and the gate for 0x20 are the three suspects, in that order.

16.3.2 Case 2: the user program dies at its first instruction

The bug is in task.c, where the user stack of a new task is mapped. One flag is missing:

--- chapter14/os/os/task.c
+++ ii_nouser/os/os/task.c
@@ -101,7 +101,7 @@
     frame = pmm_alloc_frame();
     if (frame == 0)
         panic("task_create_user: out of frames");
-    paging_map_in(dir, USER_STACK_TOP - USER_STACK_SIZE, frame, PAGE_WRITE | PAGE_USER);
+    paging_map_in(dir, USER_STACK_TOP - USER_STACK_SIZE, frame, PAGE_WRITE);
 
     t = task_alloc(name, (void (*)(void))entry, user_task_start, dir);
     t->user_stack_top = USER_STACK_TOP;

This kernel boots, mounts the disk, lists the directories and loads both programs; then:

exec /bin/hello: segment 0 at 0x08048000, 343 bytes in file, 343 in memory
exec /bin/count: segment 0 at 0x08048000, 331 bytes in file, 331 in memory

Page fault at 0x7ffffffc: protection violation, write, user mode, eip=0x08048080 (error code 0x7)
killing task 1 (/bin/hello)

Page fault at 0x7ffffffc: protection violation, write, user mode, eip=0x08048080 (error code 0x7)
killing task 2 (/bin/count)
all processes finished, 32443 frames free

The kernel survives, because chapter 13 taught the page-fault handler to kill a ring 3 task instead of panicking, and its message is step one already done for us. Decode it before touching anything. 0x7ffffffc is four bytes below USER_STACK_TOP: the first word of the user stack. eip=0x08048080 is _start, the first instruction of the program, call main, which pushes a return address, and that is the write. Error code 0x7: P = 1, the page is present; W/R = 1, a write; U/S = 1, from ring 3. A present page that ring 3 may not write to, when the stack is supposed to be writable by ring 3, is a permission bit, and there are only two: PAGE_WRITE and PAGE_USER. Had the stack not been mapped at all, the code would have been 0x6; we tried that variant too, by mapping the page one page too high, and the handler printed page not present, write, user mode ... (error code 0x6): the low bit separates “not there” from “not allowed”.

gdb confirms it in the page tables themselves. We saw the raw frame at isr14 in section 16.1.3; here is the rest of the session, from the handler:

(gdb) b page_fault_handler
Breakpoint 3 at 0x134ca: file paging.c, line 104.
(gdb) c

Breakpoint 3, page_fault_handler (regs=0xc0001fc4) at paging.c:104
104     asm volatile("mov %%cr2, %0" : "=r"(cr2));
(gdb) p/x regs->err_code
$2 = 0x7
(gdb) x/i regs->eip
   0x8048080:   call   0x80480fc
(gdb) p/x current_task->page_directory[0x7ffffffc >> 22]
$3 = 0x125023
(gdb) set $pt = (uint32_t *)(current_task->page_directory[0x7ffffffc >> 22] & 0xfffff000)
(gdb) p/x $pt[(0x7ffffffc >> 12) & 0x3ff]
$4 = 0x124003
(gdb) set $pt2 = (uint32_t *)(current_task->page_directory[0x08048000 >> 22] & 0xfffff000)
(gdb) p/x $pt2[(0x08048000 >> 12) & 0x3ff]
$5 = 0x122027
(gdb) monitor info mem
0000000000000000-0000000007fe0000 0000000007fe0000 -rw
0000000008048000-0000000008049000 0000000000001000 urw
000000007ffff000-0000000080000000 0000000000001000 -rw
00000000c0000000-00000000c0005000 0000000000005000 -rw
(gdb) bt
#0  page_fault_handler (regs=0xc0001fc4) at paging.c:104
#1  0x00012de1 in isr_dispatch (regs=0xc0001fc4) at isr.c:73
#2  0x000102a6 in isr_common () at isr_stubs.asm:115
#3  0xc0001fc4 in ?? ()
#4  0x00000fe0 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

The walk is the one of chapter 12, done by hand on the task’s directory. The directory entry for 0x7ffffffc is 0x125023: present, writable, not user, which is already wrong (get_table adds the USER bit to the directory entry only when asked for a user page, and nobody asked). The page table entry is 0x124003: frame 0x124000, flags 0x003, present and writable, no 4. Compare with the code page at 0x08048000, 0x122027: 0x027, present, writable, user, accessed. monitor info mem says it in one glance: the program’s page is urw, the stack’s page is -rw. Ring 3 may run the code and may not touch its own stack. The backtrace is the expected garbage past isr_common.

The fix is the flag. Two remarks for your own kernel. First, the kernel’s own test for a user pointer, paging_user_accessible in paging.c, which syscall.c calls on every buffer a program hands the kernel, checks exactly the bits we just read, at both levels; a kernel that has such a function can call it from the page-fault handler to print which level refused. Second, the symptom “program dies at its first instruction” has four causes with four different signatures, all visible in the raw frame: a bad entry address gives an EIP that is not _start; a missing crt0 gives a fault inside main’s epilogue when it rets to nowhere; a stack problem gives CR2 just below the stack top with EIP at the first push or call; wrong data selectors give #GP with error code 0 at the first memory access through DS. Read x/6wx $esp at the stub and you have told them apart before gdb has finished printing.

16.3.3 Case 3: the disk is read without error and everything is wrong

The bug is in ext2.c, in the one function that turns a filesystem block number into a disk address:

--- chapter14/os/os/ext2.c
+++ iii_lba/os/os/ext2.c
@@ -22,7 +22,7 @@
 
 static int read_block(uint32_t block, void *buf)
 {
-    return ata_read_sectors(EXT2_PARTITION_LBA + block * sectors_per_block,
+    return ata_read_sectors(EXT2_PARTITION_LBA + (block + 1) * sectors_per_block,
                             sectors_per_block, buf);
 }

An off-by-one of the kind that appears when someone remembers that “block 0 is the boot record, so data starts at block 1” and corrects for it in the wrong place. The serial output:

Hello World from the kernel!
CPU: GenuineIntel, QEMU Virtual CPU version 2.5+ (family 6, model 6, stepping 3)
ata: primary master "QEMU HARDDISK", 16384 sectors (8192 KiB)
ext2: volume "os01", block size 1024, 1792 inodes, 7168 blocks, revision 1
/: not a directory
/bin: not a directory
/etc/motd: not found
/log.txt: no such directory
ext2: wrote -1 bytes to /log.txt
/log.txt: not found
/: not a directory
exec /bin/hello: no such file
exec /bin/count: no such file
all processes finished, 32447 frames free

Nothing faults, nothing panics, the disk driver reports no error and the superblock is perfect: the right label, the right counts. Then the root directory is “not a directory”. This is the hardest kind of bug, the one where every component says it is fine, and the table’s advice is: when a device returns success and the data is wrong, print what you asked for, not what you got. One line in read_block:

    kprintf("read_block(%u): LBA %u\n", block,
            EXT2_PARTITION_LBA + (block + 1) * sectors_per_block);

and the output becomes:

ata: primary master "QEMU HARDDISK", 16384 sectors (8192 KiB)
read_block(2): LBA 2054
ext2: volume "os01", block size 1024, 1792 inodes, 7168 blocks, revision 1
read_block(0): LBA 2050
/: not a directory
read_block(0): LBA 2050
/bin: not a directory

Two things are visible at once. The superblock was never read through read_block, it has its own EXT2_PARTITION_LBA + 2 in ext2_mount, which is why the mount succeeded: the bug is below the first thing that uses read_block. And every inode is read from block 0. Block 0 is the boot record, never an inode table; the inode table of group 0 comes from the group descriptor, and the group descriptor was read by read_block(2) at LBA 2054. With 1 KiB blocks, two sectors per block and the filesystem starting at sector 2048, block 2 is sectors 2052 and 2053. We asked for 2054.

The host has the same disk image and better tools, and this is the moment to use them. xxd at the two addresses:

$ xxd -s $((2052*512)) -l 32 build/disk.img
00100800: 1e00 0000 1f00 0000 2000 0000 081a ef06  ........ .......
00100810: 0400 0400 0000 0000 0000 0000 0000 0000  ................
$ xxd -s $((2054*512)) -l 32 build/disk.img
00100c00: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00100c10: 0000 0000 0000 0000 0000 0000 0000 0000  ................

At 2052 there is a group descriptor as chapter 14 described it: block bitmap at 0x1e = 30, inode bitmap at 31, inode table at 32. At 2054 there are zeros, which the kernel read as “inode table at block 0” and never complained about, since 0 is a valid number. debugfs from e2fsprogs agrees about what should have been found, reading the image at the partition offset:

$ debugfs -R stats "build/disk.img?offset=1048576" | grep "Group  0"
 Group  0: block bitmap at 30, inode bitmap at 31, inode table at 32

Under gdb the same picture, from the kernel’s side, with a breakpoint on the driver and finish to see what came back:

(gdb) b ata_read_sectors
Breakpoint 2 at 0x10699: file ata.c, line 155.
(gdb) c

Breakpoint 2, ata_read_sectors (lba=2050, count=2 '\002', buf=0x170a0 <sb_raw>) at ata.c:155
155     uint8_t *p = buf;
(gdb) c

Breakpoint 2, ata_read_sectors (lba=2054, count=2 '\002', buf=0x174c0 <group_table>) at ata.c:155
155     uint8_t *p = buf;
(gdb) bt
#0  ata_read_sectors (lba=2054, count=2 '\002', buf=0x174c0 <group_table>) at ata.c:155
#1  0x0001134b in read_block (block=2, buf=0x174c0 <group_table>) at ext2.c:25
#2  0x00011467 in ext2_mount () at ext2.c:60
#3  0x0001318b in kmain () at kernel.c:112
#4  0x0001011b in _start () at entry.asm:30
(gdb) p sb.s_first_data_block
$1 = 1
(gdb) p sectors_per_block
$2 = 2
(gdb) finish
0x0001134b in read_block (block=2, buf=0x174c0 <group_table>) at ext2.c:25
25      return ata_read_sectors(EXT2_PARTITION_LBA + (block + 1) * sectors_per_block,
Value returned is $3 = 0
(gdb) x/8wx group_table
0x174c0 <group_table>:  0x00000000  0x00000000  0x00000000  0x00000000
0x174d0 <group_table+16>:   0x00000000  0x00000000  0x00000000  0x00000000

The first read is the superblock at 2050, correct; the second, block=2 for the group descriptor table, asks for 2054 where 2048 + 2 × 2 = 2052 was meant; the driver returns 0, success, and group_table is all zeros. The fix removes the + 1. Note what the driver’s success means: the ATA command was executed and sectors were transferred. The disk did exactly what it was told. Every layer that reports “fine” is only telling you that its input made sense, and the bug is always in the layer that computed that input.

Two neighbors of this bug belong in the same case. The first is the sector count rather than the LBA: outb(ATA_SECTOR_COUNT, count - 1) makes the drive prepare one sector fewer than the loop reads, and the loop then polls a drive that has nothing to give; chapter 14’s driver gives up after a million polls and prints ata: read error at LBA 2051 (status 0x50, error 0x0), status 0x50 being RDY and DSC with no DRQ, the picture of an idle drive, and ext2_mount then panics with no ext2 filesystem at sector 2048, which is the wrong diagnosis. Read the first error, not the last. A driver written from the usual examples, with an unbounded while (!(inb(ATA_STATUS) & DRQ)), hangs there silently instead, and the frozen-machine recipe applies: attach gdb, x/i $pc shows the in instruction inside the wait loop, and monitor i /b 0x1f7 shows the 0x50. The second neighbor is the controller itself. Run the chapter 14 image with the chapter 13 command line, -drive format=raw,file=build/disk.img,if=ide and no -device piix3-ide, and every register reads 0xff:

(gdb) monitor i /b 0x1f7
portb[0x01f7] = 0xff
(gdb) monitor i /b 0x1f6
portb[0x01f6] = 0xff

Chapter 14 explained why: on q35 the disk went to the AHCI controller, and nothing answers at 0x1F0. The kernel’s ata_identify recognizes 0xff as a floating bus and reports no drive, and kmain panics with a message that names the Makefile; a driver that does not check would wait for BSY to clear, forever, since 0xff has BSY set. The monitor’s i /b is the instrument for this whole family of bugs: it reads the port the kernel is polling, from outside, and either confirms that the device is where the kernel thinks it is or shows that it is not.

16.3.4 The case we did not need: a hang with no fault at all

One symptom deserves a transcript even though it fits no fault: the kernel simply stops, in the middle of its output, with interrupts enabled and no exception in the log. We removed the end-of-interrupt from isr_dispatch, two lines, and the chapter 14 kernel printed everything up to the two exec lines and went quiet. gdb attached to the running machine:

$ make gdb
...output omitted...
kmain () at kernel.c:133
133         if (task_reap() == 0)
Breakpoint 1 at 0x130e3: file kernel.c, line 92.
(gdb) info registers eip eflags
eip            0x13221             0x13221 <kmain+327>
eflags         0x202               [ IOPL=0 IF ]
(gdb) x/i $pc
=> 0x13221 <kmain+327>: jmp    0x13217 <kmain+317>
(gdb) p ticks
$1 = 1
(gdb) p current_task->name
$2 = 0x15e2c "idle"
(gdb) monitor info pic
pic0: irr=01 imr=fc isr=01 hprio=0 irq_base=20 rr_sel=0 elcr=00 fnm=0
pic1: irr=40 imr=ff isr=00 hprio=0 irq_base=28 rr_sel=0 elcr=0c fnm=0
...output omitted...
(gdb) monitor info irq
IRQ statistics for isa-i8259:
 0: 397
 4: 1
14: 133
IRQ statistics for ioapic:
 0: 397
 4: 1
14: 133

The idle loop of kmain, IF set, so interrupts are allowed, and ticks is 1: exactly one timer interrupt was ever delivered. The PIC says why. irr=01: IRQ 0 is requested again. imr=fc: IRQ 0 and 1 are unmasked. isr=01: IRQ 0 is still in service; the controller is waiting for the EOI of the first tick before it raises the second, and it has been waiting through 397 ticks of the timer. The scheduler runs on the timer, so no task ever ran, and the idle task reaps nothing forever. A kernel that works for one keystroke and then ignores the keyboard is the same bug with isr=02. info pic costs one command and answers the question that -d int cannot, because the interrupts that were never delivered are not in the log.

16.4 Exercises

Exercise 16.1. Serial port only. In a copy of chapter 13, change IDT_INTERRUPT_GATE_RING3 to IDT_INTERRUPT_GATE_RING0 in syscall_init. Boot it with -serial file: and nothing else: no -d, no gdb. From the serial output alone, decode what happened, which gate is involved and from which privilege level, and say what the processor would have done if the kernel had no handler for vector 13. Then check your answer with -d int.

Exercise 16.2. gdb only. In a copy of chapter 14, change the tss_set_kernel_stack call in schedule() so that it passes next->kernel_stack without + KERNEL_STACK_SIZE. Do not look at the serial output. With make qemu and make gdb, a breakpoint on isr128 and x/6wx $esp, find out what the first system call from ring 3 does to memory: where ESP0 pointed, what the processor pushed there, and what lived at those addresses before (p *head and the heap headers of chapter 12 will help). Explain why there is no fault at that moment, and predict when and how the damage will surface; then let the kernel run and check.

Exercise 16.3. Write a kassert(condition) macro for panic.h that prints the file, line and the text of the condition before panicking (__FILE__, __LINE__ and the # operator of the preprocessor are what you need), and make it compile to nothing when NDEBUG is defined. Add assertions to paging_map_in (the frame is page-aligned), kfree (the pointer is inside the heap and its block is in use) and schedule (interrupts are disabled: read EFLAGS with pushf; pop). Then introduce the bug of case 2 and see which assertion, if any, fires before the page fault.

Exercise 16.4. Make panic print a stack trace. With -O0 every C function starts with push ebp; mov ebp, esp, so at any point [ebp] is the caller’s EBP and [ebp + 4] the return address. Walk the chain from the current EBP, printing each return address, and stop at a zero EBP or after ten frames. Compare your trace with gdb’s bt at the same point: where do they agree, where does yours stop, and why does yours never show the function that was interrupted by the exception? Then make isr_dispatch call it with regs->ebp so that it does.

Exercise 16.5. Add -d int -D int.log to tools/serial-test.sh behind an environment variable, and make make test of chapter 14 fail if the log contains any v= entry with a vector below 32. Count the entries of a clean boot per vector, as section 16.1.2 did, and explain each count: why fourteen system calls, why no v=21, and why the count of v=20 depends on how long the test waits. What does the count become if you run the test of chapter 12 instead, and why is that one expected to contain a v=0e?

16.5 Check your understanding

  1. The serial output of a crashed kernel ends in the middle of a line. What does that tell you, and what does it not tell you?

  2. Why is the first interrupt after sti delivered at the instruction after the one following sti, and why does this matter when you read the EIP of a triple fault?

  3. -d int shows v=0d e=0000 from ring 3 just after a v=80. The error code is zero. Is the gate for 0x80 the problem? What else could raise this?

  4. A page fault reports error code 0x5 with CR2 = 0x1d000, the address of ticks. Decode it. What kind of code touched the address, and should the kernel panic? What would an error code of 0x0 at the same address mean instead?

  5. Why does bt stop or print nonsense after isr_common, and why would the same happen in a user program compiled with -fomit-frame-pointer?

  6. A kernel prints the right message, then reboots. -d int shows nothing after the last v=20. What kind of bug produces a reset without any exception in the log, and how would you confirm it?

  7. Case 3’s driver returned success for a read that produced zeros. What would it have to check to return an error instead, and why is it reasonable that it does not?

  8. Why does the chapter recommend committing every version that passes make test, rather than every version that compiles?