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 | 548 |
1 files changed, 548 insertions, 0 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 new file mode 100644 index 0000000..c586ab3 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/gsp_boot.py @@ -0,0 +1,548 @@ +"""Real-firmware GSP boot runner (device model + versioned profile + shim). + +Loads ``gsp-570.144.fwimage.bin`` at its **link addresses** (RAM base 0), builds +the :mod:`sim.devices` registry from a ``greeneye-sim-profile/1`` document, runs +bounded (<= 2M instructions per attempt), and records a normal +``greeneye-session/1`` directory carrying the device state, unknown-MMIO +summary and the boot-shim assumptions. + +Boot ladder rungs (docs/35-GREENEYE-SIMULATOR-DESIGN.md ยง3.3) reported by +:class:`GspBootResult`: + +* ``B0`` -- flat image loads, first MMIO observed at ``0x2000_0000_0000_0A00``. +* ``B1`` -- first fault past the QEMU point, explained by the device model + (the QEMU fault is the PMC ``BOOT_42`` chip-ID read; modelled by + :class:`sim.devices.pmc.PmcBlock`). +* ``B2`` -- boot-stage progress: vector/MPU/TCM setup and bootargs consumption + exercised with the profile. +* ``B3`` -- first CSB/TRANSCOM message-window poll served with read-back. +* ``B4`` -- message loop reached (waits on a host message). + +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. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import sys +from dataclasses import dataclass, field + +from greeneye.device_map import firmware_sha256 +from greeneye.elf import load_elf +from greeneye.errors import GreenEyeError +from sim.devices import load_profile, registry_from_profile +from sim.machine import ( + R_BP, + R_BUDGET, + R_EBREAK, + R_ECALL, + R_FAULT, + R_STEP, + R_WATCH, +) + +TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DEFAULT_IMAGE = os.path.normpath( + os.path.join(TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", + "gsp-570.144.fwimage.bin") +) +DEFAULT_PROFILE = os.path.join(TOOL_ROOT, "sim", "profiles", "tu104-570.144.json") + +#: Hard cap for one attempt (task rule: <= 2M instructions per attempt). +MAX_ATTEMPT_INSTRUCTIONS = 2_000_000 +#: Default bounded budget for a boot attempt. +DEFAULT_INSTRUCTIONS = 2_000_000 + +STOP_REASONS = { + R_BUDGET: "budget", + R_BP: "breakpoint", + R_WATCH: "watch", + R_EBREAK: "ebreak", + R_FAULT: "fault", + R_STEP: "step", + R_ECALL: "ecall", +} + +# GSP RISC-V address-map aperture starts (header constants). +SYSGPA_START = 0x8180000000000000 +FBGPA_START = 0x6180000000000000 +PRIV_START = 0x2000000000000000 + +# GspFwWprMeta field constants (header). +GSP_FW_WPR_META_MAGIC = 0xdc3aae21371a60b3 +GSP_FW_WPR_META_REVISION = 1 +GSP_FW_WPR_META_VERIFIED = 0xa0a0a0a0a0a0a0a0 +WPR_META_SIZE = 256 + +# NV_RISCV_BOOTLDR_PARAMS / NV_RISCV_BOOT_PARAMS layout (header). +BOOTLDR_BOOT_TYPE_RM = 0x4E56524D # 'NVRM' +BOOTLDR_VERSION = 1 +BOOTARGS_DEFAULT_SIZE = 0x78 # NV_RISCV_BOOT_PARAMS (120 bytes) +LIBOS_MEMORY_REGION_CONTIGUOUS = 1 +LIBOS_MEMORY_REGION_LOC_FB = 2 +LIBOS_MEMORY_REGION_LOC_SYSMEM = 1 +WPRM_ID8 = 0x5750524D # 'WPRM' + + +def _int(value, default=None): + if value is None: + return default + if isinstance(value, str): + return int(value, 0) + return int(value) + + +def _write_u32(machine, addr: int, value: int) -> None: + machine.write_mem(addr, (value & 0xFFFFFFFF).to_bytes(4, "little")) + + +def _write_u64(machine, addr: int, value: int) -> None: + machine.write_mem(addr, (value & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "little")) + + +def build_bootargs(machine, *, bootargs_pa: int, core_dump_pa: int, + core_dump_size: int, wpr_meta_pa: int, + wpr_meta_size: int = WPR_META_SIZE, + bootargs_size: int = BOOTARGS_DEFAULT_SIZE) -> None: + """Write ``NV_RISCV_BOOT_PARAMS`` and its ``WPRM`` init arg. + + Assumption (profile-recorded): the real SEC2 booter/host fills these; we + reconstruct the documented structure and one ``WPRM`` libos init argument + pointing at the ``GspFwWprMeta`` written by :func:`build_wpr_meta`. + """ + _write_u32(machine, bootargs_pa + 0x00, BOOTLDR_BOOT_TYPE_RM) + machine.write_mem(bootargs_pa + 0x04, (bootargs_size & 0xFFFF).to_bytes(2, "little")) + machine.write_mem(bootargs_pa + 0x06, bytes([BOOTLDR_VERSION])) + machine.write_mem(bootargs_pa + 0x07, b"\x00") + _write_u64(machine, bootargs_pa + 0x08, core_dump_pa) + _write_u32(machine, bootargs_pa + 0x10, core_dump_size) + # libos.initArgs[0]: WPRM in FB, contiguous. + base = bootargs_pa + 0x18 + _write_u64(machine, base + 0x00, WPRM_ID8) + _write_u64(machine, base + 0x08, wpr_meta_pa) + _write_u64(machine, base + 0x10, wpr_meta_size) + machine.write_mem(base + 0x18, bytes([LIBOS_MEMORY_REGION_CONTIGUOUS, + LIBOS_MEMORY_REGION_LOC_FB])) + # initArgs[1..2] stay zero (id8 == 0 terminates the array). + + +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, + fb_size: int) -> None: + """Write a ``GspFwWprMeta`` (header layout) into RAM. + + Assumption (profile-recorded): the booter/CPU-RM produces this structure; + we place it with a self-consistent layout that satisfies the documented + validation (magic/revision/verified, monotonic FB ranges). Values are + declared in the profile, never derived from silicon. + """ + machine.write_mem(wpr_meta_pa, bytes(WPR_META_SIZE)) + _write_u64(machine, wpr_meta_pa + 0x00, GSP_FW_WPR_META_MAGIC) + _write_u64(machine, wpr_meta_pa + 0x08, GSP_FW_WPR_META_REVISION) + _write_u64(machine, wpr_meta_pa + 0x10, elf_pa) + _write_u64(machine, wpr_meta_pa + 0x18, elf_size) + _write_u64(machine, wpr_meta_pa + 0x20, 0) # sysmemAddrOfBootloader + _write_u64(machine, wpr_meta_pa + 0x28, 0) # sizeOfBootloader + _write_u64(machine, wpr_meta_pa + 0x30, 0) # bootloaderCodeOffset + _write_u64(machine, wpr_meta_pa + 0x38, 0) # bootloaderDataOffset + _write_u64(machine, wpr_meta_pa + 0x40, 0) # bootloaderManifestOffset + _write_u64(machine, wpr_meta_pa + 0x48, 0) # sysmemAddrOfSignature + _write_u64(machine, wpr_meta_pa + 0x50, 0) # sizeOfSignature + _write_u64(machine, wpr_meta_pa + 0x58, wpr_start) # gspFwRsvdStart + _write_u64(machine, wpr_meta_pa + 0x60, wpr_start) # nonWprHeapOffset + _write_u64(machine, wpr_meta_pa + 0x68, 0x10000) # nonWprHeapSize + _write_u64(machine, wpr_meta_pa + 0x70, wpr_start) # gspFwWprStart + _write_u64(machine, wpr_meta_pa + 0x78, wpr_start) # gspFwHeapOffset + _write_u64(machine, wpr_meta_pa + 0x80, wpr_end - wpr_start) # heap size + _write_u64(machine, wpr_meta_pa + 0x88, gsp_fw_offset) # gspFwOffset + _write_u64(machine, wpr_meta_pa + 0x90, boot_bin_offset) # bootBinOffset + _write_u64(machine, wpr_meta_pa + 0x98, 0) # frtsOffset + _write_u64(machine, wpr_meta_pa + 0xA0, 0) # frtsSize + _write_u64(machine, wpr_meta_pa + 0xA8, wpr_end) # gspFwWprEnd + _write_u64(machine, wpr_meta_pa + 0xB0, fb_size) # fbSize + _write_u64(machine, wpr_meta_pa + 0xB8, wpr_end) # vgaWorkspaceOffset + _write_u64(machine, wpr_meta_pa + 0xC0, 0x100000) # vgaWorkspaceSize + _write_u64(machine, wpr_meta_pa + 0xC8, 0) # bootCount + _write_u64(machine, wpr_meta_pa + 0xD0, 0) # partitionRpcAddr + _write_u64(machine, wpr_meta_pa + 0xF8, GSP_FW_WPR_META_VERIFIED) + + +#: Observed boot milestones (firmware vaddrs) used to classify the rung. +#: Evidence: disassembly of the 570.144 image (bootargs consumer reads +#: NV_RISCV_BOOT_PARAMS; the relocation loop copies from the DMEM aperture +#: into the 0x400E000 runtime region). +MILESTONES = ( + ("bootargs_consumer", 0x4007A30), + ("relocation_copy", 0x4007B7C), +) + + +@dataclass +class GspBootResult: + ran: bool + reason: str + image: str + profile: str | None = None + profile_name: str | None = None + image_sha256: str | None = None + session_dir: str | None = None + instructions: int = 0 + cycles: int = 0 + cycle_model: str | None = None + stop_reason: str | None = None + stop_pc: int | None = None + stop_pc_fw: int | None = None + rung: str = "B0" + first_unknown_mmio: dict | None = None + mmio_unknown: int = 0 + device_unhandled: int = 0 + device_accesses: int = 0 + device_state_sha256: str | None = None + registers: dict = field(default_factory=dict) + shim: dict = field(default_factory=dict) + milestones: list = field(default_factory=list) + device_map: object | None = field(default=None, repr=False) + + def to_dict(self) -> dict: + return { + "schema": "greeneye-gsp-boot/1", + "ran": self.ran, + "reason": self.reason, + "image": self.image, + "image_sha256": self.image_sha256, + "profile": self.profile, + "profile_name": self.profile_name, + "session": self.session_dir, + "rung": self.rung, + "instructions_executed": self.instructions, + "cycles": self.cycles, + "cycle_model": self.cycle_model, + "stop": {"reason": self.stop_reason, "pc": self.stop_pc, + "pc_fw": self.stop_pc_fw}, + "first_unknown_mmio": self.first_unknown_mmio, + "mmio_unknown": self.mmio_unknown, + "device_unhandled": self.device_unhandled, + "device_accesses": self.device_accesses, + "device_state_sha256": self.device_state_sha256, + "registers": self.registers, + "boot_shim": self.shim, + "milestones": list(self.milestones), + } + + +def _fw_pc(pc: int) -> int: + return pc + + +def classify_rung(machine, *, shim: dict) -> str: + """Best rung supported by the observed run (never inferred, only observed).""" + if machine.mmio_unknown and machine.mmio_log: + first = machine.mmio_log[0] + if first["addr"] == 0x2000000000000A00: + return "B0" + if shim.get("applied"): + # The firmware consumed the boot shim and advanced past the first + # unknown; B2 is claimed only when the run went past the chip gate and + # exercised the MPU/bootargs path (the runner checks the PC). + return shim.get("rung", "B1") + return "B1" + + +def run_boot( + *, + image: str = DEFAULT_IMAGE, + profile_path: str = DEFAULT_PROFILE, + instructions: int = DEFAULT_INSTRUCTIONS, + record=None, + name: str = "gsp-boot", + use_shim: bool = True, + shim_overrides: dict | None = None, +) -> GspBootResult: + """Run one bounded boot attempt and record a session.""" + if instructions <= 0 or instructions > MAX_ATTEMPT_INSTRUCTIONS: + raise GreenEyeError( + f"instructions must be in 1..{MAX_ATTEMPT_INSTRUCTIONS}, got {instructions}") + if not os.path.isfile(image): + return GspBootResult(ran=False, reason=f"image absent: {image}", image=image) + if not os.path.isfile(profile_path): + return GspBootResult(ran=False, reason=f"profile absent: {profile_path}", + image=image) + + profile = load_profile(profile_path) + image_obj = load_elf(image) + if not image_obj.is_riscv: + raise GreenEyeError(f"{image}: not a RISC-V ELF image") + sha = firmware_sha256(image) + + load = profile.get("load", {}) + ram_base = _int(load.get("ram_base", 0)) + memory_mb = _int(load.get("memory_mb", 12288)) + + from greeneye.sim_backend import launch_native # deferred + + session = launch_native( + kernel=image, + memory_mb=memory_mb, + ram_base=ram_base, + record=True if record is None else record, + name=name, + device_map=None, + unknown_mmio="lenient", + bias=0, + ) + machine = session.target.machine + machine.devices = registry_from_profile(profile, machine=machine) + machine.translator = getattr(machine.devices, "dma", None) + + shim_meta = {"enabled": bool(use_shim), "assumptions": []} + if use_shim: + shim = dict(profile.get("boot_shim", {})) + if shim_overrides: + shim.update(shim_overrides) + shim["_image_path"] = image + _apply_boot_shim(machine, shim, image_obj, shim_meta) + + milestones_hit: list[str] = [] + try: + reason, pc, a, b = _run_with_milestones(machine, instructions, milestones_hit) + model = machine.cycle_model + stop_reason = STOP_REASONS.get(reason, str(reason)) + first = machine.mmio_log[0] if machine.mmio_log else None + reg = machine.devices + state = reg.state() if reg is not None else None + state_json = json.dumps(state, sort_keys=True, default=str) if state else "" + 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) + shim_meta["rung"] = rung + meta = { + "kind": "gsp-boot", + "profile": os.path.basename(profile_path), + "profile_name": profile.get("name"), + "firmware_sha256": sha, + "ram_base": ram_base, + "instructions_executed": machine.insns, + "cycles": machine.cycles, + "cycle_model": model.name if model is not None else None, + "stop_reason": stop_reason, + "stop_pc": pc, + "rung": rung, + "milestones": list(milestones_hit), + "mmio_unknown": machine.mmio_unknown, + "first_unknown_mmio": first, + "device_unhandled": getattr(reg, "unhandled", 0), + "device_accesses": len(getattr(reg, "accesses", ())), + "device_state_sha256": state_sha, + "boot_shim": shim_meta, + } + if getattr(session.recorder, "dir", None) is not None: + if state is not None: + rel = session.recorder.blob( + "device-state.json", + json.dumps(state, indent=1, sort_keys=True, default=str).encode(), + ) + meta["device_state_blob"] = rel + session.recorder.meta.update(meta) + session.note( + "gsp_boot", + rung=rung, + instructions=machine.insns, + stop_reason=stop_reason, + stop_pc=pc, + ) + session_dir = getattr(session.recorder, "dir", None) + finally: + session.close() + + return GspBootResult( + ran=True, + reason="ok", + image=image, + profile=profile_path, + profile_name=profile.get("name"), + image_sha256=sha, + session_dir=session_dir, + instructions=machine.insns, + cycles=machine.cycles, + cycle_model=model.name if model is not None else None, + stop_reason=stop_reason, + stop_pc=pc, + stop_pc_fw=_fw_pc(pc), + rung=rung, + first_unknown_mmio=first, + mmio_unknown=machine.mmio_unknown, + device_unhandled=getattr(reg, "unhandled", 0), + device_accesses=len(getattr(reg, "accesses", ())), + device_state_sha256=state_sha, + registers=registers, + shim=shim_meta, + milestones=list(milestones_hit), + ) + + +def _run_with_milestones(machine, budget: int, hit: list[str]): + """Run the budget, stopping at each milestone once to record it. + + Milestone hits are evidence of boot-stage progress (they are firmware + vaddrs reached), not inferred from timing or heuristics. + """ + remaining = budget + while remaining > 0: + pending = {addr for name, addr in MILESTONES if name not in hit} + reason, pc, a, b = machine.run(remaining, breakpoints=pending) + remaining = budget - machine.insns + if reason == R_BP: + for name, addr in MILESTONES: + if addr == pc and name not in hit: + hit.append(name) + break + continue + return reason, pc, a, b + 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. + + 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 + chip gate was passed; the run may stop at the bootargs panic/halt. + B2: the boot shim was consumed and the firmware reached its relocation + stage (milestone ``relocation_copy``) -- boot-stage progress beyond + the bootargs consumer. + """ + 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" + if "relocation_copy" in milestones_hit: + return "B2" + return "B1" + + +def _rung_note(rung: str) -> str: + return { + "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)", + }.get(rung, rung) + + +def _apply_boot_shim(machine, shim: dict, image_obj, shim_meta: dict) -> None: + """Write profile-declared reset state (documented assumption).""" + registers = {k: (int(v, 0) if isinstance(v, str) else int(v)) + for k, v in shim.get("registers", {}).items()} + machine.apply_reset_regs(registers) + bootargs_pa = shim.get("bootargs_pa") + if bootargs_pa is not None: + bootargs_pa = int(bootargs_pa, 0) if isinstance(bootargs_pa, str) else int(bootargs_pa) + core_dump_pa = _int(shim.get("core_dump_pa", 0x40000)) + core_dump_size = _int(shim.get("core_dump_size", 0x10000)) + build_bootargs( + machine, + bootargs_pa=bootargs_pa, + core_dump_pa=core_dump_pa, + core_dump_size=core_dump_size, + wpr_meta_pa=_int(shim.get("wpr_meta_pa", 0x90000)), + wpr_meta_size=_int(shim.get("wpr_meta_size", WPR_META_SIZE)), + bootargs_size=_int(shim.get("bootargs_size", BOOTARGS_DEFAULT_SIZE)), + ) + shim_meta["assumptions"].append( + "NV_RISCV_BOOT_PARAMS reconstructed at 0x%x from the documented " + "layout (header); real value is host/booter-provided" % bootargs_pa) + if shim.get("wpr_meta_pa") is not None and shim.get("wpr"): + wpr = shim["wpr"] + build_wpr_meta( + machine, + wpr_meta_pa=_int(shim["wpr_meta_pa"]), + elf_pa=_int(wpr.get("elf_pa", image_obj.entry)), + elf_size=_int(wpr.get("elf_size", image_obj.max_vaddr - 0x4000000)), + wpr_start=_int(wpr.get("wpr_start", 0x2000000)), + wpr_end=_int(wpr.get("wpr_end", 0x6000000)), + boot_bin_offset=_int(wpr.get("boot_bin_offset", 0x2000000)), + gsp_fw_offset=_int(wpr.get("gsp_fw_offset", 0x4000000)), + fb_size=_int(wpr.get("fb_size", 0x200000000)), + ) + shim_meta["assumptions"].append( + "GspFwWprMeta reconstructed from the documented layout (header); " + "real value is booter-provided") + shim_meta["applied"] = True + shim_meta["registers"] = {k: registers[k] for k in sorted(registers)} + image_at = shim.get("image_at") + if image_at is not None: + addr = _int(image_at) + with open(shim["_image_path"], "rb") as fh: + 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) + shim_meta["image_at"] = addr + shim_meta["image_bytes"] = len(blob) + + +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) + print(f"session: {result.session_dir}", file=out) + print(f"rung: {result.rung}", file=out) + print(f"instructions: {result.instructions}", file=out) + print(f"cycles (modeled): {result.cycles}", file=out) + print(f"stop: {result.stop_reason} pc={result.stop_pc:#x}" + if result.stop_pc is not None else f"stop: {result.stop_reason}", file=out) + print(f"unknown MMIO accesses: {result.mmio_unknown}", file=out) + print(f"device accesses: {result.device_accesses} " + f"(unhandled in windows: {result.device_unhandled})", file=out) + if result.first_unknown_mmio: + 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) + print("note: device semantics carry their evidence in docs/36; " + "boot-shim values are declared assumptions", file=out) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="python3 -m sim.gsp_boot", + description=("Bounded real-firmware GSP boot attempt with the device model " + "and versioned profile (read-only image; no hardware).")) + parser.add_argument("--image", default=DEFAULT_IMAGE) + parser.add_argument("--profile", default=DEFAULT_PROFILE) + parser.add_argument("--instructions", type=int, default=DEFAULT_INSTRUCTIONS) + parser.add_argument("--record", help="session directory (default: auto)") + 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("--json", action="store_true") + args = parser.parse_args(argv) + try: + result = run_boot( + image=args.image, + profile_path=args.profile, + instructions=args.instructions, + record=args.record, + name=args.name, + use_shim=not args.no_shim, + ) + except GreenEyeError as exc: + print(f"[gsp-boot] error: {exc}", file=sys.stderr) + return 1 + if not result.ran: + print(f"[gsp-boot] skipped: {result.reason}", file=sys.stderr) + return 2 + if args.json: + print(json.dumps(result.to_dict(), indent=1, sort_keys=True)) + else: + _print_result(result) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |
