Debugging xv6-riscv

4 minute read

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:

  1. Enable interrupts with intr_on() to avoid deadlocks.
  2. Iterate over the process table to find a RUNNABLE process.
  3. Lock the process, set its state to RUNNING, and assign it to the CPU.
  4. 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!