summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/greeneye/cli.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 09:13:00 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 09:13:00 -0400
commit1e701396b1b6d204dfd8e5a8ddcd3007038029ac (patch)
tree8dde895cf9d3e66ce912d01cb1a72ff674dc850f /debug/risc-v-gsp-rm-vm/greeneye/cli.py
parentab48e44832db55fdedadcf44f55fa9904ca36f2f (diff)
downloadopen-iov-1e701396b1b6d204dfd8e5a8ddcd3007038029ac.tar.gz
open-iov-1e701396b1b6d204dfd8e5a8ddcd3007038029ac.zip
debug: green-eye wave 2 - injection SDK, Frida-like calls, Ghidra control bridge, long-term docs
Adds to debug/risc-v-gsp-rm-vm/: - Session.call / run_until: Frida-like remote function calls and run-to-cursor, with register restoration; verified live (rpc_ping -> 0xaffe). - symbol+offset target resolution (gsp_ctrl+0x10) across all APIs and the REPL. - Injection SDK (sdk/) and 'greeneye inject': position-independent RV64 service blobs (verified zero relocations), loaded into RAM, registered in the demo handler table, and invoked either directly or through the firmware mailbox dispatcher. Demo ABI extended: mailbox 16..23 selects slots, arg0 carries the service command, arg2 added (struct now 0x190; schema updated). - Ghidra control bridge (greeneye ghidra + GreenEyeControl.java): run-until-here, breakpoints, memory/register access, remote calls, hit export; verified end-to-end with Ghidra headless (run_until reached rpc_sum from a GhidraScript). - MarkGreenEyeHits.java fixed for the Ghidra 12 createBookmark API; verified headless together with ExportForGreenEye.java. - Analyzer renders a QEMU first-fault table for probe sessions (fault count, epc with firmware vaddr translation, access address/size, reason). - Tests now 35 green, including injection, Ghidra bridge, remote call, run-until and REPL command smoke tests. - Documentation: docs/11-16 (GSP RM service developer guide, injection SDK specification, Ghidra control, adversarial interoperability guide, agentic GSP IDE, custom simulator design) and GREEN-EYE-ROADMAP.md (long-term H1-H7 program); doc maps and the project RISC-V VM doc updated.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/cli.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/cli.py96
1 files changed, 96 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/cli.py b/debug/risc-v-gsp-rm-vm/greeneye/cli.py
index 5bfcee2..94102a4 100644
--- a/debug/risc-v-gsp-rm-vm/greeneye/cli.py
+++ b/debug/risc-v-gsp-rm-vm/greeneye/cli.py
@@ -332,6 +332,15 @@ class GreenEyeRepl(cmd.Cmd):
stops = self.session.run(duration=seconds)
self._say(f"resumed for {seconds}s, serviced {stops} stops")
+ def do_call(self, arg):
+ """call <addr|symbol> [a0 [a1 ...]] -- remote function call; prints the result."""
+ parts = arg.split()
+ if not parts:
+ return self._say("usage: call <addr|symbol> [args...]")
+ values = [int(part, 0) for part in parts[1:]]
+ retval = self.session.call(parts[0], values)
+ self._say(f" -> {retval:#x}")
+
def do_events(self, arg):
"""events [n] -- show the last n recorded events (default 20)."""
from .record import load_events
@@ -512,6 +521,12 @@ def cmd_probe(args) -> int:
print(f"[probe] total exceptions: {len(faults)}")
else:
print("[probe] no exception recorded (execution stayed in mapped memory)")
+ recorder = session.recorder
+ if hasattr(recorder, "meta"):
+ recorder.meta["qemu_fault_count"] = len(faults)
+ recorder.meta["qemu_first_fault"] = faults[0] if faults else None
+ if qlog:
+ recorder.meta["qemu_log_blob"] = recorder.blob("qemu.log", qlog.encode("utf-8"))
tail = qlog.strip().splitlines()
if tail:
print("[probe] qemu log tail:")
@@ -523,6 +538,64 @@ def cmd_probe(args) -> int:
return 0
+def cmd_inject(args) -> int:
+ _ensure_dumps_env()
+ from .inject import invoke_remote, invoke_via_firmware, load_service
+ from .target import free_port # noqa: F401 (kept for symmetry with launch)
+
+ service_path = Path(args.service)
+ if not service_path.exists():
+ raise GreenEyeError(
+ f"service not found: {service_path} (build it with `make sdk`)"
+ )
+ kernel = args.kernel or str(_build_demo())
+ session, _manager = _start_session(
+ kernel=str(kernel), port=args.port, record=args.record or True, name="inject", plugins=args.plugin
+ )
+ try:
+ service = load_service(session, str(service_path), load_addr=args.load_addr, slot=args.slot)
+ print(f"[inject] {service.summary()}")
+ remote = invoke_remote(session, service, args.cmd, args.arg0, args.arg1)
+ print(f"[inject] direct call dispatch(cmd={args.cmd}, a0={args.arg0:#x}, a1={args.arg1:#x}) -> {remote:#x}")
+ if not args.no_firmware:
+ session.run(duration=args.duration)
+ fw = invoke_via_firmware(session, service, args.cmd, args.arg0, args.arg1)
+ print(f"[inject] firmware call mailbox={16 + service.slot} -> result {fw:#x}")
+ _print_session_dir(session)
+ finally:
+ session.close()
+ return 0
+
+
+def cmd_ghidra(args) -> int:
+ """Serve a live session to GhidraScripts over the JSON-lines bridge."""
+ _ensure_dumps_env()
+ from .gdbridge import GhidraBridge
+
+ kernel = args.kernel
+ if kernel:
+ kernel = str(Path(kernel))
+ elif not args.target_port:
+ kernel = str(_build_demo())
+ session, _manager = _start_session(
+ kernel=kernel, port=args.target_port, record=args.record or True, name="ghidra", plugins=args.plugin
+ )
+ bridge = GhidraBridge(session, port=args.bridge_port)
+ port = bridge.start()
+ print(f"[green-eye] ghidra bridge on 127.0.0.1:{port}")
+ print(f"[green-eye] session: {getattr(session.recorder, 'dir', None)}")
+ print("[green-eye] in Ghidra: analyzeHeadless ... -postScript GreenEyeControl.java " + str(port))
+ print("[green-eye] Ctrl-C to stop the bridge")
+ try:
+ bridge.serve_forever()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ bridge.stop()
+ session.close()
+ return 0
+
+
def cmd_analyze(args) -> int:
symbols = None
if args.symbols:
@@ -627,6 +700,21 @@ def build_parser() -> argparse.ArgumentParser:
probe.add_argument("--record", nargs="?", const=True, default=None)
probe.set_defaults(fn=cmd_probe)
+ inject = sub.add_parser("inject", help="load and invoke an injected RV64 service (Frida-like)")
+ inject.add_argument("--service", default=str(TOOL_ROOT / "sdk" / "build" / "injected_demo.elf"))
+ inject.add_argument("--kernel")
+ inject.add_argument("--port", type=int)
+ inject.add_argument("--load-addr", type=lambda v: int(v, 0))
+ inject.add_argument("--slot", type=int, default=6, help="demo handler slot 0..7 (default 6)")
+ inject.add_argument("--cmd", type=lambda v: int(v, 0), default=1)
+ inject.add_argument("--arg0", type=lambda v: int(v, 0), default=0x41)
+ inject.add_argument("--arg1", type=lambda v: int(v, 0), default=0x42)
+ inject.add_argument("--duration", type=float, default=0.3, help="firmware settle time before the mailbox call")
+ inject.add_argument("--no-firmware", action="store_true", help="only make the direct debugger-side call")
+ inject.add_argument("--record", nargs="?", const=True, default=None)
+ inject.add_argument("--plugin", action="append", default=[])
+ inject.set_defaults(fn=cmd_inject)
+
record = sub.add_parser("record", help="boot and record a memory-activity session")
record.add_argument("--kernel")
record.add_argument("--name", default="record")
@@ -636,6 +724,14 @@ def build_parser() -> argparse.ArgumentParser:
record.add_argument("--record", nargs="?", const=True, default=None)
record.set_defaults(fn=cmd_record)
+ ghidra = sub.add_parser("ghidra", help="serve a live session to GhidraScripts (JSON-lines bridge)")
+ ghidra.add_argument("--kernel")
+ ghidra.add_argument("--target-port", type=int, help="attach to an existing gdbstub instead of launching")
+ ghidra.add_argument("--bridge-port", type=int, default=7000)
+ ghidra.add_argument("--record", nargs="?", const=True, default=None)
+ ghidra.add_argument("--plugin", action="append", default=[])
+ ghidra.set_defaults(fn=cmd_ghidra)
+
analyze = sub.add_parser("analyze", help="render a recorded session report")
analyze.add_argument("session")
analyze.add_argument("--symbols")