diff options
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.py | 179 |
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) |
