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 — risc-v-gsp-rm-debugger 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 risc-v-gsp-rm-debugger 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
risc-v-gsp-rm-debugger 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.
|