aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 8df25603a9fa2b1c865509031b8f2457ab351491 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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.