aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorRichard Stalin <rms@sl0p.foo>2026-07-11 22:30:05 +0200
committerRichard Stalin <rms@sl0p.foo>2026-07-11 22:30:05 +0200
commit8d92208fcec4b01a1f6e227b188b30477087c08e (patch)
tree6d7542649e8ce426e3324576a58f8ff17ab4fa0c /README.md
downloadgdb-driver-main.tar.gz
gdb-driver-main.tar.xz
gdb-driver-main.zip
gdb-driver: puppeteer live gdb (plain CLI + GEF) over an RPC socketHEADmain
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.md134
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.