From ba58dcdc5ad6ca021c723d279f33e3f14e1184df Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Fri, 2 Oct 2026 08:36:35 -0400 Subject: 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. --- debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md | 219 ++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md (limited to 'debug/risc-v-gsp-rm-vm/docs/03-SCRIPTING.md') 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 ` (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. -- cgit v1.2.3