summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 08:36:35 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 08:36:35 -0400
commit31e27d455969d33190d28917ef98f034888d217b (patch)
treec0c4dc18e78e6a1702fcda43bc2806e7cfb7c931 /debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
parentc155ee8de5b642cbd407bf2231041f17ebe43734 (diff)
downloadopen-iov-31e27d455969d33190d28917ef98f034888d217b.tar.gz
open-iov-31e27d455969d33190d28917ef98f034888d217b.zip
debug: add green-eye GSP/uproc RISC-V VM runtime analysis tooling
Adds open-iov/debug/risc-v-gsp-rm-vm: a GDB-free instrumentation stack that boots uproc/GSP firmware in qemu-system-riscv64 and provides Frida-like hooks, breakpoints, watchpoints, memory dumps, structure decoding, snapshot diffs, recorded sessions, declarative probes, a plugin API and a Ghidra symbol/hit bridge. Verified: 25 tests green (including a live QEMU smoke test); the demo uproc boots with correct hook/leave/watch/schema/snapshot/fault evidence; the optional TCG trace plugin builds and recorded 15M trace lines in 3 s; the TU104 570.144 fwimage probe reports its first exception (load access fault at firmware vaddr 0x4009014 reading 0x2000000000000A00). Documentation: lean tool docs under debug/risc-v-gsp-rm-vm/docs/00-10, a new docs/05-operations/03-risc-v-gsp-rm-vm.md debugging document, and '# TOOL IN DEVELOPMENT - NOT READY YET!' markers in docs/README.md, the onboarding tooling table and the debugging workflows doc. Vendors qemu-plugin.h from QEMU v8.2.2 (GPL-2.0-or-later), with its license header intact, so the optional TCG plugin builds without a QEMU source tree.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md219
1 files changed, 219 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
new file mode 100644
index 0000000..c677c8d
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md
@@ -0,0 +1,219 @@
+# 03 — green-eye scripting
+
+> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
+
+green-eye scripts are ordinary Python programs run with
+`./greeneye.sh run <script.py>` (or `python3 -m greeneye run …`). The
+names `ge`, `session`/`s`, `memory`, `symbols`, `recorder` are preloaded.
+
+## 1. Attach or launch
+
+```python
+import greeneye as ge
+
+# Launch the bundled demo (QEMU -kernel fast path), recording by default.
+s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", record=True, name="my-run")
+
+# Or attach to an already-running gdbstub (e.g. started by `greeneye launch`).
+# s = ge.attach(port=4444, record="dumps/attach-run")
+
+try:
+ ...
+finally:
+ s.close() # clears breakpoints/watchpoints, kills owned QEMU
+```
+
+`launch(..., keep_alive=True)` leaves QEMU running after `close()`.
+
+## 2. Symbols
+
+```python
+sym = s.symbols.get("rpc_sum") # Symbol | None
+addr = s.symbols.lookup("rpc_sum") # int, raises if missing
+s.symbols.annotate(0x80000048) # "0x80000048 rpc_ping"
+s.symbols.resolve(0x80000050) # Symbol (containing or nearest)
+s.resolve("rpc_sum") # int | int passthrough | "0x..." prefix
+```
+
+Symbols come from ELF (demo), Ghidra `names.tsv`/`decompiled_index.tsv`
+(`symbols export`), or native JSON.
+
+## 3. Hooks and breakpoints
+
+```python
+events = []
+
+def on_sum_enter(hit):
+ events.append(("enter", hit.arg(0), hit.arg(1))) # a0, a1
+
+def on_sum_leave(hit):
+ events.append(("leave", hit.retval)) # a0 at return
+
+handle = s.hook("rpc_sum", on_enter=on_sum_enter, on_leave=on_sum_leave)
+handle.hits, handle.label, handle.addr
+handle.detach()
+
+bp = s.breakpoint(0x800000fe, callback=lambda hit: print(hit), temporary=True, kind="sw")
+s.remove_breakpoint(bp)
+```
+
+`Hit` carries `kind`, `addr`, `regs` (full register dict), `symbol`,
+`retval` (leave hits), `watch`, `stop`, `session`, plus helpers
+`hit.arg(n)` and `hit.label()`.
+
+A hook's `on_leave` is realised as a return breakpoint; the callback's
+`hit.addr`/`hit.symbol` are the hooked function (not the return site), and
+`hit.retval` is `a0`. Tail calls and interrupt returns are not tracked.
+
+## 4. Watchpoints
+
+```python
+def on_write(hit):
+ print(f"{hit.label()} written; pc={hit.pc:#x}")
+
+wp = s.watch("gsp_ctrl+0x10", size=8, kind="w", callback=on_write) # w|r|a
+wp.hits
+s.unwatch(wp)
+```
+
+The session transparently steps over the triggering store (required with
+QEMU; see 01 §4).
+
+## 5. Memory
+
+```python
+s.memory.read(0x80000840, 64)
+s.memory.write_u64(0x80000860, 1) # mailbox = PING
+s.memory.hexdump(0x80000840, 128) # annotated
+s.memory.search_string(0x80000000, 0x10000, "GREEN")
+s.memory.search_u32(0x80000000, 0x10000, 0x47535031)
+s.memory.read_cstring(0x800007d0)
+s.memory.snapshot(0x80000840, 392, name="ctrl")
+```
+
+`Memory.read`/`write` record `mem_read`/`mem_write` events; snapshots are
+saved and hashed by the recorder.
+
+## 6. Structures
+
+```python
+import json
+from greeneye.structs import schema_from_json, render, decode
+
+schema = schema_from_json(json.load(open("firmware/uproc_demo/gsp_ctrl.schema.json")))
+values = decode(schema, s.memory.read(ctrl, schema.size), base=ctrl)
+print(render(schema, values))
+```
+
+A schema is JSON: `{"name":…, "fields":[{"name","offset","fmt","size?","enum?","note?"}]}`
+with `fmt` in `u8..u64`, `s8..s64`, `f32/f64`, `bool`, `bytes`, `cstr`,
+`ptr`.
+
+## 7. Snapshots and diffs
+
+```python
+from greeneye import Memory
+
+before = s.snapshot(ctrl, schema.size, "before")
+s.memory.write_u64(ctrl + 32, 1)
+s.run(duration=0.4)
+after = s.snapshot(ctrl, schema.size, "after")
+print(Memory.render_diff(Memory.diff(before, after, base=ctrl), s.symbols))
+```
+
+## 8. Running
+
+```python
+s.run(duration=1.0) # service events for one second
+s.run(max_stops=20) # or until 20 stops
+hits = s.step(10) # single-step and dispatch hooks
+s.stop() # interrupt and dispatch
+```
+
+`run` returns the number of stops serviced. Every stop is recorded.
+
+## 9. Declarative probes
+
+For simple instrumentation, ship a JSON spec instead of Python:
+
+```json
+{
+ "probes": [
+ {"name": "sum", "symbol": "rpc_sum", "when": "a1 > 0",
+ "on_enter": {"log": ["a0", "a1"],
+ "dump": [{"reg": "a0", "len": 32, "label": "sum-input"}],
+ "note": "sum called"},
+ "on_leave": {"log": ["retval"]}}
+ ]
+}
+```
+
+```python
+from greeneye.probes import load_spec, install
+install(s, load_spec("examples/probes/demo_probes.json"))
+s.run(duration=1.0)
+```
+
+Conditions accept register names (`a0..a7`, `pc`, `ra`, `sp`, `x0..x31`,
+`retval`), integer/hex literals and `and/or/not` with comparisons; arbitrary
+code is rejected. Actions: `log`, `dump` (save a blob), `snapshot`, `note`.
+
+## 10. Event stream and plugins
+
+```python
+s.on_event("watch_hit", lambda hit: print("wrote", hit.label()))
+s.note("starting phase 2")
+
+# In a plugin module:
+def register(ge):
+ ge.add_command("mycmd", lambda session, args: 0)
+```
+
+See [04-PLUGINS](04-PLUGINS.md).
+
+## 11. Recording and post-analysis from Python
+
+```python
+s.recorder.dir # session directory (or None)
+s.recorder.event("phase", name="p2")
+s.recorder.close() # writes session.json + summary.md
+
+from greeneye.analyze import render_summary, compare_sessions
+print(render_summary("dumps/run-a", symbols=s.symbols))
+print(compare_sessions("dumps/run-a", "dumps/run-b", symbols=s.symbols))
+```
+
+## 12. A complete worked script
+
+```python
+"""Drive the demo, capture a PING and a FAULT, and summarise."""
+import greeneye as ge
+
+s = ge.launch(kernel="firmware/uproc_demo/build/uproc_demo.elf", record=True, name="worked")
+ctrl = s.symbols.lookup("gsp_ctrl")
+hits = []
+try:
+ for name in ("rpc_ping", "rpc_sum", "rpc_fault"):
+ if s.symbols.get(name):
+ s.hook(name, on_enter=lambda h: hits.append(h.label()))
+ s.watch(ctrl + 0x10, size=8, kind="w")
+ s.run(duration=0.5) # self-test
+ s.memory.write_u64(ctrl + 0x20, 1) # PING
+ s.memory.write_u64(ctrl + 0x28, 0x1234)
+ s.run(duration=0.5)
+ s.memory.write_u64(ctrl + 0x20, 6) # FAULT -> ebreak
+ s.run(duration=1.0, max_stops=1)
+ print("pc:", hex(s.pc), s.symbols.annotate(s.pc))
+ s.step() # past the trap
+ print("hits:", hits)
+finally:
+ print("recording:", getattr(s.recorder, "dir", None))
+ s.close()
+```
+
+## 13. Errors and timeouts
+
+All green-eye errors derive from `greeneye.GreenEyeError`:
+`RspTimeout` (transport), `RspProtocolError`, `TargetError`,
+`SymbolNotFound`, `StructError`. `RspClient.timeout` (default 10 s) bounds a
+single packet read; `run`'s budget bounds the whole pass.