"""risc-v-gsp-rm-debugger command line interface. Commands: info environment/tooling check (incl. native simulator availability) demo boot the bundled uproc demo and run a canned instrumentation pass run run a Python instrumentation script against a target attach interactive REPL over a gdbstub or the native simulator launch start a target and keep it alive for a later attach probe run an arbitrary firmware (default: the real GSP fwimage) for N instructions and report the first divergence record boot a firmware and record a memory-activity session (native: --trace collects an exec/access trace blob) inject load and invoke an injected RV64 service (Frida-like) ghidra serve a live session to GhidraScripts over the JSON-lines bridge analyze render a recorded session to a symbol-annotated report compare A/B two recorded sessions at the symbolic level symbols build/inspect a risc-v-gsp-rm-debugger symbol database from Ghidra exports plugins list discoverable plugins """ from __future__ import annotations import argparse import cmd import os import sys import tempfile import time from pathlib import Path import greeneye as ge from . import analyze as _analyze from . import ghidra as _ghidra from .elf import load_elf from .errors import GreenEyeError from .plugins import PluginManager from .structs import schema_from_json from .symbols import SymbolDB from .target import QemuTarget TOOL_ROOT = Path(__file__).resolve().parent.parent DUMPS = TOOL_ROOT / "dumps" DEMO_ELF = TOOL_ROOT / "firmware" / "uproc_demo" / "build" / "uproc_demo.elf" DEMO_SCHEMA = TOOL_ROOT / "firmware" / "uproc_demo" / "gsp_ctrl.schema.json" DEFAULT_FWIMAGE = TOOL_ROOT.parents[1] / "firmware" / "57X-GSP" / "570.144" / "gsp-570.144.fwimage.bin" RPC_HANDLERS = ["rpc_ping", "rpc_echo", "rpc_fill", "rpc_sum", "rpc_state", "rpc_fault"] # --------------------------------------------------------------------------- # helpers # --------------------------------------------------------------------------- def _ensure_dumps_env() -> None: os.environ.setdefault("GREENEYE_DUMPS", str(DUMPS)) def _build_demo(quiet: bool = False) -> Path: import subprocess fw_dir = TOOL_ROOT / "firmware" / "uproc_demo" result = subprocess.run( ["make", "-C", str(fw_dir)], capture_output=quiet, text=True, ) if result.returncode != 0: output = (result.stdout or "") + (result.stderr or "") raise GreenEyeError(f"demo firmware build failed:\n{output}") if not DEMO_ELF.exists(): raise GreenEyeError(f"demo build produced no {DEMO_ELF}") return DEMO_ELF def _load_schema(path: Path): import json with open(path, "r", encoding="utf-8") as fh: return schema_from_json(json.load(fh)) def _start_session( *, kernel: str | None, port: int | None, record, name: str, plugins: list[str] | None = None, backend: str = "qemu", trace=None, ): if port: if backend != "qemu": raise GreenEyeError("--backend native cannot attach to an existing gdbstub port") if trace is not None: raise GreenEyeError("--trace is native-only; use the qemu/greeneye-trace.so plugin for QEMU") db = SymbolDB() if kernel: db.load_elf(kernel) session = ge.attach(port=port, record=record, symbols=db, name=name) else: session = ge.launch(kernel=kernel, record=record, name=name, backend=backend, trace=trace) manager = PluginManager(session) manager.load_all(plugins) return session, manager def _print_session_dir(session) -> None: directory = getattr(session.recorder, "dir", None) if directory: print(f"[risc-v-gsp-rm-debugger] session: {directory}") def _add_backend_arg(parser) -> None: """Select the execution backend; qemu stays the default reference path.""" parser.add_argument( "--backend", choices=("qemu", "native"), default="qemu", help="execution backend: qemu (default) or the native RV64IMAC simulator", ) def _add_trace_args(parser) -> None: """Native exec/access tracing (R4), mirroring qemu/greeneye_trace.c.""" parser.add_argument( "--trace", action="store_true", help="native backend: record an exec/access trace into the session", ) parser.add_argument( "--trace-exec", choices=("on", "off"), default="on", help="trace instruction exec events (default on; 'off' records accesses only)", ) parser.add_argument( "--trace-start", type=lambda v: int(v, 0), default=None, help="only trace PCs >= this address (inclusive)", ) parser.add_argument( "--trace-end", type=lambda v: int(v, 0), default=None, help="only trace PCs <= this address (inclusive)", ) parser.add_argument( "--trace-max", type=int, default=None, help="cap buffered trace events (0 = unlimited)", ) def _trace_spec(args): """Build a launch_native trace spec from parsed args (None when disabled).""" if not getattr(args, "trace", False): return None spec = {"exec": args.trace_exec == "on"} if args.trace_start is not None: spec["start"] = args.trace_start if args.trace_end is not None: spec["end"] = args.trace_end if args.trace_max is not None: spec["max_events"] = args.trace_max return spec # --------------------------------------------------------------------------- # commands # --------------------------------------------------------------------------- def cmd_info(args) -> int: from .target import DEFAULT_QEMU print(f"risc-v-gsp-rm-debugger {ge.__version__}") print(f"tool root: {TOOL_ROOT}") qemu = os.environ.get("GREENEYE_QEMU", DEFAULT_QEMU) import shutil resolved = shutil.which(qemu) if os.path.sep not in qemu else (qemu if os.path.exists(qemu) else None) print(f"qemu: {resolved or 'NOT FOUND (install qemu-system-misc)'}") if resolved: import subprocess version = subprocess.run([resolved, "--version"], capture_output=True, text=True).stdout.strip() print(f"qemu version: {version}") try: from .sim_backend import SIM_IPS # noqa: F401 print("native simulator: available (deterministic RV64IMAC, pure-Python reference)") print("native trace: available (--trace exec/access, --trace-start/--trace-end filters)") except Exception as exc: # pragma: no cover - only on a broken install print(f"native simulator: unavailable ({exc})") for label, path in [ ("demo firmware", DEMO_ELF), ("demo schema", DEMO_SCHEMA), ("default fwimage", DEFAULT_FWIMAGE), ]: print(f"{label}: {path} ({'present' if Path(path).exists() else 'missing'})") cuda_names = Path(TOOL_ROOT).parents[1] / "userspace" / "cuda" / "570.144" / "annotations" / "names.tsv" print(f"CUDA Ghidra names.tsv: {cuda_names} ({'present' if cuda_names.exists() else 'missing'})") plugins_dir = TOOL_ROOT / "plugins" plugins = sorted(p.name for p in plugins_dir.glob("*.py") if not p.name.startswith("_")) print(f"bundled plugins: {', '.join(plugins) or 'none'}") return 0 def cmd_demo(args) -> int: _ensure_dumps_env() elf = args.kernel or (_build_demo() if not args.no_build else DEMO_ELF) if not Path(elf).exists(): raise GreenEyeError(f"demo firmware not built: {elf}") schema = _load_schema(DEMO_SCHEMA) record = True if args.record is None else args.record session, _manager = _start_session( kernel=str(elf), port=None, record=record, name="demo", backend=args.backend, trace=_trace_spec(args), ) hits: list[str] = [] try: ctrl = session.symbols.lookup("gsp_ctrl") for name in RPC_HANDLERS: sym = session.symbols.get(name) if sym is not None: session.hook( sym.addr, on_enter=lambda hit: hits.append(f"enter {hit.label()} a0={hit.arg(0):#x}"), on_leave=lambda hit: hits.append(f"leave {hit.label()} -> {hit.retval:#x}"), ) session.watch(ctrl + 16, size=8, kind="w", listener="rpc_count") print(f"[demo] ctrl={ctrl:#x} schema={schema.name} size={schema.size:#x}") print("[demo] running the firmware self-test ...") session.run(duration=args.duration) banner = session.target.read_uart() if session.target else "" print("[demo] uart:", banner.strip().splitlines()[0] if banner.strip() else "") before = session.snapshot(ctrl, schema.size, "ctrl-after-selftest") print("[demo] host stimulus: PING") session.memory.write_u64(ctrl + 32, 1) session.memory.write_u64(ctrl + 40, 0x1234) session.run(duration=0.5) after = session.snapshot(ctrl, schema.size, "ctrl-after-ping") values = schema.decode(after, base=ctrl) print("[demo] control block after stimulus:") from .structs import render print(render(schema, values)) diffs = ge.Memory.diff(before, after, base=ctrl) if diffs: print("[demo] snapshot diff (self-test -> ping):") print(ge.Memory.render_diff(diffs, session.symbols, max_lines=16)) print("[demo] host stimulus: FAULT (ebreak) to demonstrate trap capture") session.memory.write_u64(ctrl + 32, 6) session.run(duration=1.0, max_stops=1) pc = session.pc sym = session.symbols.resolve(pc) print(f"[demo] stopped at {pc:#x} {sym.name if sym else ''} (ebreak)") session.step() # step past the trap print("[demo] stepped past the trap; hits:") for line in hits[:24]: print(" ", line) print(f"[demo] total hook lines: {len(hits)}") _print_session_dir(session) finally: session.close() return 0 def cmd_run(args) -> int: _ensure_dumps_env() script = Path(args.script) if not script.exists(): raise GreenEyeError(f"script not found: {script}") kernel = args.kernel if not kernel and not args.port: kernel = str(_build_demo()) session, manager = _start_session( kernel=kernel, port=args.port, record=args.record or True, name=script.stem, plugins=args.plugin, backend=args.backend, trace=_trace_spec(args), ) namespace = { "ge": ge, "session": session, "s": session, "memory": session.memory, "symbols": session.symbols, "recorder": session.recorder, "greeneye": ge, "__file__": str(script), "__name__": "__main__", } try: source = script.read_text(encoding="utf-8") exec(compile(source, str(script), "exec"), namespace) return 0 except SystemExit as exc: return int(exc.code or 0) finally: session.close() # -- interactive REPL ------------------------------------------------------- class GreenEyeRepl(cmd.Cmd): intro = "risc-v-gsp-rm-debugger interactive instrumentation. Type 'help' for commands." prompt = "risc-v-gsp-rm-debugger> " def __init__(self, session, manager): super().__init__() self.session = session self.manager = manager self._listener_hooks = [] # utility def _print_hit(self, hit): print(" " + str(hit)) def _say(self, text): print(text) # commands def do_regs(self, arg): """regs -- dump all registers.""" regs = self.session.read_regs() for name, value in regs.items(): print(f" {name:<5} = {value:#018x}") def do_reg(self, arg): """reg [value] -- read or write one register.""" parts = arg.split() if not parts: return self._say("usage: reg [value]") if len(parts) == 1: print(f" {parts[0]} = {self.session.client.read_reg(parts[0]):#x}") else: self.session.client.write_reg(parts[0], int(parts[1], 0)) self._say("ok") def do_x(self, arg): """x [len] -- hexdump memory (annotated).""" parts = arg.split() if not parts: return self._say("usage: x [len]") addr = self.session.resolve(parts[0]) length = int(parts[1], 0) if len(parts) > 1 else 64 print(self.session.memory.hexdump(addr, length)) def do_mem(self, arg): """mem -- raw memory read.""" addr, length = arg.split() print(self.session.memory.read(self.session.resolve(addr), int(length, 0)).hex()) def do_w(self, arg): """w -- write memory.""" addr, data = arg.split() self.session.memory.write(self.session.resolve(addr), bytes.fromhex(data)) self._say("ok") def do_wstr(self, arg): """wstr -- write a string plus NUL.""" addr, _, text = arg.partition(" ") self.session.memory.write(self.session.resolve(addr), text.encode() + b"\x00") self._say("ok") def do_bp(self, arg): """bp [sw|hw] -- set a breakpoint that prints on hit.""" parts = arg.split() if not parts: return self._say("usage: bp [sw|hw]") kind = parts[1] if len(parts) > 1 else "sw" handle = self.session.hook(parts[0], on_enter=self._print_hit, kind=kind, listener="repl") self._listener_hooks.append(handle) self._say(f"breakpoint @ {handle.addr:#x} ({handle.label})") def do_hook(self, arg): """hook -- alias for bp with enter/leave printing.""" parts = arg.split() if not parts: return self._say("usage: hook ") handle = self.session.hook( parts[0], on_enter=self._print_hit, on_leave=self._print_hit, listener="repl" ) self._listener_hooks.append(handle) self._say(f"hooked {handle.label}") def do_watch(self, arg): """watch [size] [w|r|a] -- set a watchpoint.""" parts = arg.split() if not parts: return self._say("usage: watch [size] [w|r|a]") size = int(parts[1], 0) if len(parts) > 1 else 8 kind = parts[2] if len(parts) > 2 else "w" wp = self.session.watch(parts[0], size=size, kind=kind, callback=self._print_hit, listener="repl") self._say(f"watch {kind}@{wp.addr:#x} size={size}") def do_s(self, arg): """s [n] -- step n instructions (default 1).""" count = int(arg) if arg.strip() else 1 for hit in self.session.step(count): self._print_hit(hit) def do_c(self, arg): """c [seconds] -- continue and service events for N seconds (default 1).""" seconds = float(arg) if arg.strip() else 1.0 stops = self.session.run(duration=seconds) self._say(f"resumed for {seconds}s, serviced {stops} stops") def do_call(self, arg): """call [a0 [a1 ...]] -- remote function call; prints the result.""" parts = arg.split() if not parts: return self._say("usage: call [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 directory = getattr(self.session.recorder, "dir", None) if not directory: return self._say("this session is not recording") events = load_events(directory) for event in events[-int(arg or 20) :]: print(" " + str(event)) def do_snap(self, arg): """snap