pidb is a debugger for bare-metal ARM programs that have no OS, no gdbserver, and no debug port
to attach to. It runs on a second Raspberry Pi, wired directly to the target board, and takes
control of the running target by hijacking a GPIO interrupt and injecting machine code over a
bit-banged serial link.
Once attached, you can read and write arbitrary memory, inspect and drive GPIO pins, single-step through code that has no stepping support, and soft-restart the target — all from a menu-driven UI on an LCD, navigated with an IR remote.
(need to add photo/demo here still)
On a bare-metal target there is nothing to attach a debugger to. No OS means no ptrace, no
gdbserver, no signals, no runtime to ask questions of. If the program misbehaves, your options are
printk and a reboot.
pidb solves this from the outside: a second Pi acts as the debugger, drives an interrupt line into
the target, and ships executable code to it on demand.
┌──────────────────┐ ┌──────────────────┐
│ DEBUGGER PI │ doorbell (GPIO 20) ───────▶ │ TARGET PI │
│ │ │ │
│ ST7735R LCD │ bit-banged UART │ GPIO interrupt │
│ IR remote │ TX 16 / RX 19 ◀──────────▶ │ handler │
│ menu state │ packet protocol │ JIT exec buffer │
└──────────────────┘ └──────────────────┘
1. Interrupt hijacking. The debugger asserts a GPIO "doorbell" line (pin 20). The target's GPIO interrupt fires, yanking it out of whatever it was doing and into a debug handler that parks it, waiting for commands. No cooperation from the target program is required beyond linking the handler.
2. Bit-banged UART + packet protocol. The two Pis talk over a software UART on GPIO 16/19 (the hardware UART is left free). A small framed protocol carries commands and responses:
enum {
PKT_READY = 0x4e,
PKT_ACK = 0x4c,
PKT_RESP = 0x4d,
PKT_CMD_JIT = 0x01,
PKT_CMD_MEM = 0x02,
PKT_CMD_GPIO = 0x03,
};3. JIT code injection. Rather than implementing a command interpreter on the target, the debugger ships raw ARM machine code across the wire and the target executes it out of a buffer. Each operation is a precompiled blob:
| Payload | Purpose |
|---|---|
get32_jit / put32_jit |
arbitrary memory read/write |
gpio_set_on_jit / gpio_set_off_jit |
drive a pin |
gpio_set_mode_jit / gpio_get_mode_jit |
configure / query pin mode |
jit_read_cpsr_code |
processor status |
jit_read_dfsr_code / jit_read_dfar_code |
data fault status / address |
jit_read_ifsr_code / jit_read_ifar_code |
instruction fault status / address |
soft_restart_jit |
restart the target in place |
minimal_step_payload |
single-step engine (see below) |
Because a fixed blob can't be recompiled per call, argument_injector() patches the arguments
directly into the machine code before it goes over the wire — rewriting the immediate operands so a
static payload can be invoked with dynamic values:
int argument_injector(uint32_t *jit_code, uint32_t words_n,
uint32_t *args, uint32_t args_n,
uint32_t *out);4. Single-stepping without stepping support. ARM's CP14 debug coprocessor supports mismatch breakpoints: set the breakpoint value register to the current PC and configure the breakpoint control register to fire on any instruction that is not that address. The next instruction executed traps. Chain that and you have single-stepping on hardware that has no single-step mode.
The stepping payload is compiled standalone and embedded as a byte array:
arm-none-eabi-gcc -O2 -static -nostdlib -march=armv6k -marm -T payload.ld minimal-step.c \
-o minimal-step-payload.elf
arm-none-eabi-objcopy -O binary --only-section=.text minimal-step-payload.elf minimal-step-payload.bin
xxd -i minimal-step-payload.bin > minimal-step-payload.h5. The UI. A full-screen interface on an ST7735R LCD over SPI, with a custom graphics and font library. Navigation is by IR remote — a hand-rolled NEC-protocol decoder on GPIO 21 that times pulse widths against the mark/space thresholds and maps them to key codes.
Screens: main menu, GPIO dashboard (live pin states, editable), memory viewer (browse and patch arbitrary addresses), memory address entry, single-step control, and reset.
- Memory read/write at arbitrary addresses on a running target
- GPIO inspection and control — live pin state dashboard, toggle pins, change modes
- Single-stepping via CP14 mismatch breakpoints, on targets with no stepping support
- Hardware breakpoints and watchpoints (CP14 BVR/BCR, DSCR)
- Fault register inspection — CPSR, DFSR, DFAR, IFSR, IFAR
- Soft restart of the target without a power cycle
- Self-debug mode (
self = 1inmain.c) — the debugger can attach to itself for development
| Debugger | Raspberry Pi (ARM1176, bare metal) |
| Target | Raspberry Pi (ARM1176, bare metal) |
| Display | ST7735R LCD over SPI |
| Input | IR receiver on GPIO 21 (NEC protocol) |
| Link | Bit-banged UART, TX GPIO 16 / RX GPIO 19 |
| Doorbell | GPIO 20 (debugger → target interrupt line) |
| Path | Contents |
|---|---|
debugger/main.c |
UI state machine, top-level debugger loop |
debugger/jdebug.c / jtarget.c |
debugger-side and target-side entry points |
debugger/comms/ |
packet protocol over the software UART |
debugger/jit-lib/, jit-finder.c |
JIT payloads and the runtime argument injector |
debugger/debugger-lib/ |
breakpoints, watchpoints, single-stepping engine |
debugger/ir-driver/ |
IR receiver and NEC decoder |
debugger/display-driver/, graphics-lib/, font/ |
ST7735R driver and UI rendering |
debugger/interrupts.c, interrupt-asm.S |
interrupt vectors and handlers |
Everything under debugger/ is original work, written jointly by Joe Robertazzi and
Julian Reed as a two-person project for Stanford's CS240LX.
The support libraries are not our work and are vendored here only so the project builds:
libpi/(includinglibpi/staff-src/) — course support library from Stanford CS240LX.lib/libm/— openlibm, vendored unmodified.