Sooner or later every RISC-V project hits a bug that printf cannot catch β a crash in early boot, a wrong register, a misbehaving interrupt handler. That is when you reach for a real debugger. The good news: RISC-V has a standardized, well-supported debug stack built on GDB, OpenOCD, and JTAG. Here is how it all fits together.

The Debug Stack at a Glance
GDB <-- remote protocol --> OpenOCD <-- JTAG/probe --> RISC-V targetThree layers:
- GDB β the debugger you interact with (breakpoints, stepping, inspecting variables).
- OpenOCD β the translator that speaks GDBβs remote protocol on one side and the hardware debug transport on the other.
- The RISC-V Debug Spec β the standardized on-chip debug module OpenOCD drives, so one toolchain works across vendors.
The RISC-V Debug Specification
A big reason RISC-V debugging βjust worksβ across hardware is the official RISC-V Debug Specification, which standardizes the on-chip Debug Module (DM) and its transport. It defines how an external debugger can:
- Halt and resume harts
- Read and write registers and memory while halted
- Set hardware breakpoints and watchpoints (via trigger registers)
- Access the core over JTAG (the most common debug transport)
Because this is standardized, OpenOCD can support many different RISC-V chips with a config file rather than bespoke drivers β the same portability dividend the ISA itself delivers.
Debugging Without Hardware: QEMU + GDB
The fastest way to start needs no probe at all. QEMU has a built-in GDB server:
# Start QEMU halted, exposing a GDB server on :1234
qemu-system-riscv64 -machine virt -nographic \
-kernel my_program.elf -s -S-s opens the gdbstub on port 1234; -S freezes the CPU at reset so you can set breakpoints before anything runs. In another terminal:
riscv64-linux-gnu-gdb my_program.elf
(gdb) target remote :1234
(gdb) break main
(gdb) continue
(gdb) info registers
(gdb) stepi # single-step one instructionThis is ideal for learning assembly β watch a0βa7, sp, and pc change instruction by instruction.
Debugging Real Hardware: OpenOCD + JTAG
For bare-metal firmware on a dev board, you connect a debug probe (built-in USB-JTAG on many boards, or an external one) and run OpenOCD:
# Start OpenOCD with the board's config; it exposes a GDB server on :3333
openocd -f interface/your-probe.cfg -f target/your-riscv-soc.cfgThen attach GDB and, if needed, flash:
riscv64-unknown-elf-gdb firmware.elf
(gdb) target extended-remote :3333
(gdb) load # flash the firmware to the target
(gdb) monitor reset halt # OpenOCD command: reset and halt
(gdb) break main
(gdb) continuemonitor passes commands straight to OpenOCD, which is how you reset, halt, and control the chip at the hardware level.
Hardware vs Software Breakpoints
Worth understanding for embedded work:
- Software breakpoints replace an instruction with the
ebreakinstruction in RAM. Unlimited in number, but only work in writable memory. - Hardware breakpoints use the coreβs trigger registers β they work even in flash/ROM (where you cannot modify code), but the chip has a limited number (often 2β8).
When debugging code running from flash, you depend on hardware breakpoints, so use them sparingly.
Tools Beyond GDB
The ecosystem is rich:
- VS Code with the Cortex-Debug or native debug extensions wraps GDB+OpenOCD in a GUI.
ebreakin your code forces a trap to the debugger β handy as a programmatic breakpoint.- objdump / addr2line from the toolchain turn a crash address into a source line.
- Tracing (where supported by the SoC) records execution history for post-mortem analysis.
A Practical Workflow
- Reproduce in QEMU first when possible β fastest iteration, no hardware.
- Move to hardware + OpenOCD for timing-sensitive or peripheral bugs QEMU cannot model.
- Use
monitor reset halt, set a breakpoint before the fault, and single-step into it. - Inspect
mcause/scauseandmepc/sepcon a trap to find why and where it faulted.
The Bottom Line
RISC-V debugging stands on a standardized foundation: the RISC-V Debug Spec defines the on-chip debug module, OpenOCD drives it over JTAG, and GDB gives you the familiar interface. Start in QEMU with -s -S for zero-cost single-stepping, then graduate to OpenOCD + a probe for real hardware. Master info registers, stepi, and reading the trap CSRs, and almost no RISC-V bug can hide from you.
Part of my RISC-V series. See also the assembly tutorial and interrupts on RISC-V.



