summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
commit1f2073d4fbb1d412d9bbf373312e3c51b83db92a (patch)
treea661537d7ec9510324e0a40a5a65e4b8c853f01b /debug/risc-v-gsp-rm-vm/sim/gsp_boot.py
parentc1adf2727983f5cace8ca0f5d1119ac2256c348e (diff)
downloadopen-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.tar.gz
open-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.zip
greeneye: GMMU/FBIF observability, PTE decoder and oracle, MPU/libos simulator wave
Simulator development wave (docs 42-44): GMMU page-table entry decode with its oracle/workflow harness, FBIF observability, and MPU/LIBOS device surfaces in the device model, plus refreshed boot/machine/profile data (tu104-570.144), the qemu-line results, and the new unit tests. Docs 00/06/35-41 updated to match; harness helpers and .gitignore included.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim/gsp_boot.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/gsp_boot.py179
1 files changed, 172 insertions, 7 deletions
diff --git a/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py b/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py
index c586ab3..a6f2545 100644
--- a/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py
+++ b/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py
@@ -22,6 +22,16 @@ The boot shim supplies reset-time state that the real SEC2 booter/host would
provide (bootargs and the register arguments); every shim value is an
explicit, documented assumption in the profile and the session metadata. The
device model itself never fakes a register read.
+
+GMMU/FBIF observability (``--trace-out``, default off): the profile's
+``gmmu``/``fbif`` sections declare observe-only register windows
+(:mod:`sim.devices.observe`); every access in them is recorded (never served,
+never faked) and, when requested, exported as a
+``greeneye-gmmu-fbif-trace/1`` document. ``--symbols`` optionally resolves
+recorded PCs through a ``greeneye-symbols/1`` JSON database, and
+``--observe-window BASE:SIZE[:LABEL[:MODE]]`` adds a runtime window list for
+surfaces whose base is not established yet. The count summary is always part
+of the boot report; see ``docs/42-GMMU-FBIF-OBSERVABILITY.md``.
"""
from __future__ import annotations
@@ -35,7 +45,14 @@ from dataclasses import dataclass, field
from greeneye.device_map import firmware_sha256
from greeneye.elf import load_elf
from greeneye.errors import GreenEyeError
+from greeneye.symbols import SymbolDB
from sim.devices import load_profile, registry_from_profile
+from sim.libos import LibosSyscalls, PROCESSOR_SUSPEND, TASK_PORT_SEND_RECV_WAIT
+from sim.devices.observe import (
+ ObservedWindow,
+ build_trace,
+ parse_window_spec,
+)
from sim.machine import (
R_BP,
R_BUDGET,
@@ -89,6 +106,10 @@ LIBOS_MEMORY_REGION_LOC_SYSMEM = 1
WPRM_ID8 = 0x5750524D # 'WPRM'
+#: ``libos_bootstrap_args_t``: IMEM then DMEM ``base/size/bs`` u64 fields.
+BOOTSTRAP_ARGS_SIZE = 6 * 8
+
+
def _int(value, default=None):
if value is None:
return default
@@ -131,6 +152,25 @@ def build_bootargs(machine, *, bootargs_pa: int, core_dump_pa: int,
# initArgs[1..2] stay zero (id8 == 0 terminates the array).
+def build_bootstrap_args(machine, *, pa: int, imem_base_pa: int, imem_size: int,
+ imem_bs_size: int, dmem_base_pa: int, dmem_size: int,
+ dmem_bs_size: int) -> None:
+ """Write the ``libos_bootstrap_args_t`` the bootloader passes in ``a0``.
+
+ Assumption (profile-recorded): the real libos bootloader builds this
+ structure (IMEM/DMEM base+size+bs) from its TCM configuration and calls
+ the firmware entry with a pointer to it; we reconstruct the documented
+ layout. Values are declared in the profile, never derived from silicon.
+ """
+ machine.write_mem(pa, bytes(BOOTSTRAP_ARGS_SIZE))
+ _write_u64(machine, pa + 0x00, imem_base_pa)
+ _write_u64(machine, pa + 0x08, imem_size)
+ _write_u64(machine, pa + 0x10, imem_bs_size)
+ _write_u64(machine, pa + 0x18, dmem_base_pa)
+ _write_u64(machine, pa + 0x20, dmem_size)
+ _write_u64(machine, pa + 0x28, dmem_bs_size)
+
+
def build_wpr_meta(machine, *, wpr_meta_pa: int, elf_pa: int, elf_size: int,
wpr_start: int, wpr_end: int,
boot_bin_offset: int, gsp_fw_offset: int,
@@ -206,7 +246,10 @@ class GspBootResult:
device_state_sha256: str | None = None
registers: dict = field(default_factory=dict)
shim: dict = field(default_factory=dict)
+ libos: dict = field(default_factory=dict)
milestones: list = field(default_factory=list)
+ trace_summary: dict | None = None
+ trace_path: str | None = None
device_map: object | None = field(default=None, repr=False)
def to_dict(self) -> dict:
@@ -232,7 +275,12 @@ class GspBootResult:
"device_state_sha256": self.device_state_sha256,
"registers": self.registers,
"boot_shim": self.shim,
+ "libos": self.libos,
"milestones": list(self.milestones),
+ "gmmu_fbif_trace": {
+ "path": self.trace_path,
+ "summary": self.trace_summary,
+ },
}
@@ -263,8 +311,18 @@ def run_boot(
name: str = "gsp-boot",
use_shim: bool = True,
shim_overrides: dict | None = None,
+ trace_out: str | None = None,
+ symbols_path: str | None = None,
+ observe_windows: list[str] | None = None,
) -> GspBootResult:
- """Run one bounded boot attempt and record a session."""
+ """Run one bounded boot attempt and record a session.
+
+ ``trace_out`` (default off) writes the ``greeneye-gmmu-fbif-trace/1``
+ observer document to that path (a directory gets ``gmmu-fbif-trace.json``
+ inside); ``symbols_path`` resolves recorded PCs through a
+ ``greeneye-symbols/1`` JSON database; ``observe_windows`` adds runtime
+ observer windows (``BASE:SIZE[:LABEL[:MODE]]``, repeatable).
+ """
if instructions <= 0 or instructions > MAX_ATTEMPT_INSTRUCTIONS:
raise GreenEyeError(
f"instructions must be in 1..{MAX_ATTEMPT_INSTRUCTIONS}, got {instructions}")
@@ -274,6 +332,15 @@ def run_boot(
return GspBootResult(ran=False, reason=f"profile absent: {profile_path}",
image=image)
+ resolve_pc = None
+ if symbols_path:
+ db = SymbolDB()
+ db.load_json(symbols_path)
+
+ def resolve_pc(pc, _db=db):
+ sym = _db.resolve(pc)
+ return sym.label(pc) if sym is not None else None
+
profile = load_profile(profile_path)
image_obj = load_elf(image)
if not image_obj.is_riscv:
@@ -299,6 +366,16 @@ def run_boot(
machine = session.target.machine
machine.devices = registry_from_profile(profile, machine=machine)
machine.translator = getattr(machine.devices, "dma", None)
+ libos = LibosSyscalls()
+ machine.syscall_handler = libos.handle
+ for spec in observe_windows or ():
+ win = parse_window_spec(spec)
+ machine.devices.add(ObservedWindow(
+ base=win["base"], size=win["size"], label=win["label"],
+ mode=win["mode"], registers=win["registers"],
+ evidence="runtime --observe-window",
+ clock=machine.insn_count,
+ ))
shim_meta = {"enabled": bool(use_shim), "assumptions": []}
if use_shim:
@@ -309,6 +386,7 @@ def run_boot(
_apply_boot_shim(machine, shim, image_obj, shim_meta)
milestones_hit: list[str] = []
+ trace_path: str | None = None
try:
reason, pc, a, b = _run_with_milestones(machine, instructions, milestones_hit)
model = machine.cycle_model
@@ -320,8 +398,19 @@ def run_boot(
state_sha = hashlib.sha256(state_json.encode()).hexdigest() if state else None
registers = {f"x{i}": machine.x[i] for i in range(32) if i and machine.x[i]}
registers["pc"] = pc
- rung = _rung_from_run(machine, shim_meta, milestones_hit)
+ rung = _rung_from_run(machine, shim_meta, milestones_hit, libos)
shim_meta["rung"] = rung
+ trace_doc = build_trace(
+ reg,
+ image_sha256=sha,
+ profile=os.path.basename(profile_path),
+ profile_name=profile.get("name"),
+ symbols_path=symbols_path,
+ resolve=resolve_pc,
+ ) if reg is not None else None
+ trace_summary = trace_doc["summary"] if trace_doc is not None else None
+ if trace_out and trace_doc is not None:
+ trace_path = _write_trace_file(trace_out, trace_doc)
meta = {
"kind": "gsp-boot",
"profile": os.path.basename(profile_path),
@@ -341,7 +430,13 @@ def run_boot(
"device_accesses": len(getattr(reg, "accesses", ())),
"device_state_sha256": state_sha,
"boot_shim": shim_meta,
+ "mpu": machine.mpu.state(),
+ "libos": libos.state(),
}
+ if trace_summary is not None:
+ meta["gmmu_fbif"] = trace_summary
+ if trace_path is not None:
+ meta["gmmu_fbif_trace"] = trace_path
if getattr(session.recorder, "dir", None) is not None:
if state is not None:
rel = session.recorder.blob(
@@ -349,6 +444,12 @@ def run_boot(
json.dumps(state, indent=1, sort_keys=True, default=str).encode(),
)
meta["device_state_blob"] = rel
+ if trace_doc is not None and trace_out:
+ rel = session.recorder.blob(
+ "gmmu-fbif-trace.json",
+ json.dumps(trace_doc, indent=1, sort_keys=True).encode(),
+ )
+ meta["gmmu_fbif_trace_blob"] = rel
session.recorder.meta.update(meta)
session.note(
"gsp_boot",
@@ -383,7 +484,10 @@ def run_boot(
device_state_sha256=state_sha,
registers=registers,
shim=shim_meta,
+ libos=libos.state(),
milestones=list(milestones_hit),
+ trace_summary=trace_summary,
+ trace_path=trace_path,
)
@@ -408,8 +512,8 @@ def _run_with_milestones(machine, budget: int, hit: list[str]):
return R_BUDGET, machine.pc, 0, 0
-def _rung_from_run(machine, shim_meta: dict, milestones_hit) -> str:
- """Observed rung from milestones and the unknown-MMIO surface.
+def _rung_from_run(machine, shim_meta: dict, milestones_hit, libos=None) -> str:
+ """Observed rung from milestones, libos services and the unknown-MMIO surface.
B0: the first unmodelled access is the PMC ``BOOT_42`` chip-ID read.
B1: the device model served the whole run (no unknown MMIO) and the boot
@@ -417,10 +521,17 @@ def _rung_from_run(machine, shim_meta: dict, milestones_hit) -> str:
B2: the boot shim was consumed and the firmware reached its relocation
stage (milestone ``relocation_copy``) -- boot-stage progress beyond
the bootargs consumer.
+ B3: the libos init path completed its services (TCM/pagetable
+ registration, DMA flush) and the message-loop task blocks in the
+ port wait -- the CSB/TRANSCOM poll itself is host-traffic-triggered
+ and remains owed to the message-injection rung (docs/35 ยง3.3 rev).
"""
first_unknown = machine.mmio_log[0] if machine.mmio_log else None
if first_unknown is not None and first_unknown["addr"] == 0x2000000000000A00:
return "B0"
+ stopped = getattr(libos, "stopped", None) if libos is not None else None
+ if stopped and stopped.get("num") in (TASK_PORT_SEND_RECV_WAIT, PROCESSOR_SUSPEND):
+ return "B3"
if "relocation_copy" in milestones_hit:
return "B2"
return "B1"
@@ -431,6 +542,8 @@ def _rung_note(rung: str) -> str:
"B0": "first MMIO is the unmodelled PMC BOOT_42 chip-ID read",
"B1": "device model serves the boot path; bootargs consumer reached",
"B2": "boot-stage progress past the bootargs consumer (relocation)",
+ "B3": "init complete; message-loop task blocks in the port wait "
+ "(CSB poll awaits host traffic)",
}.get(rung, rung)
@@ -472,6 +585,24 @@ def _apply_boot_shim(machine, shim: dict, image_obj, shim_meta: dict) -> None:
shim_meta["assumptions"].append(
"GspFwWprMeta reconstructed from the documented layout (header); "
"real value is booter-provided")
+ bootstrap = shim.get("bootstrap_args")
+ if bootstrap:
+ args_pa = _int(bootstrap.get("pa", 0x81000))
+ build_bootstrap_args(
+ machine,
+ pa=args_pa,
+ imem_base_pa=_int(bootstrap.get("imem_base_pa", 0x0)),
+ imem_size=_int(bootstrap.get("imem_size", 0x10000)),
+ imem_bs_size=_int(bootstrap.get("imem_bs_size", 0x0)),
+ dmem_base_pa=_int(bootstrap.get("dmem_base_pa", 0x100000000)),
+ dmem_size=_int(bootstrap.get("dmem_size", 0x10000)),
+ dmem_bs_size=_int(bootstrap.get("dmem_bs_size", 0x0)),
+ )
+ shim_meta["assumptions"].append(
+ "libos_bootstrap_args_t (IMEM/DMEM base+size+bs) reconstructed at "
+ "0x%x from the documented layout; real value is bootloader-built "
+ "and passed in a0" % args_pa)
+ shim_meta["bootstrap_args_pa"] = args_pa
shim_meta["applied"] = True
shim_meta["registers"] = {k: registers[k] for k in sorted(registers)}
image_at = shim.get("image_at")
@@ -481,13 +612,27 @@ def _apply_boot_shim(machine, shim: dict, image_obj, shim_meta: dict) -> None:
blob = fh.read()
machine.write_mem(addr, blob)
shim_meta["assumptions"].append(
- "raw firmware image mirrored at 0x%x (DMEM) so the app's own "
- "self-relocation copy is self-consistent; real value is "
- "booter-DMA-provided" % addr)
+ "raw firmware image mirrored at 0x%x (DMEM); legacy fallback for "
+ "profiles without a bootstrap-args record (no longer used by the "
+ "tu104-570.144 profile: the MMU model resolves the copy source)"
+ % addr)
shim_meta["image_at"] = addr
shim_meta["image_bytes"] = len(blob)
+def _write_trace_file(path: str, doc: dict) -> str:
+ """Write the observer trace document; a directory gets the canonical name."""
+ target = path
+ if os.path.isdir(path) or path.endswith(os.sep):
+ target = os.path.join(path, "gmmu-fbif-trace.json")
+ parent = os.path.dirname(os.path.abspath(target))
+ os.makedirs(parent, exist_ok=True)
+ with open(target, "w", encoding="utf-8") as fh:
+ json.dump(doc, fh, indent=1, sort_keys=True)
+ fh.write("\n")
+ return target
+
+
def _print_result(result: GspBootResult, out=sys.stdout) -> None:
print(f"image: {result.image}", file=out)
print(f"profile: {result.profile_name} ({result.profile})", file=out)
@@ -504,6 +649,14 @@ def _print_result(result: GspBootResult, out=sys.stdout) -> None:
first = result.first_unknown_mmio
print(f"first unknown MMIO: pc={first['pc']:#x} addr={first['addr']:#x} "
f"size={first['size']} rw={first['rw']}", file=out)
+ if result.trace_summary is not None:
+ summary = result.trace_summary
+ print(f"GMMU/FBIF observers: {summary['windows']} windows, "
+ f"{summary['accesses']} accesses ({summary['reads']} r / "
+ f"{summary['writes']} w, {summary['served']} served, "
+ f"{summary['windows_touched']} windows touched)", file=out)
+ if result.trace_path is not None:
+ print(f"GMMU/FBIF trace: {result.trace_path}", file=out)
print("note: device semantics carry their evidence in docs/36; "
"boot-shim values are declared assumptions", file=out)
@@ -520,6 +673,15 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--name", default="gsp-boot")
parser.add_argument("--no-shim", action="store_true",
help="run without the boot shim (pure device model)")
+ parser.add_argument("--trace-out", default=None,
+ help="write the greeneye-gmmu-fbif-trace/1 observer "
+ "document to this file (or directory); default off")
+ parser.add_argument("--symbols", default=None,
+ help="greeneye-symbols/1 JSON database for PC resolution")
+ parser.add_argument("--observe-window", action="append", default=None,
+ metavar="BASE:SIZE[:LABEL[:MODE]]",
+ help="extra runtime observer window (repeatable); "
+ "observe-only unless MODE=storage")
parser.add_argument("--json", action="store_true")
args = parser.parse_args(argv)
try:
@@ -530,6 +692,9 @@ def main(argv: list[str] | None = None) -> int:
record=args.record,
name=args.name,
use_shim=not args.no_shim,
+ trace_out=args.trace_out,
+ symbols_path=args.symbols,
+ observe_windows=args.observe_window,
)
except GreenEyeError as exc:
print(f"[gsp-boot] error: {exc}", file=sys.stderr)