diff options
| author | Richard Stalin <rms@sl0p.foo> | 2026-07-11 22:30:05 +0200 |
|---|---|---|
| committer | Richard Stalin <rms@sl0p.foo> | 2026-07-11 22:30:05 +0200 |
| commit | 8d92208fcec4b01a1f6e227b188b30477087c08e (patch) | |
| tree | 6d7542649e8ce426e3324576a58f8ff17ab4fa0c /README.md | |
| download | gdb-driver-main.tar.gz gdb-driver-main.tar.xz gdb-driver-main.zip | |
Embedded gdb-Python plugin exposing a JSONL unix-socket RPC: three tiers of
control (raw tmux keys / semantic verbs / structured introspection), real
stop-event settling, and gef>-prompt command echoing so the tmux pane stays
human-legible. Includes the drive/pane client CLIs, an end-to-end smoke test,
and the TUI-driving design blueprint.
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..8df2560 --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# gdb-driver + +Puppeteer a **live** gdb (plain CLI + [GEF](https://github.com/hugsy/gef), **not** +`-tui`) programmatically over a unix socket, while it renders normally in a tmux +pane. An embedded Python plugin runs an RPC server on gdb's own thread, so +debugger state is read and written directly — and every driven command is echoed +at the `gef>` prompt exactly as if a human typed it. + +It was built for **agent-driven dynamic analysis on a livestream**: the pane is a +real, watchable gdb session (GEF's colored context is what a viewer sees), but a +script is doing the driving. + +``` + your script ──JSONL──▶ unix socket ──▶ driver.py (in gdb's Python) + │ marshals onto gdb's thread + tmux pane ◀── gef> echo + GEF context ────┘ reads/writes real state +``` + +## Why + +gdb automation usually means GDB/MI or brittle `expect` scraping. Neither gives +you *both* a clean structured API **and** a genuine, human-looking terminal +session. gdb-driver does: + +- **Three tiers of control** — raw tmux keys, semantic verbs (run-control / + breakpoints / eval), and structured introspection (regs / memory / backtrace + as data). +- **Real stop-event settling.** Mutating verbs return only after gdb's actual + `stop`/`exited` event fires — no sleeps, no guessing. The returned `state` is + final. +- **Visible by construction.** Everything is echoed at the prompt and rendered by + GEF, so the pane is legible to a human (or a stream) the whole time. +- **The escape hatch is first-class.** `drive cmd "<any gdb/GEF command>"` runs + anything — `disassemble`, `vmmap`, `checksec`, even in-gdb `python …` — so you + are never blocked by a missing verb. + +All verbs go through gdb's **CLI** path (GEF-compatible). GDB/MI is not used. + +## Requirements + +- Linux, running **inside tmux** (the driver spawns/handles panes). +- **gdb 17+** with **GEF** auto-loaded (`~/.gdbinit` → `~/.gef.py`). +- `python3` (stdlib only) for the `drive`/`pane` client CLIs. The `driver.py` + plugin runs inside gdb's own embedded Python. + +No pip install, no dependencies — it's three files and the stdlib. + +## Quickstart + +```bash +cd gdb-driver # so ./drive resolves (it's a plain executable) + +# 1. spawn a pane with gdb on a target (+ program args). Blocks until ready, +# then prints: ready pane=%N sock=/run/.../gdb-XXXX.sock +./drive spawn /abs/path/to/binary [prog-args...] +# forward environment to the inferior (shim libs, LD_PRELOAD, …): +# ./drive spawn --env LD_LIBRARY_PATH=/path/to/libs ./target arg1 + +# 2. drive it. ./drive auto-finds the socket when one pane is live (no --sock). +./drive where # main @ 0x555… at file:line (or func+0xoff) [stopped] +./drive break main # b *main+42 | b <func> | tbreak … +./drive run # settles at the next real stop (breakpoint/exit) +./drive bt / regs / mem '$sp' 64 +./drive print '(char*)$rdi'; ./drive cont; ./drive finish +./drive cmd "disassemble main" # ANY gdb/GEF command (the escape hatch) +./drive set '$r14d = 5' # patch a register live +./drive screen [color] # tmux capture of the pane (GEF context) + +# 3. tear down (graceful quit + kill pane + rm socket). Never leak panes. +./drive stop +./drive list [--prune] # inventory; --prune drops dead panes +``` + +**Socket resolution** for `./drive`: `--sock <path>` (anywhere on the line), else +`$GDB_DRIVER_SOCK`, else the single live pane (with several, it lists them). Calls +are **sequential** — single-driver, one connection at a time; a second concurrent +request gets `busy`. + +## Command surface + +- **Lifecycle / meta:** `ping` · `methods` (self-documenting verb table) · `quit` + · `spawn` · `list [--prune]` · `stop` +- **Read (never settle):** `state`/`where` · `regs` · `bt [n]` · `mem <addr-expr> + [len]` · `breakpoints`/`bp` · `screen [color]` +- **Run-control (settle on the real `stop`/`exited` event):** `run [args]` · + `start` · `cont`/`c` · `next [n]`/`n` · `step [n]`/`s` · `stepi`/`si` · + `nexti`/`ni` · `finish` · `until [loc]` +- **Breakpoints:** `break <loc>`/`b` · `tbreak <loc>` · `delete [n]` · + `watch <expr>` +- **Eval / examine / patch:** `print <expr>`/`p` · `x <fmt> <addr>` · + `set <expr>` (vars and registers: `set '$rax=1'`) · `frame <n>` · `up`/`down` +- **Escape hatch:** `cmd <any gdb/GEF command…>` +- **Raw keys:** `keys <tmux-key…>` (e.g. `keys C-c` to interrupt a running + inferior) — client-side tmux `send-keys`, so it works even while a `cont` is + blocking. + +Mutating verbs settle, then return a fresh `{cmd, state[, output, note]}`. Reads +return immediately; a program-dependent read before the inferior is stopped +returns a clean `not ready` error rather than garbage. + +## Layout + +| file | what | +|------|------| +| `driver.py` | the embedded gdb-Python plugin: unix-socket JSONL server on a background thread, marshalling to gdb's thread via `post_event`, all verbs, run-control stop-event settling, and `gef>`-prompt echoing. | +| `pane.py` | tmux spawn/stop/list, registry under `$XDG_RUNTIME_DIR/gdb-driver/`, readiness poll, socket resolution, `send_keys`. | +| `drive` | the ergonomic CLI (terse text, auto socket). Use this all day. | +| `tests/smoke.py` | end-to-end: compiles a fixture, spawns, drives the whole surface, ~28 asserts. A protocol-regression lock. | +| `TUI_DRIVING_BLUEPRINT.md` | the design / generalization reference (this is a *Level-2 embedded* adapter). | + +## Testing + +```bash +python3 tests/smoke.py # compiles a fixture, exercises the full surface +``` + +## Gotchas worth knowing + +A few that will bite (the blueprint and inline comments have the rest): + +- **Break at the `call`, not the `mov`/`lea` before it** — argument registers + (`$rdi`/`$rsi`/…) are only set *at* the call. +- **PIE raw-offset breakpoints fail before load** (`break *0x1275` → *Cannot + access memory*). Use symbol-relative: `break *main+<off>` so gdb relocates it. +- **Plugin changes need a respawn** — editing `driver.py` won't affect a running + gdb; `./drive stop && ./drive spawn …` reloads it. +- **fork + ptrace anti-debug targets** (a parent↔child `PTRACE_SEIZE` coroutine) + do **not** reproduce native behavior under gdb — a process can have only one + tracer. Observe those with eBPF/bpftrace (orthogonal to ptrace) instead of + trusting gdb-extracted intermediate state. + +## License + +No license yet — all rights reserved by the authors pending one. |
