summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/01-DESIGN.md
blob: c5de2daab90e8ed027e2480effd9441f40e77cb5 (plain)
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# 01 — green-eye design

> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.

## 1. Why this shape

Two facts drive the design:

1. **Frida does not run here.** Frida upstream ships no RISC-V target
   (verified against the 17.20 release assets: x86/x86_64, arm/arm64, mips,
   macOS/Windows/QNX/Prospero — no riscv64), and its agent model requires a
   hosted process. The GSP image is bare-metal RV64IMAC with its own libos.
   So the instrumentation layer is built from QEMU's gdbstub and QEMU TCG
   plugins instead (see 10 for the full comparison).
2. **The project needs scripting, not a debugger.** The recurring workflow is
   "run firmware, capture a symbol-annotated map of behaviour, diff it against
   another run". A pure-Python RSP client makes that a library call; GDB
   would make it a subprocess and a parsing problem.

Design goals, in order: direct time-to-value, scriptability, extensibility,
robustness, and honest fidelity reporting.

## 2. Component map

```
                       +-----------------------------+
                       | qemu-system-riscv64 (virt)  |
                       |  -bios none -kernel <elf>   |
                       |  gdbstub  <-- TCP           |
                       +--------------+--------------+
                                      | RSP
   greeneye/rsp.py      pure-Python RSP client
        |                            |
        |  registers, memory, Z0-Z4, continue/step/interrupt, qRcmd, qXfer
        v
   greeneye/target.py   QEMU process lifecycle, loader segments, UART/qemu logs
   greeneye/elf.py      ELF64/32 parse: segments, symbols, entry
   greeneye/symbols.py  symbol DB (ELF + Ghidra TSV + JSON) + address translation
   greeneye/session.py  hooks, breakpoints, watchpoints, stop dispatch, run loop
   greeneye/memory.py   read/write/hexdump/search/snapshot/diff
   greeneye/structs.py  declarative structure schemas + render
   greeneye/probes.py   declarative probe specs + conditions
   greeneye/record.py   session dir: events.jsonl + snapshots + session.json
   greeneye/analyze.py  summary/compare reports (symbol annotated)
   greeneye/plugins.py  plugin discovery + API
   greeneye/ghidra.py   Ghidra export/import bridge
   greeneye/cli.py      info/demo/run/attach/probe/record/analyze/compare/symbols
```

## 3. Transport: the RSP client

`rsp.py` implements the gdbstub subset QEMU needs:

* packet framing `$<data>#<checksum>`, ack/no-ack, retransmit on bad checksum;
* `QStartNoAckMode` (QEMU acknowledges with an **empty packet**, then
  switches once the client acks — a bring-up bug worth remembering);
* `qSupported`, including the stub's `PacketSize` (memory payloads are chunked
  so hex expansion never exceeds it);
* `qXfer:features:read:target.xml`, recursively inlining `xi:include` register
  files; RV64 register layout with a 33×8-byte fallback;
* `g`/`p`/`P`, `m`/`M`/`X`, `Z0-Z4`, `c`/`s`, interrupt `0x03`, `qRcmd`;
* stop-reply parsing (`T`/`S`/`W`/`X`, `watch:`/`rwatch:`/`awatch:`,
  `thread:`).

The client is deliberately blocking and simple; async/concurrency belongs in
the plugin layer, not the transport.

## 4. Session semantics (the Frida-like core)

* **Breakpoint kinds**: `sw` (default; stepped over transparently) and `hw`.
* **Hooks**: `hook(target, on_enter, on_leave)`. `on_leave` is implemented by
  reading `ra` on entry and arming a *return breakpoint*; nested calls at the
  same return address are queued (LIFO). Tail calls and asynchronous
  interrupts are not tracked — documented limitation.
* **Watchpoints**: `Z2/Z3/Z4` (write/read/access). QEMU stops *at* the
  accessing instruction, so green-eye temporarily clears the watch, steps one
  instruction, and re-arms it. Without this the target livelocks on the same
  store.
* **Stop dispatch**: every stop reads the register file, resolves the PC to a
  symbol, records an event, invokes matching hooks/watchpoints, and resumes.
* **Run loop**: `run(duration=…, max_stops=…)` services events until the
  wall-clock budget, stop budget, or target exit; the deadline path interrupts
  **once**, dispatches that stop, and returns (never interrupt twice).
* **Faults**: `ebreak`/traps surface as plain stops; `step()` walks past them.
  The demo uses this to demonstrate trap capture.

## 5. Address spaces and the symbol layer

Three address spaces must never be confused (the CUDA docs learned this the
hard way):

| Space | Meaning | Example |
|---|---|---|
| file vaddr | offset in the binary file (`libcuda+0x…`) | `0x353ef0` |
| program addr | what Ghidra shows (file vaddr + image base) | `0x453ef0` (base `0x100000`) |
| runtime addr | link address + relocation bias | `0x80000048` |

`SymbolDB` stores runtime addresses. Loaders take a `bias`:

```python
db.load_elf(image, bias=relocation)           # demo firmware: bias 0
db.load_names_tsv("names.tsv", bias=0x100000) # libcuda: file vaddr -> runtime
db.load_index_tsv("decompiled_index.tsv")     # program+file addr columns
db.load_callgraph("callgraph.tsv")            # caller/callee extras
```

`resolve()` returns the containing symbol (or nearest preceding within a
delta), and `annotate()` renders `addr name+0xoff`.

## 6. Recording format (`greeneye-session/1`)

A session directory is the durable artifact:

```
dumps/<stamp>-<name>/
  session.json       # format, name, created, counts, snapshot inventory, meta
  events.jsonl       # one JSON object per event, monotonic t + fields
  snapshots/*.bin.z  # zlib-compressed region captures (sha256 recorded)
  blobs/*            # plugin/probe dumps
  summary.md         # rendered analysis (analyze/render_summary)
```

Event types: `session_start`, `image_loaded`, `hook`, `watch`, `bp_hit`,
`leave`, `watch_hit`, `stop`, `step`, `mem_read`, `mem_write`, `snapshot`,
`snapshot_saved`, `probe_log`, `probe_dump`, `plugin*`, `note`, `exit`,
`session_end`. The format is append-only JSON lines so partial sessions stay
readable, and `compare` can profile two sessions symbolically.

This is the "complete map and recorded dump of runtime memory activity": the
event stream is the map, the snapshot/blob files are the dump.

## 7. Structures

Schemas are JSON data (`structs.schema_from_json`) with integer, float,
`bytes`, `cstr` and `ptr` fields plus optional `enum` names and notes. The
demo control block schema lives at
`firmware/uproc_demo/gsp_ctrl.schema.json`. Schemas are intentionally
declarative so they can come from a Ghidra type export or a plugin later.

## 8. Execution backends

* `QemuTarget` (`virt`, RAM, NS16550A UART) is the default and the only
  supported backend today.
* The **kernel fast path** passes `-kernel <elf>`; QEMU loads segments and
  sets PC.
* The **probe path** passes each PT_LOAD segment as a raw loader device and
  green-eye sets PC through the gdbstub — this lets a firmware linked for GPU
  physical addresses be relocated into RAM for observation.
* A **custom simulator** can be added as a session backend later (roadmap,
  not v0.1) because `Session` only assumes the RSP-like operations.

## 9. Ghidra bridge

* Out: `greeneye symbols export --names … --index … --exports … --callgraph …`
  builds `greeneye-symbols.json` from the project's existing TSV exports.
* In: `greeneye.ghidra.export_hits(session_dir, out_tsv)` writes
  `addr<TAB>hits<TAB>symbol`, and `MarkGreenEyeHits.java` annotates those
  functions in a Ghidra project (plate comments + bookmarks).
* Mapping a hit to source: symbols carrying `bucket`/`line` extras resolve to
  the decompiled corpus via `greeneye.ghidra.corpus_location()`.

## 10. Plugins

`plugins.py` discovers modules and calls `register(ge)` with a `GreenEyeAPI`
(`session`, `memory`, `symbols`, `recorder`, `hook`, `watch`, `on_event`,
`add_command`, `log`). Errors are recorded as `plugin_error` and never break a
session. MCP is a planned plugin, not an implementation (08).

## 11. Known limits (be honest)

1. QEMU `virt` models **no TU104**: GPU MMIO, falcon windows, WPR2, PMU,
   timers and FB DMA do not exist. The real GSP image will fault early; the
   `probe` command reports exactly where. Not a boot environment (06).
2. The transport is blocking; no replay of async interrupts mid-run.
3. Return hooks miss tail calls and interrupt-driven returns.
4. `qRcmd` monitor commands are host-tooling only and not portable.
5. Recorded events describe *observed* stops; instructions executed between
   stops are invisible unless the TCG plugin is enabled or a step trace is
   taken.