how to debug a bare-metal raspberry pi 5 on macos with gdb & openocd

a step-by-step technical guide for setting up and using the raspberry pi debug probe with openocd and gdb to perform hardware-level debugging of a bare-metal program on the raspberry pi 5.
embedded
raspberry pi
code
Author

Devansh Lodha

Published

September 9, 2025

the problem

when developing bare-metal code, the system will eventually fail to boot with no serial output. without printf, you are debugging blind. a hardware debugger is the only way to gain direct control over the cpu and diagnose the state of the machine.

this guide details the complete workflow for debugging a bare-metal program on the raspberry pi 5 using the official debug probe on macos.

debug session architecture

the system consists of four components. gdb is the client application on your mac. openocd is the server that translates gdb’s generic commands into hardware-specific protocols. the raspberry pi debug probe is the physical hardware adapter (a cmsis-dap compliant device). swd (serial wire debug) is the two-wire protocol used to communicate with the arm cortex cores on the pi 5.

the control flow is: gdb -> openocd -> debug probe -> pi 5 cpu.

strategy: the parking stub

the pi 5 firmware establishes memory protection in high-privilege exception levels (el2/el3) during boot. this blocks gdb’s load command, which needs to write your kernel to ram.

the strategy is to bypass this by booting a minimal “parking stub” from the sd card. this stub’s only job is to put the cpu in a clean, unprotected infinite loop. with the cpu parked, we can connect the debugger, take control, and use load to overwrite the stub in ram with our actual kernel.

prerequisites & installation

hardware

  • raspberry pi 5
  • raspberry pi debug probe
  • microsd card
  • macos machine

software (macos)

all tools are installed via homebrew.

install openocd, the debug server.

brew install open-ocd

install the aarch64 cross-compilation toolchain. this package provides the assembler (as), linker (ld), objcopy, and gdb. this is a critical step; do not just install aarch64-elf-gdb.

brew install aarch64-elf-gcc

installation verification

after installation, you must verify that the tools are available in your shell’s path. close and re-open your terminal, then run:

which openocd
# expected: /opt/homebrew/bin/openocd

which aarch64-elf-gdb
# expected: /opt/homebrew/bin/aarch64-elf-gdb

which aarch64-elf-as
# expected: /opt/homebrew/bin/aarch64-elf-as

if any command reports “not found,” see the troubleshooting section.

configuration files & the parking stub

the parking stub

create a subdirectory halt_stub in your project.

  • halt_stub/start.s:
// halt_stub/start.s
.section ".text.boot"
.global _start

_start:
    wfe // wait for event (low-power idle)
    b _start
  • halt_stub/linker.ld:
// halt_stub/linker.ld
ENTRY(_start)
SECTIONS
{
    // the physical address the pi firmware expects for a kernel.
    . = 0x80000;
    .text : { *(.text.boot) }
}
  • makefile rule: add this to your project’s makefile.
##----------------------------------------------------
## halt stub
##----------------------------------------------------
.PHONY: halt_stub
HALT_STUB_IMG = halt_stub/halt_stub.img

# correct toolchain prefix for homebrew.
CROSS_COMPILE = aarch64-elf-

halt_stub: $(HALT_STUB_IMG)

$(HALT_STUB_IMG): halt_stub/start.s halt_stub/linker.ld
    @echo "building halt stub..."
    @$(CROSS_COMPILE)as -o halt_stub/start.o halt_stub/start.s
    @$(CROSS_COMPILE)ld -T halt_stub/linker.ld -o halt_stub/halt_stub.elf halt_stub/start.o
    @$(CROSS_COMPILE)objcopy -O binary halt_stub/halt_stub.elf $(HALT_STUB_IMG)

the sd card

  • format (macos): use the disk utility app. select the sd card device and click erase. set the format to exfat and the scheme to master boot record (mbr). the mbr scheme is critical.

  • firmware: you need the official raspberry pi firmware. the most reliable source is a fresh raspberry pi os lite (64-bit) image. copy the contents of its boot partition to your sd card. the overlays directory is not needed for this workflow and can be deleted. source.

  • config.txt: create config.txt on the sd card root with this line:

enable_jtag_gpio=1

openocd configuration

  • interface/cmsis-dap.cfg:
# interface/cmsis-dap.cfg
adapter driver cmsis-dap
  • target/raspberrypi5.cfg:
# target/raspberrypi5.cfg
# configuration derived from raspberry pi forums community work.
transport select swd
adapter speed 4000
reset_config srst_push_pull srst_only
set _CHIPNAME bcm2712
set _DAP_TAPID 0x4ba00477
set _CHIPCORES 4
swd newdap $_CHIPNAME cpu -expected-id $_DAP_TAPID
dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu
set _DBGBASE {0x80010000 0x80110000 0x80210000 0x80310000}
set _CTIBASE {0x80020000 0x80120000 0x80220000 0x80320000}
for { set _core 0 } { $_core < $_CHIPCORES } { incr _core } {
    set _CTINAME $_CHIPNAME.cti$_core
    set _TARGETNAME $_CHIPNAME.cpu$_core
    cti create $_CTINAME -dap $_CHIPNAME.dap -ap-num 0 -baseaddr [lindex $_CTIBASE $_core]
    target create $_TARGETNAME aarch64 -dap $_CHIPNAME.dap -coreid $_core -dbgbase [lindex $_DBGBASE $_core] -cti $_CTINAME
    $_TARGETNAME configure -event gdb-attach { halt }
}
targets $_CHIPNAME.cpu0
init
$_CHIPNAME.cpu0 configure -event reset-init { halt }

gdb automation (.gdbinit)

create a .gdbinit file in your project root. gdb runs these commands on startup. this file automates the initial connection.

# .gdbinit
target extended-remote :3333
monitor reset halt

the debugging workflow

1. build binaries

build your real kernel with debug symbols. this is essential for gdb to map executable code back to your source files. in your makefile, ensure your build rule adds the -g flag to gcc/clang or -C debuginfo=2 to rustc.

# example makefile rule for a debug build
debug-build: CFLAGS += -g # or RUSTFLAGS += -C debuginfo=2
debug-build: $(KERNEL_BIN)

run make debug-build.

then, build the parking stub.

make halt_stub

2. prepare the sd card

rename your real kernel: mv kernel8.img kernel8_real.img. rename the stub to take its place: mv halt_stub/halt_stub.img kernel8.img. copy the new kernel8.img (the stub) to the sd card root.

3. connect and power on

insert the sd card into the pi 5. connect the debug probe cable from the pi’s 3-pin debug header to the probe’s port ‘d’ (debug). power on the pi. it will boot the stub and park itself.

4. launch debug tools

in terminal 1, start openocd. it will connect and wait for gdb. leave this running.

make openocd-pi5

in terminal 2, start gdb. it will start and, using .gdbinit, automatically connect and halt the target. you will be at the (gdb) prompt.

make gdb-pi5

5. take control in gdb

all subsequent commands are typed in terminal 2 at the (gdb) prompt.

(Note: When copying commands from the blocks below, be sure to omit the leading (gdb) prompt.)

load your real kernel into ram. this overwrites the stub.

(gdb) load

set a breakpoint. you may need to find your kernel’s entry point first, as the name might be mangled by the compiler.

(gdb) info functions main
# output shows the real name, e.g., 'kernel::main'
(gdb) break 'kernel::main'

set the program counter to tell the cpu where to start executing.

(gdb) set $pc = 0x80000

run.

(gdb) continue

gdb will now report that it has hit your breakpoint. you have a live debug session.

gdb primer

the text user interface (tui)

  • ctrl+x a: toggle tui mode (source, assembly, register, and command panes).
  • ctrl+x o: cycle the active window for scrolling.

key commands

command abbr. description
continue c resume execution until next breakpoint.
next n execute current source line, stepping over function calls.
step s execute current source line, stepping into function calls.
stepi si step one assembly instruction.
finish execute until the current function returns.
break <loc> b set a breakpoint at a function, file:line, or *address.
info breakpoints i b list all breakpoints.
delete <num> d delete a breakpoint by its number.
print/x <expr> p/x print a variable or expression in hexadecimal.
info registers i r display all cpu registers.
x/[n][f][u] <addr> x examine memory at <addr>.
backtrace bt display the call stack.

troubleshooting

“aarch64-elf-as: command not found”

your shell’s path is not configured correctly. for macos/zsh, the definitive fix is to add homebrew’s environment setup to your profile. run this command once, then restart your terminal.

echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile

“openocd fails to connect”

check the physical setup. is the pi powered on? is the probe on port ‘d’? is config.txt correct?

“gdb load fails with data abort”

you did not use the parking stub method. ensure kernel8.img on the sd card is the stub, and power-cycle the pi before starting the debug session.


if you have questions or find issues with this guide, please leave a comment. this resource can be improved with your feedback.