summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/greeneye
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye')
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/analyze.py28
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/cli.py96
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py220
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/inject.py129
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/record.py2
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/session.py113
-rw-r--r--debug/risc-v-gsp-rm-vm/greeneye/target.py2
7 files changed, 589 insertions, 1 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/analyze.py b/debug/risc-v-gsp-rm-vm/greeneye/analyze.py
index 07c24eb..66429e9 100644
--- a/debug/risc-v-gsp-rm-vm/greeneye/analyze.py
+++ b/debug/risc-v-gsp-rm-vm/greeneye/analyze.py
@@ -110,6 +110,34 @@ def render_summary_from(meta: dict, events: list[dict], symbols=None, max_rows:
lines.append("* recorder counts: " + ", ".join(f"{k}={v}" for k, v in sorted(counts_meta.items())))
lines.append("")
+ # QEMU exception report (probe sessions)
+ fault_count = meta.get("qemu_fault_count")
+ first = meta.get("qemu_first_fault")
+ if fault_count or first:
+ lines.append("## QEMU exceptions")
+ lines.append("")
+ lines.append(f"* total exceptions: {fault_count if fault_count is not None else '?'}")
+ if meta.get("qemu_log_blob"):
+ lines.append(f"* qemu log blob: `{meta['qemu_log_blob']}`")
+ if first:
+ access = first.get("access") or {}
+ lines.append("")
+ lines.append("| field | value |")
+ lines.append("|---|---|")
+ lines.append(f"| descriptor | `{first.get('desc', '?')}` |")
+ lines.append(f"| cause | `{first.get('cause', 0):#x}` ({_RISCV_CAUSES.get(first.get('cause', 0) & 0xFF, '?')}) |")
+ epc = first.get("epc", 0)
+ bias = meta.get("bias") or 0
+ epc_cell = f"`{epc:#x}`" + (f" (firmware vaddr `{epc - bias:#x}`)" if bias else "")
+ lines.append(f"| epc | {epc_cell} |")
+ lines.append(f"| tval | `{first.get('tval', 0):#x}` |")
+ if access:
+ lines.append(
+ f"| first access | `{access.get('rw')}` at `{access.get('access_addr', 0):#x}` "
+ f"({access.get('size')} bytes, reason `{access.get('reason')}`) |"
+ )
+ lines.append("")
+
def annotate(addr):
if not isinstance(addr, int):
return ""
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")
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py b/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py
new file mode 100644
index 0000000..cc06e43
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py
@@ -0,0 +1,220 @@
+"""Ghidra control bridge: let GhidraScripts drive a green-eye session.
+
+A tiny JSON-lines server over localhost. Each request is one line::
+
+ {"id": 1, "cmd": "run_until", "args": {"target": "0x800000ce", "timeout": 5}}
+
+and each response is one line::
+
+ {"id": 1, "ok": true, "result": {"addr": 2147483854}}
+
+This is the transport behind ``ghidra/GreenEyeControl.java``: a Ghidra user
+can "execute until here", set/clear breakpoints, read memory and export hit
+lists without leaving the decompiler. See ``docs/13-GHIDRA-CONTROL.md``.
+"""
+from __future__ import annotations
+
+import json
+import socket
+import threading
+
+from .errors import GreenEyeError
+
+
+class GhidraBridge:
+ def __init__(self, session, host: str = "127.0.0.1", port: int = 0):
+ self.session = session
+ self.host = host
+ self.port = port
+ self._server: socket.socket | None = None
+ self._thread: threading.Thread | None = None
+ self._clients: list[threading.Thread] = []
+ self._stop = threading.Event()
+
+ # -- lifecycle ---------------------------------------------------------
+
+ def start(self) -> int:
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ server.bind((self.host, self.port))
+ server.listen(4)
+ self.port = server.getsockname()[1]
+ self._server = server
+ self._thread = threading.Thread(target=self._accept_loop, name="greeneye-ghidra", daemon=True)
+ self._thread.start()
+ return self.port
+
+ def stop(self) -> None:
+ self._stop.set()
+ if self._server is not None:
+ try:
+ self._server.close()
+ except OSError:
+ pass
+ self._server = None
+
+ def serve_forever(self) -> None:
+ if self._thread is None:
+ self.start()
+ try:
+ while not self._stop.is_set() and (self._thread is not None and self._thread.is_alive()):
+ self._thread.join(timeout=0.5)
+ except KeyboardInterrupt:
+ pass
+ finally:
+ self.stop()
+
+ def _accept_loop(self) -> None:
+ assert self._server is not None
+ while not self._stop.is_set():
+ try:
+ client, _addr = self._server.accept()
+ except OSError:
+ return
+ handler = threading.Thread(target=self._client_loop, args=(client,), daemon=True)
+ handler.start()
+ self._clients.append(handler)
+
+ def _client_loop(self, client: socket.socket) -> None:
+ with client:
+ stream = client.makefile("rwb")
+ for line in stream:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ request = json.loads(line)
+ except json.JSONDecodeError as exc:
+ response = {"id": None, "ok": False, "error": f"bad json: {exc}"}
+ else:
+ response = self._dispatch(request)
+ stream.write((json.dumps(response) + "\n").encode("utf-8"))
+ stream.flush()
+ if response.get("close"):
+ self.stop()
+ return
+
+ # -- command dispatch --------------------------------------------------
+
+ def _dispatch(self, request: dict) -> dict:
+ request_id = request.get("id")
+ cmd = request.get("cmd", "")
+ args = request.get("args") or {}
+ try:
+ handler = getattr(self, f"cmd_{cmd}", None)
+ if handler is None:
+ raise GreenEyeError(f"unknown bridge command {cmd!r}")
+ result = handler(**args)
+ response = {"id": request_id, "ok": True, "result": result}
+ if cmd == "close":
+ response["close"] = True
+ return response
+ except Exception as exc: # never kill the bridge on a bad request
+ return {"id": request_id, "ok": False, "error": str(exc)}
+
+ # -- commands ----------------------------------------------------------
+
+ def cmd_ping(self) -> str:
+ return "pong"
+
+ def cmd_info(self) -> dict:
+ return {
+ "name": self.session.name,
+ "pc": self.session.pc,
+ "symbols": len(self.session.symbols),
+ "recording": getattr(self.session.recorder, "dir", None),
+ }
+
+ def cmd_resolve(self, name: str) -> int:
+ return self.session.resolve(name)
+
+ def cmd_bp_set(self, target, kind: str = "sw") -> int:
+ handle = self.session.hook(target, kind=kind, listener="ghidra")
+ return handle.addr
+
+ def cmd_bp_clear(self, addr) -> bool:
+ addr = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ for _key, bp in list(self.session._bps.items()): # noqa: SLF001 - bridge is a privileged client
+ if bp.addr == addr:
+ self.session.remove_breakpoint(bp)
+ return True
+
+ def cmd_bp_list(self) -> list[dict]:
+ return [
+ {"addr": bp.addr, "label": bp.label, "hits": bp.hits, "kind": bp.kind}
+ for bp in self.session._bps.values() # noqa: SLF001
+ ]
+
+ def cmd_watch_set(self, target, size: int = 8, kind: str = "w") -> int:
+ wp = self.session.watch(target, size=size, kind=kind, listener="ghidra")
+ return wp.addr
+
+ def cmd_run_until(self, target, timeout: float = 5.0) -> dict:
+ hit = self.session.run_until(target, timeout=timeout)
+ return {"addr": hit.addr, "symbol": hit.symbol.name if hit.symbol else None, "kind": hit.kind}
+
+ def cmd_run(self, duration: float = 1.0, max_stops: int | None = None) -> int:
+ return self.session.run(duration=duration, max_stops=max_stops)
+
+ def cmd_step(self, count: int = 1) -> list[int]:
+ return [hit.addr for hit in self.session.step(count)]
+
+ def cmd_read_mem(self, addr, size: int = 64) -> str:
+ real = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ return self.session.memory.read(real, size).hex()
+
+ def cmd_write_mem(self, addr, hex: str) -> bool:
+ real = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ self.session.memory.write(real, bytes.fromhex(hex))
+ return True
+
+ def cmd_read_reg(self, name: str) -> int:
+ return self.session.client.read_reg(name)
+
+ def cmd_write_reg(self, name: str, value) -> bool:
+ self.session.client.write_reg(name, int(value))
+ return True
+
+ def cmd_call(self, target, args: list | None = None, timeout: float = 5.0) -> int:
+ return self.session.call(target, tuple(args or ()), timeout=timeout)
+
+ def cmd_snapshot(self, addr, size: int, label: str = "ghidra") -> str:
+ real = self.session.resolve(addr) if isinstance(addr, str) else int(addr)
+ self.session.memory.snapshot(real, size, name=label)
+ return label
+
+ def cmd_note(self, text: str) -> bool:
+ self.session.note(text)
+ return True
+
+ def cmd_hits(self) -> list[dict]:
+ from .record import load_events
+
+ directory = getattr(self.session.recorder, "dir", None)
+ if not directory:
+ return []
+ counts: dict[int, int] = {}
+ for event in load_events(directory):
+ if event.get("type") == "bp_hit":
+ addr = event.get("pc")
+ if isinstance(addr, int):
+ counts[addr] = counts.get(addr, 0) + 1
+ return [
+ {
+ "addr": addr,
+ "hits": hits,
+ "symbol": (self.session.symbols.resolve(addr).name if self.session.symbols.resolve(addr) else None),
+ }
+ for addr, hits in sorted(counts.items(), key=lambda item: -item[1])
+ ]
+
+ def cmd_export_hits(self, path: str) -> str:
+ from .ghidra import export_hits
+
+ directory = getattr(self.session.recorder, "dir", None)
+ if not directory:
+ raise GreenEyeError("session is not recording; hits cannot be exported")
+ return export_hits(directory, path, symbols=self.session.symbols)
+
+ def cmd_close(self) -> bool:
+ return True
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/inject.py b/debug/risc-v-gsp-rm-vm/greeneye/inject.py
new file mode 100644
index 0000000..f9605fc
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/greeneye/inject.py
@@ -0,0 +1,129 @@
+"""Frida-like service injection for green-eye.
+
+Loads a position-independent RV64 service blob into a live target, registers
+its dispatch entry with the firmware (on the demo uproc: a handler-table slot
+that commands 16..23 dispatch to), and invokes it either directly through
+``Session.call`` (a debugger-side remote function call) or through the
+firmware's own mailbox dispatcher.
+
+On a future GSP-RM simulator the same shape becomes "inject an RM engine and
+register it with the simulated RM core"; see ``docs/12-INJECTION-SDK.md`` and
+``GREEN-EYE-ROADMAP.md``.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from .elf import load_elf
+from .errors import GreenEyeError
+
+#: Symbol the injector calls in a service blob.
+ENTRY_SYMBOL = "ge_demo_service_dispatch"
+
+#: Demo uproc control-block field offsets (see firmware/uproc_demo).
+CTRL_HANDLERS = 0x150
+CTRL_MAILBOX = 0x20
+CTRL_ARG0 = 0x28
+CTRL_ARG1 = 0x30
+CTRL_ARG2 = 0x38
+CTRL_RESULT = 0x40
+INJECT_CMD_BASE = 16
+
+
+@dataclass
+class InjectedService:
+ path: str
+ name: str
+ load_addr: int
+ entry_addr: int
+ dispatch_offset: int
+ slot: int
+ size: int
+
+ def summary(self) -> str:
+ return (
+ f"{self.name}: {self.size} bytes at {self.load_addr:#x}, "
+ f"entry {self.entry_addr:#x} (offset {self.dispatch_offset:#x}), slot {self.slot}"
+ )
+
+
+def load_service(
+ session,
+ service_elf: str,
+ *,
+ load_addr: int | None = None,
+ slot: int = 6,
+ entry_symbol: str = ENTRY_SYMBOL,
+) -> InjectedService:
+ """Load a service blob and register it with the demo firmware."""
+ image = load_elf(service_elf)
+ entry = None
+ for symbol in image.symbols:
+ if symbol.name == entry_symbol:
+ entry = symbol
+ break
+ if entry is None:
+ available = ", ".join(sorted(s.name for s in image.symbols if s.is_function)) or "<none>"
+ raise GreenEyeError(f"service {service_elf}: symbol {entry_symbol!r} not found (functions: {available})")
+ if not 0 <= slot <= 7:
+ raise GreenEyeError(f"slot must be 0..7 (got {slot})")
+ if load_addr is None:
+ base = getattr(session.target, "ram_base", 0x80000000) if session.target else 0x80000000
+ size = getattr(session.target, "memory_mb", 256) * 1024 * 1024 if session.target else 256 * 1024 * 1024
+ load_addr = base + 0x01000000
+ if load_addr + image.max_vaddr + 0x1000 >= base + size:
+ load_addr = base + max(0x1000, size - image.max_vaddr - 0x2000)
+ if getattr(session.target, "memory_mb", 0):
+ ram_end = getattr(session.target, "ram_base", 0x80000000) + session.target.memory_mb * 1024 * 1024
+ if load_addr + image.max_vaddr > ram_end:
+ raise GreenEyeError(
+ f"service does not fit: load {load_addr:#x} + {image.max_vaddr:#x} exceeds RAM end {ram_end:#x}"
+ )
+
+ total = 0
+ for segment in image.load_segments:
+ data = image.segment_bytes(segment)
+ if data:
+ session.memory.write(load_addr + segment.p_vaddr, data)
+ total += len(data)
+ entry_addr = load_addr + entry.value
+
+ ctrl = session.symbols.get("gsp_ctrl")
+ if ctrl is not None:
+ session.memory.write_u64(ctrl.addr + CTRL_HANDLERS + slot * 8, entry_addr)
+
+ service = InjectedService(
+ path=service_elf,
+ name=service_elf.rsplit("/", 1)[-1],
+ load_addr=load_addr,
+ entry_addr=entry_addr,
+ dispatch_offset=entry.value,
+ slot=slot,
+ size=total,
+ )
+ session.recorder.event(
+ "service_injected",
+ path=service.path,
+ name=service.name,
+ load_addr=load_addr,
+ entry_addr=entry_addr,
+ slot=slot,
+ size=total,
+ )
+ return service
+
+
+def invoke_remote(session, service: InjectedService, cmd: int, a0: int = 0, a1: int = 0, timeout: float = 3.0) -> int:
+ """Call the service directly from the debugger (Frida NativeFunction)."""
+ return session.call(service.entry_addr, (cmd, a0, a1), timeout=timeout)
+
+
+def invoke_via_firmware(session, service: InjectedService, cmd: int, a0: int = 0, a1: int = 0, settle: float = 0.25) -> int:
+ """Invoke the service through the firmware's mailbox dispatcher."""
+ ctrl = session.symbols.lookup("gsp_ctrl")
+ session.memory.write_u64(ctrl + CTRL_ARG0, cmd)
+ session.memory.write_u64(ctrl + CTRL_ARG1, a0)
+ session.memory.write_u64(ctrl + CTRL_ARG2, a1)
+ session.memory.write_u64(ctrl + CTRL_MAILBOX, INJECT_CMD_BASE + service.slot)
+ session.run(duration=settle)
+ return session.memory.read_u64(ctrl + CTRL_RESULT)
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/record.py b/debug/risc-v-gsp-rm-vm/greeneye/record.py
index bf70e58..0b025b5 100644
--- a/debug/risc-v-gsp-rm-vm/greeneye/record.py
+++ b/debug/risc-v-gsp-rm-vm/greeneye/record.py
@@ -40,7 +40,7 @@ class Recorder:
self.meta = dict(meta or {})
self.meta.update({"format": FORMAT, "name": name, "created": time.time(), "greeneye": _version()})
self._events_path = os.path.join(self.dir, "events.jsonl")
- self._fh = open(self._events_path, "w", encoding="utf-8")
+ self._fh = open(self._events_path, "w", encoding="utf-8", buffering=1)
self._t0 = time.monotonic()
self._counts: dict[str, int] = {}
self._snapshots: list[dict] = []
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/session.py b/debug/risc-v-gsp-rm-vm/greeneye/session.py
index 7ad6444..fa54f77 100644
--- a/debug/risc-v-gsp-rm-vm/greeneye/session.py
+++ b/debug/risc-v-gsp-rm-vm/greeneye/session.py
@@ -6,6 +6,7 @@ plugin-supplied backend later.
"""
from __future__ import annotations
+import re
import time
from dataclasses import dataclass, field as _field
from typing import Callable
@@ -16,6 +17,15 @@ from .record import NullRecorder, Recorder
from .rsp import RspClient, StopReply
from .symbols import Symbol, SymbolDB
+#: Registers saved and restored around ``Session.call`` (RISC-V ABI + pc).
+RESTORE_REGS = (
+ ["ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1"]
+ + [f"a{i}" for i in range(8)]
+ + [f"s{i}" for i in range(2, 12)]
+ + [f"t{i}" for i in range(3, 7)]
+ + ["pc"]
+)
+
@dataclass
class Hit:
@@ -195,11 +205,21 @@ class Session:
# -- symbol helpers ----------------------------------------------------
def resolve(self, target: int | str) -> int:
+ """Resolve an address, a symbol name, or a ``symbol+0xNN`` expression."""
if isinstance(target, int):
return target
if isinstance(target, str):
if target.lower().startswith("0x"):
return int(target, 16)
+ expression = re.fullmatch(
+ r"([A-Za-z_][A-Za-z0-9_.]*)\s*([+-])\s*(0x[0-9a-fA-F]+|\d+)", target.strip()
+ )
+ if expression:
+ base = self.symbols.get(expression.group(1))
+ if base is None:
+ raise SymbolNotFound(f"symbol {expression.group(1)!r} not found in symbol DB")
+ delta = int(expression.group(3), 0)
+ return base.addr + delta if expression.group(2) == "+" else base.addr - delta
sym = self.symbols.get(target)
if sym is None:
raise SymbolNotFound(f"symbol {target!r} not found in symbol DB")
@@ -338,6 +358,99 @@ class Session:
break
return hits
+ # -- Frida-like remote call / run-until -------------------------------
+
+ def _scratch_addr(self, override: int | None = None) -> int:
+ if override:
+ return override
+ if self.target is not None and getattr(self.target, "memory_mb", 0):
+ base = getattr(self.target, "ram_base", 0x80000000)
+ return base + self.target.memory_mb * 1024 * 1024 - 0x1000
+ if self.image is not None and getattr(self.image, "max_vaddr", 0):
+ return self.image.max_vaddr + 0x10000
+ return 0x801FF000
+
+ def run_until(self, target: int | str, timeout: float = 5.0) -> Hit:
+ """Run until execution reaches ``target`` (Frida's run-to-cursor).
+
+ Sets a temporary breakpoint, services intervening stops, and returns
+ the Hit for the target. Raises ``RspTimeout`` if not reached.
+ """
+ addr = self.resolve(target)
+ if self.pc == addr:
+ return Hit(kind="stop", addr=addr, regs=self.read_regs(), symbol=self.symbols.resolve(addr), session=self)
+ temp = Breakpoint(addr=addr, kind="sw", temporary=True, on_enter=None)
+ self.add_breakpoint(temp)
+ deadline = time.monotonic() + timeout
+ try:
+ while True:
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise RspTimeout(f"run_until {addr:#x}: not reached in {timeout}s")
+ try:
+ stop = self.client.continue_(timeout=remaining)
+ except RspTimeout:
+ self.client.interrupt()
+ raise RspTimeout(f"run_until {addr:#x}: not reached in {timeout}s")
+ hit = self._dispatch_stop(stop)
+ if hit is not None and hit.addr == addr and hit.kind == "breakpoint":
+ return hit
+ if stop.is_exit:
+ raise GreenEyeError(f"run_until {addr:#x}: target exited")
+ finally:
+ self.remove_breakpoint(temp)
+
+ def call(self, target: int | str, args: tuple | list = (), timeout: float = 5.0, scratch: int | None = None) -> int:
+ """Call a function in the target from the debugger (Frida NativeFunction).
+
+ Writes an ``ebreak`` trampoline into a scratch page, sets ``ra`` to it,
+ loads ``a0..a7`` and a private stack, jumps to ``target``, services
+ intervening stops (hooks/watchpoints included), and returns ``a0``.
+ All caller-saved registers and ``pc`` are restored afterwards.
+ """
+ addr = self.resolve(target)
+ scratch = self._scratch_addr(scratch)
+ stack = scratch + 0x800
+ saved = self.read_regs()
+ temp = Breakpoint(addr=scratch, kind="sw", temporary=True, purpose="user")
+ self.add_breakpoint(temp)
+ try:
+ for name in ("a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"):
+ self.client.write_reg(name, 0)
+ for index, value in enumerate(args[:8]):
+ self.client.write_reg(f"a{index}", int(value) & 0xFFFF_FFFF_FFFF_FFFF)
+ self.client.write_reg("ra", scratch)
+ self.client.write_reg("sp", stack)
+ self.client.write_reg("pc", addr)
+ self.recorder.event("call_enter", addr=addr, args=list(args), scratch=scratch)
+ deadline = time.monotonic() + timeout
+ while True:
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise RspTimeout(f"call {addr:#x}: no return in {timeout}s")
+ try:
+ stop = self.client.continue_(timeout=remaining)
+ except RspTimeout:
+ self.client.interrupt()
+ raise RspTimeout(f"call {addr:#x}: no return in {timeout}s")
+ hit = self._dispatch_stop(stop)
+ pc = self.client.pc
+ if pc == scratch or (hit is not None and hit.addr == scratch):
+ retval = self.client.read_reg("a0")
+ self.recorder.event("call_return", addr=addr, retval=retval)
+ return retval
+ if stop.is_exit:
+ raise GreenEyeError(f"call {addr:#x}: target exited")
+ finally:
+ if temp.attached:
+ self.remove_breakpoint(temp)
+ for name in RESTORE_REGS:
+ if name in saved:
+ try:
+ self.client.write_reg(name, saved[name])
+ except Exception:
+ pass
+
def run(
self,
duration: float | None = None,
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/target.py b/debug/risc-v-gsp-rm-vm/greeneye/target.py
index efcb1d1..273f216 100644
--- a/debug/risc-v-gsp-rm-vm/greeneye/target.py
+++ b/debug/risc-v-gsp-rm-vm/greeneye/target.py
@@ -59,10 +59,12 @@ class QemuTarget:
qemu_log: str | None = None,
workdir: str | None = None,
keep_alive: bool = False,
+ ram_base: int = 0x80000000,
):
self.qemu = qemu or find_qemu()
self.machine = machine
self.memory_mb = memory_mb
+ self.ram_base = ram_base
self.host = "127.0.0.1"
self.port = gdb_port or free_port()
self.bios = bios