# 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 ""` 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 | 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 ` (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 [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 `/`b` · `tbreak ` · `delete [n]` · `watch ` - **Eval / examine / patch:** `print `/`p` · `x ` · `set ` (vars and registers: `set '$rax=1'`) · `frame ` · `up`/`down` - **Escape hatch:** `cmd ` - **Raw keys:** `keys ` (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+` 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.