Debugging xv6-riscv
Published:
Debugging xv6-riscv
Debugging xv6 is an eye-opener for anyone delving into operating systems. It lets you trace code execution step-by-step and follow the boot-up process closely. The xv6-riscv codebase splits into user mode and kernel mode. Since the processor (or the qemu emulator) switches between these modes using CPU instructions, you can only debug one mode at a time. Typically, most of the debugging focuses on the kernel mode.
Prerequisites
First, install the required tools by following the instructions here.
Once the tools are ready, clone the xv6-riscv repository:
git clone https://github.com/mit-pdos/xv6-riscv
Setting Up the Debugging Environment
Open two terminal sessions and navigate to the cloned repository directory in both. In the first terminal, run:
make CPUS=1 qemu-gdb
This starts xv6 in the qemu emulator with GDB debugging enabled. You should see output similar to this:
*** Now run 'gdb' in another window.
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 1 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::26000
Since xv6 runs inside qemu, direct debugging isn’t possible. Instead, the Makefile enables qemu to expose a TCP port (in this case 26000) for remote debugging via GDB, making it efficient and powerful for stepping through the xv6 code.
Connecting with GDB
In the second terminal, start GDB:
gdb-multiarch
You’ll see output like this:
[Insert a screenshot of the gdb-multiarch
session opening here]
Load the kernel symbols:
file kernel/kernel
Switch to the source layout view:
layout src
This shows the kernel source code, letting you interactively step through and analyze it.
Debugging the Scheduler Function
One of the most interesting functions to debug is scheduler
in proc.c
. It’s the core of the OS’s multitasking, deciding which process runs next.
Setting a Breakpoint in the Scheduler
Set a breakpoint at the start of the scheduler:
break scheduler
Then start execution:
continue
When the breakpoint hits, GDB pauses execution at the scheduler
function. From here, use:
next
to step line-by-line, or:
step
to dive into function calls.
Understanding Context Switching
The scheduler
runs in an infinite loop, choosing a process marked RUNNABLE
and switching to it. Let’s break it down:
- Enable interrupts with
intr_on()
to avoid deadlocks. - Iterate over the process table to find a
RUNNABLE
process. - Lock the process, set its state to
RUNNING
, and assign it to the CPU. - Call
swtch
to switch the CPU context to the selected process.
Here’s the code:
void scheduler(void) {
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;) {
intr_on();
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
c->proc = 0;
}
release(&p->lock);
}
}
}
The swtch
function saves the current CPU state and restores the next process’s state. To step into it:
step
Round-Robin Scheduling
The scheduler uses a round-robin algorithm, cycling through the process table. Each process gets its turn, as long as it’s marked RUNNABLE
. When a process’s turn ends, control returns to the scheduler, which picks the next process.
Inspecting Key Variables
Check key variables to understand what’s happening:
- Process Table (
proc
): View all processes with:print proc
- Current Process (
c->proc
): See the process currently assigned to the CPU:print c->proc
Backtracing in GDB
To see how the scheduler
function was invoked, use:
backtrace
This shows the call stack, revealing that scheduler
is part of the kernel initialization and never returns. This infinite loop ensures processes keep running in a round-robin fashion.
Wrapping Up
Debugging xv6 opens a window into the internals of an operating system. Walking through the scheduler reveals how processes are managed and highlights key concepts like multitasking, context switching, and scheduling algorithms. Using GDB backtracing to examine the call stack adds another layer of understanding.
Take the time to explore other functions and set breakpoints to uncover more about xv6. The insights you gain here will translate to a deeper appreciation of modern OS design. Happy debugging!