Appendix C: Debugging concurrency
Everything in Project 1’s testing and debugging appendix still applies. What follows is the additional toolkit for this project, where bugs are no longer purely about what your code does but also about when.
Five common failure modes
Almost every non-trivial bug in this project falls into one of the following categories. Learning to tell them apart from the symptom saves enormous amounts of time.
| Symptom | Usual cause |
|---|---|
| Hangs immediately on the first switch | restore_context wrong: bad ELR_EL1 (you jumped into nothing), invalid SPSR_EL1 (illegal exception return), or misaligned sp. |
| First switch works, second hangs | save_context wrong. The first switch only tests restoring a context you built; the second tests one that save_context produced. |
| A process resumes but computes nonsense | A register is not being saved or restored, or is being restored from the wrong offset. Suspect x0 (restore it last!) or an off-by-8 in your offsets. |
| Preemption happens exactly once | SPSR_PROC still masks interrupts, or you did not re-arm the timer. |
| Everything works, then dies after a while | Stack growth. A process’s stack has grown down into its own code, or your kernel is using more of the current process’s stack than you think. |
Debugging a context switch in gdb
The single most useful thing you can do is look at a context frame directly. It is 34 8-byte values (“words”), so you can read it on one screen.
(gdb) break restore_context
(gdb) continue
(gdb) p/x $x0 # the context pointer
(gdb) x/34gx $x0 # the whole frame: x29, x30, x0..x28, sp_el0, elr, spsrReading that dump against the layout table in Quest 2 will tell you almost immediately whether a context is plausible: the last three words should be a stack-ish address (or 0), a code address in a process’s region, and a small value ending in 5 (EL1h). Zeroes or wild values in the middle mean a save/restore offset bug.
Other recipes:
| Goal | How |
|---|---|
| Watch a switch happen instruction by instruction | break restore_context, then stepi through to past the eret and inspect $pc, $sp, $cpsr. |
| Check the exception level | p/x $cpsr; the low four bits are the mode (0x5 = EL1h, 0x9 = EL2h, 0x0 = EL0t). |
| See which process is on the CPU | p *current_process (and p process_table for the whole table). |
| Stop when a specific process is scheduled | break resume if p->pid == 2. |
| Find out where a hung machine is stuck | Ctrl-c in gdb, then bt, info registers, and x/8i $pc. If $pc is in your vector table (e.g., 0x200 is a common value we’ve seen), you took an exception you don’t handle. |
| Confirm your vector table is laid out correctly | aarch64-elf-objdump -d kernel/kernel-qemu.elf, find exception_vector_table, and check that your handlers sit at multiples of 0x80 from its start. (On Linux/WSL the tool is aarch64-linux-gnu-objdump; our build system normally picks the right prefix for you, but you have to type it yourself here.) |
Techniques that work better than a debugger here
- Print, but print carefully. A
printfin the middle of your switch path changes the timing of the thing you are debugging, and (until Quest 4 Part E) can itself be preempted. Prefer printing around the interesting region, or record events into a global array and dump it later. - A ring buffer of events is a classic kernel technique: create a global array of
{pid, event, timestamp}, write into it in your context switch path with no formatting or I/O at all, and dump its contents when something goes wrong. This perturbs timing far less than printing as saving data into the array is fast and the formatting only happens when you dump debug output later, and you get a history rather than a snapshot. - Fail aggressively when impossible things happen. Add
panic()calls for the things that must never happen:pick_nextfinding aRUNNINGprocess,yieldreturning, an unrecognized interrupt source, aresumeon a process with a null context. A panic at the moment an invariant breaks is much better than trying to figure out what went wrong from garbage execution further down the line. TheCHECK()macro inkernel/debug.hcan help you with this; see Appendix D for how to use it. - Reduce the problem. Use two processes instead of three, such as
helloandcounterinstead ofprimecheck. This combination requires cooperative scheduling only, and thus works with the timer disabled. This simpler setting tells you whether general context switch and yield infrastructure or your timer code is at fault. - Predict the output before you run it. With two cooperatively-scheduled processes, your OS is entirely deterministic. Writing down what you expect and comparing catches misunderstandings that reading code does not.
When the machine hangs and gdb is no help: try make qemu-verbose, which passes -d int to QEMU and makes it log every exception it takes. If your kernel is stuck in an exception loop—a common cause of a mysterious hang in this project—you’ll see this immediately when running QEMU with verbose output.