Debugging
Debugging is essential for finding bugs and truly understanding what your program does. While different programming languages have various debugging approaches, for low-level programs like kernels, gdb
is an extremely powerful and useful tool.
Unlike regular programs where you can directly run gdb
on the compiled binary, kernel debugging requires additional steps since we're running our code in a VM.
There are several debugging approaches:
- Create logfiles
- Print to the terminal (via QEMU)
- Use
gdb
The first two methods are similar, with the main difference being the output destination - file vs terminal. Choose based on your preference.
GDB with QEMU
QEMU can be configured to wait for a GDB connection before executing any code, enabling debugging. Here's how to set it up:
- Run
cargo debug
in the src/kernel directory (this is an alias fordebug = "run debug"
) - Cargo will build and run
runner.sh
, which adds the appropriate QEMU flags based on the mode (debug, test, or normal) - For debugging, the crucial flags are
-s -S
- A window will open & QEMU will wait for GDB to connect before starting execution
Connect GDB from another terminal:
gdb
(gdb) target remote localhost:1234
Load debug symbols:
(gdb) symbol-file <kernel bin>
Example debugging session:
gdb
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()
(gdb) symbol-file kernel.b
Reading symbols from kernel.b...done.
(gdb) break kernel_main # Add breakpoint to any kernel function
Breakpoint 1 at 0x101800: file kernel/kernel.c, line 12.
(gdb) continue # Qemu starts the kernel
Breakpoint 1, kernel_main (mdb=0x341e0, magic=0) at kernel/kernel.c:12
12 {
Rust & Debugging
Rust's name mangling can make debugging challenging since function names may be modified. To ensure a function name remains unchanged for debugging purposes, add the following attribute:
#![allow(unused)] fn main() { #[no_mangle] }
This prevents rustc
from modifying the function name, making it easier to set breakpoints.