Link Search Menu Expand Document

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-gdb is 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 condition if 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.
  • tbreak creates 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 src shows you the C source file
  • layout asm shows the assembly code
  • layout split shows 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/16wx inspects 16 words (each 32-bit) in hex
  • x/32bx inspects 32 bytes
  • x/10i $pc disassemble 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 registers prints the value of every general-purpose register (32 of them in RISC-V)
  • info frame prints the current stack frame
  • info locals prints the value of every local variable
  • backtrace prints 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 threads lists all threads (CPU cores) known to GDB
  • thread <n> switches the current context to thread <n>
  • Once you switch threads, commands like bt, step, info registers apply 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.