Tutorial: GDB Debugging on EGOS
Author: Yutong Wang
Date: 02/23/2026
Setup
egos runs as guest code inside QEMU (the emulator), while GDB runs on your host machine and does not work like usual debugging. Instead, GDB talks to QEMU using the GDB Remote Serial Protocol.
- QEMU emulates a RISC-V machine (CPUs/harts, RAM, devices, interrupts) and executes code on egos. It exposes a remote debugging port, so that we can stop / resume the guest CPUs, single step through some code, and inspect registers / memory.
riscv-none-elf-gdbis an RSP client, which loads symbols from compiled ELFs, and translates your debugging commands into RSP requests to QEMU.
Our Makefile defines a qemu-gdb target that starts a GDB server on TCP port 6640. On terminal A, run
make qemu-gdb
This command compiles the egos code, launches and prints a console. The -gdb tcp::6640 argument indicates that the GDB server is listening on port 6640. But execution is paused on the very first instruction, until GDB says “continue”.
On terminal B, start GDB using
riscv-none-elf-gdb
This command automatically loads the .gbdinit file, which attaches this process to QEMU’s remote target 127.0.0.1:6640. It also runs
add-symbol-file build/release/egos.elf
which loads symbol table (function, variable, and address mappings) from egos.elf into the current debugging session. This is required because QEMU knows nothing about symbols; it only knows addresses and bytes. GDB maintains a symbol mapping that is in sync with the binary image booted by QEMU.
Our Makefile compiles everything in earth/, grass/, and library/ to this ELF file, but not system or user apps. If you want to set a break point in these files, you need to add symbol table from the compiled ELF file under build/release.
Workflow
GDB is powerful because it can stop / resume execution at any point of code. The core workflow is:
1) set breakpoint where you want to stop, 2) continue to run until a breakpoint hits, 3) inspect state (registers/variables/memory), 4) step, 5) repeat.
Breakpoints
Set a breakpoint on a function:
(gdb) b excp_entry
Set a breakpoint by file and line number:
(gdb) b kernel.c:100
List breakpoints:
(gdb) info breakpoints
Breakpoints can be disabled, enabled, and deleted (by number):
(gdb) disable 1
(gdb) enable 1
(gdb) delete 2
Tips:
break <location> if <condition>sets a breakpoint at the specified location, but only breaks if the condition is satisfied. This is useful because some functions are executed many times during OS booting. For example, you can set a breakpoint with the conditionif core_to_proc_idx[core_in_kernel] >= 5, so that triggers only when user applications are running, not system apps.- Alternatively,
cond <number> <condition>adds a condition on an existing breakpoint. tbreakcreates a temporary breakpoint, which is automatically removed after hitting it once.
Running
Continue the program:
(gdb) c
This runs your code until a breakpoint is encountered or you interrupt it with control-c.
Finish the current function’s execution (run until the current stack frame returns):
(gdb) finish
This is useful when you step into the function to see whether it’s entering/executing correctly, then finish to get back to the caller without stepping through every line of code.
advance <location> runs code until the instruction pointer gets to the specified location.
Stepping
Step a line of C code, and step into a function when there is a function call:
(gdb) s
Step a line of C code, but step over a function call:
(gdb) n
stepi/si and nexti/ni do the same thing for assembly instructions rather than lines of C code.
Layouts
GDB has a text user interface that shows useful information like code listing and disassembly
layout srcshows you the C source filelayout asmshows the assembly codelayout splitshows both the assembly and the source
Examining
Low-level memory examine command:
(gdb) x/format address
Example: x/x 0x80001000 examines memory in hex at the address 0x80001000.
x(hexidecimal)d(decimal)u(unsigned decimal)a(pointer)i(assembly instruction, disassemble the expression as code)t(binary)c(char)s(string until nul-terminator)
These can be combined with count and unit size. For example,
x/16wxinspects 16 words (each 32-bit) in hexx/32bxinspects 32 bytesx/10i $pcdisassemble 10 instructions starting at PC
The print command evaluates a C expression and prints the results as its proper type:
(gdb) p expr
(gdb) p/x expr
Sometimes this can be more useful than x, because it returns cleaner result. For example, you can do p *(struct process *0x1000), which is easier to read than x/16x 0x1000.
More examining commands
info registersprints the value of every general-purpose register (32 of them in RISC-V)info frameprints the current stack frameinfo localsprints the value of every local variablebacktraceprints the backtrace of all stack frames
Multi-core debugging
egos supports multiple CPUs. GDB represents each emulated CPU as a thread. Each “thread” corresponds to a CPU context, each with its own PC, registers, state, etc.
info threadslists all threads (CPU cores) known to GDBthread <n>switches the current context to thread<n>- Once you switch threads, commands like
bt,step,info registersapply to the selected thread.
When multiple cores run, single-stepping one core while others continue can create confusing results. You can use GDB scheduler-locking so other threads don’t run while you’re stepping through a specific thread:
(gdb) set scheduler-locking on
(gdb) set scheduler-locking off # normal behavior
It also helps to check info threads and confirm which thread you are currently running.