Files
Embedded-Hacking/debug-server.sh
T
Kevin Thomas 3e52dd0412 Add Week 4 Binary Ninja notebook and OpenOCD/flash host tooling
- WEEK04-BN.md/.pdf: full Binary Ninja dynamic + static lab for 0x0005 and 0x0008
- README: link the Week 4-BN notebook after Week 4a
- debug-server.sh/.ps1: OpenOCD launcher (USE_CORE=0, -rtos none, BP_ADDR)
- flash.sh/.ps1: UF2 flash helpers
- pyproject.toml: ruff config for root host-side scripts
2026-10-03 11:33:10 -04:00

260 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# debug-server.sh - start OpenOCD as a live GDB server for the RP2350 via the Pico Debug Probe.
#
# Synopsis:
# ./debug-server.sh
#
# Description:
# Starts OpenOCD in the foreground as a long-running GDB server. It exposes
# rp2350.dap.core0 on 127.0.0.1:3333 and leaves the core RUNNING, so that a
# debugger (Binary Ninja, or plain GDB) can attach to a target that is already
# executing and therefore has sane registers.
#
# This script is deliberately NOT flash.sh. flash.sh programs flash and exits;
# this one claims the probe and stays up so you can single-step, read memory,
# and set breakpoints. Do not run both at once -- exactly one process may own
# the debug probe.
#
# The script ends with "reset run" rather than OpenOCD's default halt. This is
# the single most important line in the file; see "Why reset run" below.
#
# Requirements:
# - Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
# - OpenOCD with the rp2350 target script installed. See PICO_OPENOCD.
# - Exactly one process may own the debug probe.
# - A firmware image already programmed into flash (use flash.sh first).
#
# Environment variables:
# PICO_OPENOCD Directory containing the openocd binary AND its scripts/
# directory. Default: $HOME/.pico-sdk/openocd/0.12.0+dev
# macOS note: an OpenOCD on PATH is frequently the x86_64
# Homebrew build, which will not run under Rosetta on some
# setups and cannot talk to the ARM64 firmware tooling. The
# Pico SDK ships an arm64 build; point this at it explicitly.
# ADAPTER_SPEED SWD clock in kHz. Default: 24000.
# This is a read-mostly debug session, so a fast clock is fine
# and makes stepping noticeably smoother. Drop it (10000 or
# lower) if the link is flaky or you are using long dupont
# wires instead of the probe's own connector.
# BP_ADDR OPTIONAL address to halt at during the startup run, as 0x
# prefixed hex, e.g. 0x10000234 for main. Unset by default,
# which is the normal "attach to a running target" behavior.
# Arms a 2-byte hardware execute breakpoint just before the
# final "reset run", so the core runs from the vector table
# and stops there on its own, with no client attached yet.
# See "Why BP_ADDR is one-shot" below for the important
# limitation. Arming a length of 2 is mandatory: the Cortex-M33
# comparators are halfword-based and reject anything else.
# USE_CORE Which cores to expose to the client. Default: 0 (core0 only).
# Accepted values:
# 0 core0 only <- use this with Binary Ninja
# 1 core1 only <- rarely useful
# SMP both cores as hwthreads <- see "Why USE_CORE=0"
# Do not change this to SMP when driving Binary Ninja. The
# explanation below is the whole reason this default is 0.
#
# Why USE_CORE=0 (core1 must be hidden from the client)
# The RP2350 has two Cortex-M33 cores. Core1 does not start on its own: nothing
# in a Pico SDK application releases it from reset unless the program
# explicitly does so. If you expose it anyway (USE_CORE=SMP), its registers
# read back as meaningless reset defaults:
#
# core1: pc 0x000000ec sp 0xf0000000 xpsr 0x09000000 lr 0x00000147
# core0: pc 0x1000320c sp 0x20081f38 xpsr 0xa9000000 lr 0x100030ab
#
# Binary Ninja's register widget renders a memory preview for every register,
# which means it treats each register value as an ADDRESS and reads it. The
# core1 values above are not real addresses. Each read data-aborts, and
# OpenOCD responds by tearing down and re-establishing the SWD debug port,
# logging a pair of lines per fault:
#
# Error: Failed to read memory at 0xf0000000
# Info : SWD DPIDR 0x4c013477
#
# That becomes a self-sustaining loop of roughly 450 fault-and-recover cycles
# every two seconds, which makes the session unusable. It looks exactly like a
# broken debugger or a broken firmware. It is neither: it is a configuration
# mismatch, and hiding core1 removes it completely.
#
# Why reset run (the target must be released, not halted)
# On RP2350, halting during reset stops the core at the boot ROM stub BEFORE
# the stack pointer is loaded:
#
# xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000 lr: 0xffffffff
#
# Those values are garbage for every core, including core0. Attaching in that
# window triggers the identical fault storm described above. Ending this
# script with "reset run" means the core starts from the vector table and is
# executing normally by the time you attach, so registers read back correct.
#
# Consequence for the client: ATTACH WHILE THE TARGET IS RUNNING. Do not
# press Reset/Restart in Binary Ninja -- it performs reset-halt and puts you
# back in the garbage window. If you must reset, send "reset run" over the
# OpenOCD telnet port (4444) instead:
#
# nc 127.0.0.1 4444 then type: reset run
#
# ...or use debug-server.sh's restart instructions in WEEK04/WEEK04-BN.md.
#
# Why BP_ADDR is one-shot (read this before relying on it)
# A breakpoint armed here DOES fire during the startup "reset run" and halts
# the core at your address, so the server comes up parked there and your
# debugger can simply attach and look at it. That part works.
#
# What you do NOT get is a reusable breakpoint. As soon as any GDB client
# connects, OpenOCD unconditionally flushes every breakpoint it is holding:
#
# Info : accepting 'gdb' connection on tcp/3333
# Debug: breakpoints.c:328 breakpoint_remove_all_internal():
# [rp2350.dap.core0] Delete all breakpoints
#
# So by the time Binary Ninja is up, the comparator is gone -- reading
# 0xE0002000 shows zeros, not your address. Consequences:
#
# * The startup stop is single-use. You cannot Resume and re-catch the
# same address.
# * You cannot use BP_ADDR to stop in a loop that is already running,
# because the core only passes that point once per reset.
# * main (0x10000234) is a good BP_ADDR value precisely because it is
# reached exactly once, right after reset.
#
# To arm anything further, or to re-arm main, do it from the OpenOCD command
# port AFTER your debugger has connected -- the order is the whole trick:
#
# nc 127.0.0.1 4444
# > bp 0x1000023e 2 hw
# > reset run
#
# Arming before the client connects does not work (flushed above), and in
# plain GDB the equivalent "hbreak" is cleared by "detach" -- both were
# verified by reading the FPB comparator registers back.
#
# Related Binary Ninja bug: Binary Ninja's own breakpoints cannot be used on
# this target at all. It sends Z0,<addr>,1 -- a 1-byte packet -- and OpenOCD
# answers "only breakpoints of two bytes length supported". Both Toggle
# Breakpoint (F2) and Add Hardware Breakpoint (F3) fail, at every address, and
# the dialog's Size field is disabled so there is no UI way around it.
# gdb_breakpoint_override hard does not change this. Hence the command-port
# workflow described above.
#
# Why the remaining OpenOCD flags are set
# gdb_breakpoint_override hard
# Force every client breakpoint onto the Cortex-M33 hardware comparators
# (the RP2350 has 8 breakpoints and 4 watchpoints). Without this, a client
# may try to write a BKPT instruction into flash at 0x10000000, which is
# read-only XIP memory, and the write fails.
# gdb_memory_map disable
# Stops the client probing the entire 32 MiB flash map on connect. Pure
# Raspberry Pi guidance for suppressing spurious "Failed to read memory"
# reports during target discovery.
# cortex_m reset_config sysresetreq
# The Cortex-M33 in the RP2350 has no VECTRESET. Without this, OpenOCD
# warns on every reset:
# VECTRESET is not supported on this Cortex-M core, using SYSRESETREQ
# instead
# NOTE: this MUST be the generic "cortex_m" command, not
# "rp2350.dap.core1 cortex_m ...". With USE_CORE=0 the core1 target does
# not exist, so a core1-scoped command aborts OpenOCD before "init" runs,
# and the script exits silently having printed nothing useful.
# adapter speed
# See ADAPTER_SPEED above.
#
# Examples:
# ./debug-server.sh
# ADAPTER_SPEED=10000 ./debug-server.sh
# PICO_OPENOCD=/opt/homebrew/bin ./debug-server.sh
# BP_ADDR=0x10000234 ./debug-server.sh
# Comes up halted at main (one-shot; see "Why BP_ADDR is one-shot").
#
# Exit status:
# * propagated from OpenOCD. A clean shutdown via Ctrl-C exits 0; an
# OpenOCD configuration error exits non-zero.
#
# Related scripts:
# flash.sh / flash.ps1 one-shot raw .bin programmer (exits when done)
#
# See also:
# WEEK04/WEEK04-BN.md the full walkthrough this script belongs to
set -euo pipefail
# --- configuration ---------------------------------------------------------
OCD="${PICO_OPENOCD:-$HOME/.pico-sdk/openocd/0.12.0+dev}"
SPEED="${ADAPTER_SPEED:-24000}"
USE_CORE="${USE_CORE:-0}"
BP_ADDR="${BP_ADDR:-}"
if [ -n "$BP_ADDR" ] && ! printf '%s' "$BP_ADDR" | grep -qiE '^0x[0-9a-f]+$'; then
echo "error: BP_ADDR must be 0x-prefixed hex, e.g. 0x10000234 (got '$BP_ADDR')" >&2
exit 1
fi
if [ ! -x "$OCD/openocd" ]; then
echo "error: OpenOCD not found at $OCD/openocd" >&2
echo " set PICO_OPENOCD=/path/to/openocd (the directory containing the openocd binary)" >&2
exit 1
fi
# --- announce, so the operator can verify intent before the target is touched --
echo "Starting OpenOCD GDB server on 127.0.0.1:3333 using $OCD/openocd"
echo "SWD adapter speed: ${SPEED} kHz"
echo "Cores exposed to GDB (USE_CORE): ${USE_CORE}"
echo "Target will be reset and released (reset run) - attach while it is running."
if [ -n "$BP_ADDR" ]; then
echo "Startup breakpoint at ${BP_ADDR} (2-byte hardware execute, one-shot)."
echo " It fires during this startup reset run. Any client connecting later"
echo " causes OpenOCD to delete it, so arm further breakpoints on port 4444"
echo " after attaching: bp <addr> 2 hw"
fi
# --- start -----------------------------------------------------------------
#
# Order matters: USE_CORE must be set BEFORE -f target/rp2350.cfg is read,
# because the target script branches on it when creating the DAP targets.
#
# "reset run" is last so it happens after init and after the target is
# examined, releasing the core rather than halting it. See the header for why.
#
# Any BP_ADDR breakpoint is inserted after "init" (the target must exist before
# a comparator can be programmed) but before "reset run" (so the core is already
# armed when it starts). The length must be 2: Cortex-M33 comparators reject
# other widths.
ocd_args=(
-s "$OCD/scripts"
-f interface/cmsis-dap.cfg
-c "set USE_CORE ${USE_CORE}"
-f target/rp2350.cfg
# Drop the hwthread RTOS the RP2350 target script attaches to core0.
#
# target/rp2350.cfg creates core0 with "-rtos hwthread", which registers a
# fake RTOS whose "current thread" is coreid+1 = 1. Binary Ninja single-steps
# with the GDB packet "vCont;s" and no thread id, i.e. thread 0. OpenOCD's
# gdb_server sees rtos->current_thread (1) != thread_id (0) and takes its
# "fake step" path, replying with a stop without ever stepping the core:
#
# gdb_server.c gdb_handle_vcont_packet(): fake step thread 0
#
# The result is that Step Into / Step Over in Binary Ninja does nothing: the
# PC never moves. Clearing the RTOS removes the mismatch so the step is real.
# Harmless for single-core use, which is all this lab does (USE_CORE=0).
-c "rp2350.dap.core0 configure -rtos none"
-c "adapter speed ${SPEED}"
-c "gdb_memory_map disable"
-c "gdb_breakpoint_override hard"
-c "cortex_m reset_config sysresetreq"
-c "init"
)
if [ -n "$BP_ADDR" ]; then
ocd_args+=(-c "bp ${BP_ADDR} 2 hw")
fi
ocd_args+=(-c "reset run")
exec "$OCD/openocd" "${ocd_args[@]}"