| Mode | Name | Size | |
|---|---|---|---|
| -rw-r--r-- | .gitignore | 234 | logstatsplainblame |
| -rw-r--r-- | README.md | 6499 | logstatsplainblame |
| -rw-r--r-- | TUI_DRIVING_BLUEPRINT.md | 18782 | logstatsplainblame |
| -rwxr-xr-x | drive | 7898 | logstatsplainblame |
| -rw-r--r-- | driver.py | 20153 | logstatsplainblame |
| -rw-r--r-- | pane.py | 6504 | logstatsplainblame |
| d--------- | tests | 36 | logstatsplain |
gdb-driver
Puppeteer a live gdb (plain CLI + 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/exitedevent fires — no sleeps, no guessing. The returnedstateis 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-gdbpython …— 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 thedrive/paneclient CLIs. Thedriver.pyplugin runs inside gdb's own embedded Python.
No pip install, no dependencies — it's three files and the stdlib.
Quickstart
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/exitedevent):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-cto interrupt a running inferior) — client-side tmuxsend-keys, so it works even while acontis 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
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 themov/leabefore 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.pywon't affect a running gdb;./drive stop && ./drive spawn …reloads it. - fork + ptrace anti-debug targets (a parent↔child
PTRACE_SEIZEcoroutine) 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.
