summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/sim
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim')
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/devices/__init__.py26
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/devices/base.py16
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py21
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/devices/observe.py372
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py988
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/gsp_boot.py179
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/libos.py217
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/machine.py80
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/mpu.py154
-rw-r--r--debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json815
10 files changed, 2841 insertions, 27 deletions
diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py
index caf22b3..6fdce2e 100644
--- a/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py
+++ b/debug/risc-v-gsp-rm-vm/sim/devices/__init__.py
@@ -26,6 +26,14 @@ from .dma import DmaTranslator
from .falcon import FalconBlock
from .fwsec import Wpr2Block
from .irq import IrqRouter
+from .observe import (
+ TRACE_SCHEMA,
+ ObservedWindow,
+ build_trace,
+ collect_observations,
+ parse_window_spec,
+ windows_from_section,
+)
from .pmc import PmcBlock
from .pmu import PmuStub
from .priscv import PriscvBlock
@@ -33,12 +41,16 @@ from .timer import PtimerBlock
PROFILE_SCHEMA = "greeneye-sim-profile/1"
+#: Profile device sections that declare observe-only/log-only windows.
+OBSERVE_SECTIONS = ("gmmu", "fbif")
+
__all__ = [
"Device",
"DeviceRegistry",
"Window",
"SNAPSHOT_SCHEMA",
"PROFILE_SCHEMA",
+ "TRACE_SCHEMA",
"PmcBlock",
"FalconBlock",
"PriscvBlock",
@@ -48,8 +60,13 @@ __all__ = [
"PmuStub",
"DmaTranslator",
"IrqRouter",
+ "ObservedWindow",
"load_profile",
"registry_from_profile",
+ "windows_from_section",
+ "parse_window_spec",
+ "collect_observations",
+ "build_trace",
]
@@ -153,6 +170,15 @@ def registry_from_profile(profile: dict, *, machine=None,
)
elif name == "pmu":
dev = PmuStub()
+ elif name in OBSERVE_SECTIONS:
+ for win in windows_from_section(section):
+ reg.add(ObservedWindow(
+ base=win["base"], size=win["size"], label=win["label"],
+ mode=win["mode"], registers=win["registers"],
+ evidence=section.get("evidence", ""),
+ clock=(machine.insn_count if machine is not None else None),
+ ))
+ continue
else:
raise ValueError(f"unknown device section {name!r}")
for base, size in _windows(section):
diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/base.py b/debug/risc-v-gsp-rm-vm/sim/devices/base.py
index 877109c..7254334 100644
--- a/debug/risc-v-gsp-rm-vm/sim/devices/base.py
+++ b/debug/risc-v-gsp-rm-vm/sim/devices/base.py
@@ -44,6 +44,11 @@ class Device:
name = "device"
+ #: Opt-in hook: when True the registry forwards the faulting PC to
+ #: ``read``/``write`` as ``pc=`` (observer windows record it; every other
+ #: device keeps the original two-/three-argument call unchanged).
+ wants_pc = False
+
def __init__(self, base: int = 0, size: int = 0):
self.base = base
self.size = size
@@ -184,7 +189,10 @@ class DeviceRegistry:
matched = False
for win in self._candidates(addr, size):
matched = True
- value = win.device.read(addr - win.base, size)
+ if win.device.wants_pc:
+ value = win.device.read(addr - win.base, size, pc=pc)
+ else:
+ value = win.device.read(addr - win.base, size)
if value is not None:
self._record(addr, size, False, value, True, pc)
return True, value
@@ -202,7 +210,11 @@ class DeviceRegistry:
matched = False
for win in self._candidates(addr, size):
matched = True
- if win.device.write(addr - win.base, size, value):
+ if win.device.wants_pc:
+ handled = win.device.write(addr - win.base, size, value, pc=pc)
+ else:
+ handled = win.device.write(addr - win.base, size, value)
+ if handled:
self._record(addr, size, True, value, True, pc)
return True
self.unhandled += 1
diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py
index 8dcf633..b278ec8 100644
--- a/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py
+++ b/debug/risc-v-gsp-rm-vm/sim/devices/fwsec.py
@@ -25,6 +25,17 @@ WPR2_ADDR_HI = 0x1FA828
WPR2_ALIGNMENT = 12
WPR2_BLOCK = 0x20000
+#: Block-relative offsets (a profile window that starts at ``+0x1FA000``
+#: reaches the pair at ``0x824``/``0x828``; the device accepts both forms).
+WPR2_ADDR_LO_REL = WPR2_ADDR_LO & 0xFFF
+WPR2_ADDR_HI_REL = WPR2_ADDR_HI & 0xFFF
+_KEYS = {
+ WPR2_ADDR_LO: WPR2_ADDR_LO,
+ WPR2_ADDR_HI: WPR2_ADDR_HI,
+ WPR2_ADDR_LO_REL: WPR2_ADDR_LO,
+ WPR2_ADDR_HI_REL: WPR2_ADDR_HI,
+}
+
class Wpr2Block(Device):
"""WPR2 address pair as seen by the firmware (host/booter-written)."""
@@ -60,13 +71,15 @@ class Wpr2Block(Device):
self.regs: dict[int, int] = {}
def read(self, offset: int, size: int) -> int | None:
- if size == 4 and offset in self.regs:
- return self.regs[offset]
+ key = _KEYS.get(offset)
+ if size == 4 and key is not None and key in self.regs:
+ return self.regs[key]
return None
def write(self, offset: int, size: int, value: int) -> bool:
- if size == 4 and offset in (WPR2_ADDR_LO, WPR2_ADDR_HI):
- self.regs[offset] = value & 0xFFFFFFFF
+ key = _KEYS.get(offset)
+ if size == 4 and key is not None:
+ self.regs[key] = value & 0xFFFFFFFF
return True
return False
diff --git a/debug/risc-v-gsp-rm-vm/sim/devices/observe.py b/debug/risc-v-gsp-rm-vm/sim/devices/observe.py
new file mode 100644
index 0000000..d43cfe6
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/sim/devices/observe.py
@@ -0,0 +1,372 @@
+"""Observer-only MMIO windows: record accesses, change no semantics.
+
+Evidence rule (see ``sim/devices/base.py`` and ``docs/36``): this module
+carries **no modelled register behaviour** beyond optional read-back of
+values written to registers the header proves plain read/write. Every access
+is recorded -- address, window-relative offset, size, direction,
+value-if-known, PC, virtual time, window label and register name -- and the
+observer then **declines** the access, so the machine's existing
+device-map/lenient/unknown path serves it exactly as before. A read value is
+never invented: an observe-only read records ``value: null`` and returns
+``None``; a storage read before any write is declined the same way.
+
+Modes
+=====
+
+``observe-only`` (default)
+ Read and write are recorded and declined. Nothing changes.
+
+``storage``
+ A declared register whose header access type is plain ``rw`` (or ``wo``)
+ stores the last written value; a later read of a register that has been
+ written returns that value. Undeclared offsets, ``ro`` registers and
+ never-written storage registers are still declined.
+
+Register specs come from the profile (or a runtime window list)::
+
+ "0x24": {"name": "NV_PFB_PRI_MMU_TLB_CTRL",
+ "width": 4, "access": "rw",
+ "fields": {"L2_SIZE": [1, 0], ...}}
+
+``fields`` is optional and only used to decode known values in the trace
+(``hi``/``lo`` bit positions, header field names); no field semantics are
+implied.
+
+The access record is the unit of evidence. It lives in
+:attr:`ObservedWindow.accesses` for the live run and is exported through the
+``greeneye-gmmu-fbif-trace/1`` document built by :func:`build_trace`;
+:meth:`ObservedWindow.state` deliberately carries only counters and storage
+values so a device snapshot stays small.
+
+Nothing here is a silicon claim and nothing here authorises hardware: these
+windows are simulator-only observability for the GMMU/FBIF configuration
+surface (``docs/42-GMMU-FBIF-OBSERVABILITY.md``).
+"""
+from __future__ import annotations
+
+from .base import Device
+
+#: Accepted observer modes.
+MODES = ("observe-only", "storage")
+#: Accepted per-register access kinds (header access type, plain meaning).
+ACCESS_KINDS = ("rw", "ro", "wo")
+#: Schema tag for the GMMU/FBIF trace document.
+TRACE_SCHEMA = "greeneye-gmmu-fbif-trace/1"
+
+DEFAULT_MAX_ACCESS_LOG = 65536
+
+
+def _int(value, default=None):
+ if value is None:
+ return default
+ if isinstance(value, str):
+ return int(value, 0)
+ return int(value)
+
+
+def _normalise_spec(offset, spec) -> dict:
+ """Accept ``"NAME"`` or ``{name, width, access, fields}`` register specs."""
+ if isinstance(spec, str):
+ spec = {"name": spec}
+ if not isinstance(spec, dict) or not spec.get("name"):
+ raise ValueError(f"observer register {offset:#x}: spec needs a name")
+ access = spec.get("access", "rw")
+ if access not in ACCESS_KINDS:
+ raise ValueError(f"observer register {offset:#x}: unknown access {access!r}")
+ width = int(spec.get("width", 4))
+ if width not in (1, 2, 4, 8):
+ raise ValueError(f"observer register {offset:#x}: unsupported width {width}")
+ fields = {}
+ for name, span in (spec.get("fields") or {}).items():
+ if isinstance(span, str):
+ hi, lo = span.split(":")
+ hi, lo = int(hi, 0), int(lo, 0)
+ else:
+ hi, lo = int(span[0]), int(span[1])
+ if not 0 <= lo <= hi <= 8 * width - 1:
+ raise ValueError(
+ f"observer register {offset:#x}: bad field {name} {hi}:{lo}")
+ fields[str(name)] = (hi, lo)
+ return {"name": str(spec["name"]), "width": width,
+ "access": access, "fields": fields}
+
+
+class ObservedWindow(Device):
+ """One observe-only (or storage) MMIO window; a :class:`~sim.devices.base.Device`.
+
+ ``clock`` is an optional zero-argument callable returning virtual time
+ (``Machine.insn_count`` follows the existing ``PtimerBlock`` pattern).
+ The counter advances at :meth:`sim.machine.Machine.run` boundaries, so all
+ accesses inside one bounded run share the run-start value; that granularity
+ is documented, not hidden.
+ """
+
+ name = "observe"
+ #: Opt-in: the registry forwards ``pc=`` to devices that set this.
+ wants_pc = True
+
+ def __init__(self, *, base: int = 0, size: int = 0x1000, label: str | None = None,
+ mode: str = "observe-only", registers=None, clock=None,
+ evidence: str = "", max_access_log: int = DEFAULT_MAX_ACCESS_LOG):
+ super().__init__(base=base, size=size)
+ self.label = str(label or "observe")
+ if mode not in MODES:
+ raise ValueError(f"observer window {self.label}: unknown mode {mode!r}")
+ self.mode = mode
+ self.evidence = str(evidence)
+ self.clock = clock
+ self.registers: dict[int, dict] = {}
+ for off, spec in (registers or {}).items():
+ self.registers[_int(off)] = _normalise_spec(_int(off), spec)
+ self.max_access_log = max(0, int(max_access_log))
+ self.accesses: list[dict] = []
+ self.dropped = 0
+ self.counts = {"r": 0, "w": 0, "served": 0, "declined": 0}
+ self.written: dict[int, int] = {}
+
+ # -- recording ---------------------------------------------------------
+
+ def _record(self, offset: int, size: int, rw: str, value, served: bool,
+ pc: int) -> None:
+ spec = self.registers.get(offset)
+ entry = {
+ "addr": self.base + offset,
+ "offset": offset,
+ "size": size,
+ "rw": rw,
+ "value": value,
+ "pc": pc,
+ "vt": self.clock() if self.clock is not None else None,
+ "window": self.label,
+ "register": spec["name"] if spec is not None else None,
+ "served": bool(served),
+ }
+ if value is not None and spec is not None and spec["fields"]:
+ entry["decoded"] = {
+ name: (value >> lo) & ((1 << (hi - lo + 1)) - 1)
+ for name, (hi, lo) in spec["fields"].items()
+ }
+ self.counts[rw] += 1
+ self.counts["served" if served else "declined"] += 1
+ if len(self.accesses) < self.max_access_log:
+ self.accesses.append(entry)
+ else:
+ self.dropped += 1
+
+ # -- guest-visible semantics ------------------------------------------
+
+ def read(self, offset: int, size: int, pc: int = 0) -> int | None:
+ spec = self.registers.get(offset)
+ value = None
+ served = False
+ if (self.mode == "storage" and spec is not None
+ and spec["width"] == size and spec["access"] == "rw"
+ and offset in self.written):
+ value = self.written[offset]
+ served = True
+ self._record(offset, size, "r", value, served, pc)
+ return value
+
+ def write(self, offset: int, size: int, value: int, pc: int = 0) -> bool:
+ spec = self.registers.get(offset)
+ served = False
+ if (self.mode == "storage" and spec is not None
+ and spec["width"] == size and spec["access"] in ("rw", "wo")):
+ mask = (1 << (8 * size)) - 1
+ self.written[offset] = value & mask
+ served = True
+ self._record(offset, size, "w", value & ((1 << (8 * size)) - 1), served, pc)
+ return served
+
+ def peek(self, offset: int, size: int) -> int | None:
+ spec = self.registers.get(offset)
+ if (self.mode == "storage" and spec is not None
+ and spec["width"] == size and spec["access"] == "rw"
+ and offset in self.written):
+ return self.written[offset]
+ return None
+
+ # -- state -------------------------------------------------------------
+
+ def state(self) -> dict:
+ return {
+ "label": self.label,
+ "mode": self.mode,
+ "counts": dict(self.counts),
+ "written": {str(k): v for k, v in sorted(self.written.items())},
+ "dropped_accesses": self.dropped,
+ }
+
+ def load_state(self, data: dict) -> None:
+ counts = data.get("counts", {})
+ self.counts = {k: int(counts.get(k, 0)) for k in ("r", "w", "served", "declined")}
+ self.written = {int(k): int(v) for k, v in data.get("written", {}).items()}
+ self.dropped = int(data.get("dropped_accesses", 0))
+
+ def reset(self) -> None:
+ self.accesses.clear()
+ self.dropped = 0
+ self.counts = {"r": 0, "w": 0, "served": 0, "declined": 0}
+ self.written.clear()
+
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
+ return (f"ObservedWindow({self.label} @ {self.base:#x}+{self.size:#x}, "
+ f"{self.mode}, {len(self.registers)} registers)")
+
+
+def windows_from_section(section: dict) -> list[dict]:
+ """Expand a profile observer section into per-window declarations.
+
+ A window entry may carry ``count`` and ``stride`` (header-proven register
+ instances, e.g. the 16 FBPA partitions); labels get an ``-<i>`` suffix
+ when more than one instance is declared. Window-level ``mode`` and
+ ``registers`` override the section-level values.
+ """
+ section_regs = section.get("registers") or {}
+ section_mode = section.get("mode", "observe-only")
+ section_label = section.get("label", "observe")
+ out: list[dict] = []
+ for win in section.get("windows", ()):
+ base = _int(win["base"])
+ size = _int(win.get("size", 0x1000))
+ count = int(win.get("count", 1))
+ stride = _int(win.get("stride", size))
+ label = str(win.get("label", section_label))
+ mode = win.get("mode", section_mode)
+ regs = win.get("registers", section_regs)
+ if count < 1:
+ raise ValueError(f"observer window {label}: count must be >= 1")
+ for i in range(count):
+ out.append({
+ "base": base + i * stride,
+ "size": size,
+ "label": label if count == 1 else f"{label}-{i}",
+ "mode": mode,
+ "registers": regs,
+ })
+ return out
+
+
+def parse_window_spec(text: str) -> dict:
+ """Parse ``BASE:SIZE[:LABEL[:MODE]]`` for a runtime observer window.
+
+ Bases/sizes accept ``0x`` forms. This is the escape hatch for windows
+ whose base cannot be established from evidence yet (docs/42); it never
+ invents a value, it only records accesses.
+ """
+ parts = str(text).split(":")
+ if len(parts) < 2:
+ raise ValueError(f"observer window spec {text!r}: expected BASE:SIZE[:LABEL[:MODE]]")
+ base, size = _int(parts[0]), _int(parts[1])
+ if base < 0 or size <= 0:
+ raise ValueError(f"observer window spec {text!r}: bad base/size")
+ label = parts[2] if len(parts) > 2 and parts[2] else "runtime-observe"
+ mode = parts[3] if len(parts) > 3 and parts[3] else "observe-only"
+ if mode not in MODES:
+ raise ValueError(f"observer window spec {text!r}: unknown mode {mode!r}")
+ return {"base": base, "size": size, "label": label, "mode": mode, "registers": {}}
+
+
+def collect_observations(registry) -> list[ObservedWindow]:
+ """All observer windows in a device registry, in registration order."""
+ return [d for d in getattr(registry, "devices", ()) if isinstance(d, ObservedWindow)]
+
+
+def _counts(entries) -> dict:
+ out = {"r": 0, "w": 0, "served": 0}
+ for entry in entries:
+ out[entry["rw"]] += 1
+ if entry["served"]:
+ out["served"] += 1
+ return out
+
+
+def _decorate(entry: dict, resolve) -> dict:
+ out = dict(entry)
+ out["addr"] = hex(entry["addr"])
+ out["offset"] = hex(entry["offset"])
+ if entry["value"] is not None:
+ out["value"] = hex(entry["value"])
+ if entry["pc"] is not None:
+ out["pc"] = hex(entry["pc"])
+ out["pc_symbol"] = None
+ if resolve is not None and entry["pc"] is not None:
+ out["pc_symbol"] = resolve(entry["pc"])
+ return out
+
+
+def build_trace(registry, *, image_sha256: str | None = None,
+ profile: str | None = None, profile_name: str | None = None,
+ symbols_path: str | None = None, resolve=None) -> dict:
+ """Build the ``greeneye-gmmu-fbif-trace/1`` document.
+
+ Accesses are grouped per window and per declared register; offsets inside
+ an observed window that the profile does not name go to ``unclassified``
+ (recorded, never guessed). ``resolve`` is an optional ``pc -> str``
+ callable (``SymbolDB``-backed in :mod:`sim.gsp_boot`).
+
+ Truncation is never silent: a window's access log is capped at
+ ``ObservedWindow.max_access_log`` entries (default 65536);
+ ``dropped_accesses`` reports the accesses beyond the cap in each window
+ and in the summary, and the summary separates ``accesses`` (total
+ observed) from ``retained_accesses`` (present in this document).
+ """
+ windows = []
+ total = {"windows": 0, "windows_touched": 0, "accesses": 0,
+ "retained_accesses": 0, "dropped_accesses": 0,
+ "reads": 0, "writes": 0, "served": 0, "registers_touched": 0}
+ for dev in collect_observations(registry):
+ total["windows"] += 1
+ win = {
+ "label": dev.label,
+ "base": hex(dev.base),
+ "size": hex(dev.size),
+ "mode": dev.mode,
+ "evidence": dev.evidence,
+ "counts": dict(dev.counts),
+ "dropped_accesses": dev.dropped,
+ "retained_accesses": len(dev.accesses),
+ "registers": [],
+ "unclassified": {"counts": {"r": 0, "w": 0, "served": 0},
+ "accesses": []},
+ }
+ grouped: dict[int, list] = {}
+ unclassified: list = []
+ for entry in dev.accesses:
+ if entry["offset"] in dev.registers:
+ grouped.setdefault(entry["offset"], []).append(entry)
+ else:
+ unclassified.append(_decorate(entry, resolve))
+ for offset in sorted(grouped):
+ spec = dev.registers[offset]
+ entries = [_decorate(e, resolve) for e in grouped[offset]]
+ win["registers"].append({
+ "offset": hex(offset),
+ "name": spec["name"],
+ "access": spec["access"],
+ "counts": _counts(entries),
+ "accesses": entries,
+ })
+ win["unclassified"]["accesses"] = unclassified
+ win["unclassified"]["counts"] = _counts(unclassified)
+ observed = dev.counts["r"] + dev.counts["w"]
+ total["dropped_accesses"] += dev.dropped
+ if observed:
+ total["windows_touched"] += 1
+ total["accesses"] += observed
+ total["retained_accesses"] += len(dev.accesses)
+ total["reads"] += dev.counts["r"]
+ total["writes"] += dev.counts["w"]
+ total["served"] += dev.counts["served"]
+ total["registers_touched"] += len(grouped)
+ windows.append(win)
+ doc = {
+ "schema": TRACE_SCHEMA,
+ "image_sha256": image_sha256,
+ "profile": profile,
+ "profile_name": profile_name,
+ "symbols": symbols_path,
+ "summary": total,
+ "windows": windows,
+ }
+ return doc
diff --git a/debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py b/debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py
new file mode 100644
index 0000000..24381eb
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/sim/gmmu_pte.py
@@ -0,0 +1,988 @@
+"""Clean-room decoder for the Turing (TU104) GMMU page-table entry formats.
+
+This module decodes *captured* GMMU page-table data. It is a decoder, not a
+page-table walker: it never dereferences a decoded address and it never fills
+a gap with a guess. Every named field below has an exact bit position taken
+from the Turing GMMU format definitions; the page-tree geometry (depth -> VA
+bits, index width, entry size, page sizes) is taken from the driver's own
+page-tree HAL for Turing, which uses the VER2 entry encoding.
+
+The write-up, the field tables and the uncertainty notes live in
+``docs/43-GMMU-PTE-DECODER.md``.
+
+Formats shipped (evidence: the Turing GMMU register-field definitions and the
+driver's page-table encoder behaviour):
+
+* ``ver2`` -- the 8-byte VER2 leaf PTE and directory PDE, the encoding the
+ Turing driver programs (``NV_MMU_VER2_PTE`` / ``NV_MMU_VER2_PDE``).
+* ``ver2_dual`` -- the 16-byte dual PDE0 slot used for mixed 64K/4K page
+ tables (``NV_MMU_VER2_DUAL_PDE``): a BIG half (64K) and a SMALL half (4K)
+ in one 16-byte entry. The BIG address field spans bits 53:4 and stores
+ the address shifted right by 8; the format also names bit 5 NO_ATS.
+ Depth-3 page-table allocations are 4 KB aligned, so address bit 9 (the
+ overlapping bit) is zero in a valid pointer; the decoder reports
+ ``no_ats`` from bit 5 and excludes that bit from the BIG address.
+
+Known unsupported encodings (explicit ``E_UNSUPPORTED``, never guessed): the
+older non-VER2 PTE/PDE encoding and the "NEW"/VER1 encodings that are also
+described by the same register definitions but are not used by the Turing
+driver's page-tree HAL. See ``docs/43-GMMU-PTE-DECODER.md`` section 6.
+
+Reserved-bit policy: any set bit that no named field of the decoded role
+covers is reported in ``reserved_bits`` / ``reserved_mask`` -- never silently
+dropped and never given an invented meaning.
+
+Rule: decode captured data only; validate against a capture before trusting.
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from typing import Any
+
+# ---------------------------------------------------------------------------
+# Errors
+# ---------------------------------------------------------------------------
+
+E_UNSUPPORTED = "E_UNSUPPORTED"
+E_TRUNCATED = "E_TRUNCATED"
+E_BAD_VALUE = "E_BAD_VALUE"
+
+
+class GmmuDecodeError(ValueError):
+ """A decode request that the format evidence cannot answer."""
+
+ def __init__(self, code: str, message: str):
+ super().__init__(f"{code}: {message}")
+ self.code = code
+ self.message = message
+
+
+# ---------------------------------------------------------------------------
+# Encoding families
+# ---------------------------------------------------------------------------
+
+KIND_VER2 = "ver2"
+KIND_VER2_DUAL = "ver2_dual"
+
+#: Entry-encoding families this decoder ships.
+SUPPORTED_KINDS = (KIND_VER2, KIND_VER2_DUAL)
+
+#: Page-tree depths the evidence supports, root (0) -> leaf (4).
+SUPPORTED_LEVELS = (0, 1, 2, 3, 4)
+
+#: Depth -> hardware level, VA bits, index width and entry size.
+#:
+#: Evidence: the driver's page-tree HAL for Turing inherits the Pascal/Volta
+#: geometry; the comments in that HAL state the mapping below. Depths 0-2
+#: are pointer directories, depth 3 is the PDE0 slot (a dual 64K/4K entry,
+#: or a 2 MB leaf PTE), depth 4 is the 4K/64K leaf page table.
+LEVELS = (
+ {"depth": 0, "hw": "pde3", "va_bits": (48, 47), "index_bits": 2,
+ "entry_size": 8, "role": "pde"},
+ {"depth": 1, "hw": "pde2", "va_bits": (46, 38), "index_bits": 9,
+ "entry_size": 8, "role": "pde"},
+ {"depth": 2, "hw": "pde1", "va_bits": (37, 29), "index_bits": 9,
+ "entry_size": 8, "role": "pde"},
+ {"depth": 3, "hw": "pde0", "va_bits": (28, 21), "index_bits": 8,
+ "entry_size": 16, "role": "pde_or_2m_pte"},
+ {"depth": 4, "hw": "pte", "va_bits": (20, 12), "index_bits": 9,
+ "entry_size": 8, "role": "pte"},
+)
+
+#: Page sizes the format evidence supports, and the depth their entries live at.
+PAGE_SIZES = ("4k", "64k", "2m")
+PAGE_TABLE_DEPTH = {"2m": 3, "4k": 4, "64k": 4}
+
+#: PTE aperture field (bits 2:1).
+APERTURE_NAMES_PTE = {
+ 0: "video_memory",
+ 1: "peer_memory",
+ 2: "system_coherent_memory",
+ 3: "system_non_coherent_memory",
+}
+
+#: PDE aperture field (bits 2:1). Note aperture 0 is INVALID here, while for
+#: a leaf PTE aperture 0 is video memory -- the two entry roles differ.
+APERTURE_NAMES_PDE = {
+ 0: "invalid",
+ 1: "video_memory",
+ 2: "system_coherent_memory",
+ 3: "system_non_coherent_memory",
+}
+
+#: PTE KIND field (bits 63:56), numeric IDs from the Turing GMMU format
+#: definitions. Values not listed are reported as unknown, never guessed.
+PTE_KIND_NAMES = {
+ 0x00: 'PITCH',
+ 0x01: 'Z16',
+ 0x02: 'S8',
+ 0x03: 'S8Z24',
+ 0x04: 'ZF32_X24S8',
+ 0x05: 'Z24S8',
+ 0x06: 'GENERIC_MEMORY',
+ 0x07: 'INVALID',
+ 0x08: 'GENERIC_MEMORY_COMPRESSIBLE',
+ 0x09: 'GENERIC_MEMORY_COMPRESSIBLE_DISABLE_PLC',
+ 0x0A: 'S8_COMPRESSIBLE_DISABLE_PLC',
+ 0x0B: 'Z16_COMPRESSIBLE_DISABLE_PLC',
+ 0x0C: 'S8Z24_COMPRESSIBLE_DISABLE_PLC',
+ 0x0D: 'ZF32_X24S8_COMPRESSIBLE_DISABLE_PLC',
+ 0x0E: 'Z24S8_COMPRESSIBLE_DISABLE_PLC',
+ 0x0F: 'SMSKED_MESSAGE',
+ 0x10: 'Z16_MS2_2Z',
+ 0x11: 'Z16_MS2_2C',
+ 0x12: 'S8Z24_1Z',
+ 0x13: 'S8Z24_MS2_1Z',
+ 0x14: 'S8Z24_MS4_1Z',
+ 0x15: 'S8Z24_MS8_1Z',
+ 0x16: 'S8Z24_MS16_1Z',
+ 0x17: 'S8Z24_2CZ',
+ 0x18: 'S8Z24_MS2_2CZ',
+ 0x19: 'S8Z24_MS4_2CZ',
+ 0x1A: 'S8Z24_MS8_2CZ',
+ 0x1B: 'S8Z24_MS16_2CZ',
+ 0x1C: 'S8Z24_2CS',
+ 0x1D: 'S8Z24_MS2_2CS',
+ 0x1E: 'S8Z24_MS4_2CS',
+ 0x1F: 'S8Z24_MS8_2CS',
+ 0x20: 'S8Z24_MS16_2CS',
+ 0x21: 'S8Z24_4CSZV',
+ 0x22: 'S8Z24_MS2_4CSZV',
+ 0x23: 'S8Z24_MS4_4CSZV',
+ 0x24: 'S8Z24_MS8_4CSZV',
+ 0x25: 'S8Z24_MS16_4CSZV',
+ 0x26: 'V8Z24_MS4_VC12',
+ 0x27: 'V8Z24_MS4_VC4',
+ 0x28: 'V8Z24_MS8_VC8',
+ 0x29: 'V8Z24_MS8_VC24',
+ 0x2A: 'Z16_2C',
+ 0x2B: 'S8_2S',
+ 0x2C: 'C32_MS4_4CBRA',
+ 0x2D: 'C64_MS4_4CBRA',
+ 0x2E: 'V8Z24_MS4_VC12_1ZV',
+ 0x2F: 'V8Z24_MS4_VC4_1ZV',
+ 0x30: 'V8Z24_MS8_VC8_1ZV',
+ 0x31: 'V8Z24_MS8_VC24_1ZV',
+ 0x32: 'V8Z24_MS4_VC12_2CS',
+ 0x33: 'V8Z24_MS4_VC4_2CS',
+ 0x34: 'V8Z24_MS8_VC8_2CS',
+ 0x35: 'V8Z24_MS8_VC24_2CS',
+ 0x36: 'Z16_2CZ',
+ 0x37: 'Z16_MS2_2CZ',
+ 0x38: 'Z16_MS4_2CZ',
+ 0x39: 'Z16_MS8_2CZ',
+ 0x3A: 'V8Z24_MS4_VC12_2CZV',
+ 0x3B: 'V8Z24_MS4_VC4_2CZV',
+ 0x3C: 'V8Z24_MS8_VC8_2CZV',
+ 0x3D: 'V8Z24_MS8_VC24_2CZV',
+ 0x3E: 'V8Z24_MS4_VC12_2ZV',
+ 0x3F: 'V8Z24_MS4_VC4_2ZV',
+ 0x40: 'V8Z24_MS8_VC8_2ZV',
+ 0x41: 'V8Z24_MS8_VC24_2ZV',
+ 0x42: 'V8Z24_MS4_VC12_4CSZV',
+ 0x43: 'V8Z24_MS4_VC4_4CSZV',
+ 0x44: 'V8Z24_MS8_VC8_4CSZV',
+ 0x45: 'V8Z24_MS8_VC24_4CSZV',
+ 0x46: 'Z16_MS8_2C',
+ 0x47: 'Z24S8_1Z',
+ 0x48: 'Z24S8_MS2_1Z',
+ 0x49: 'Z24S8_MS4_1Z',
+ 0x4A: 'Z24S8_MS8_1Z',
+ 0x4B: 'Z24S8_MS16_1Z',
+ 0x4C: 'Z24S8_2CS',
+ 0x4D: 'Z24S8_MS2_2CS',
+ 0x4E: 'Z24S8_MS4_2CS',
+ 0x4F: 'Z24S8_MS8_2CS',
+ 0x50: 'Z24S8_MS16_2CS',
+ 0x51: 'Z24S8_2CZ',
+ 0x52: 'Z24S8_MS2_2CZ',
+ 0x53: 'Z24S8_MS4_2CZ',
+ 0x54: 'Z24S8_MS8_2CZ',
+ 0x55: 'Z24S8_MS16_2CZ',
+ 0x56: 'Z24S8_4CSZV',
+ 0x57: 'Z24S8_MS2_4CSZV',
+ 0x58: 'Z24S8_MS4_4CSZV',
+ 0x59: 'Z24S8_MS8_4CSZV',
+ 0x5A: 'Z24S8_MS16_4CSZV',
+ 0x5B: 'Z24V8_MS4_VC12',
+ 0x5C: 'Z24V8_MS4_VC4',
+ 0x5D: 'Z24V8_MS8_VC8',
+ 0x5E: 'Z24V8_MS8_VC24',
+ 0x5F: 'Z16_MS16_2CZ',
+ 0x60: 'Z16_MS4_2Z',
+ 0x61: 'Z16_MS8_2Z',
+ 0x62: 'Z16_MS16_2Z',
+ 0x63: 'Z24V8_MS4_VC12_1ZV',
+ 0x64: 'Z24V8_MS4_VC4_1ZV',
+ 0x65: 'Z24V8_MS8_VC8_1ZV',
+ 0x66: 'Z24V8_MS8_VC24_1ZV',
+ 0x67: 'Z24V8_MS4_VC12_2CS',
+ 0x68: 'Z24V8_MS4_VC4_2CS',
+ 0x69: 'Z24V8_MS8_VC8_2CS',
+ 0x6A: 'Z24V8_MS8_VC24_2CS',
+ 0x6B: 'Z16_2Z',
+ 0x6C: 'Z16_MS16_2C',
+ 0x6F: 'Z24V8_MS4_VC12_2CZV',
+ 0x70: 'Z24V8_MS4_VC4_2CZV',
+ 0x71: 'Z24V8_MS8_VC8_2CZV',
+ 0x72: 'Z24V8_MS8_VC24_2CZV',
+ 0x73: 'Z24V8_MS4_VC12_2ZV',
+ 0x74: 'Z24V8_MS4_VC4_2ZV',
+ 0x75: 'Z24V8_MS8_VC8_2ZV',
+ 0x76: 'Z24V8_MS8_VC24_2ZV',
+ 0x77: 'Z24V8_MS4_VC12_4CSZV',
+ 0x78: 'Z24V8_MS4_VC4_4CSZV',
+ 0x79: 'Z24V8_MS8_VC8_4CSZV',
+ 0x7A: 'Z24V8_MS8_VC24_4CSZV',
+ 0x7B: 'ZF32',
+ 0x7C: 'ZF32_1Z',
+ 0x7D: 'ZF32_MS2_1Z',
+ 0x7E: 'ZF32_MS4_1Z',
+ 0x7F: 'ZF32_MS8_1Z',
+ 0x80: 'ZF32_MS16_1Z',
+ 0x81: 'ZF32_2CS',
+ 0x82: 'ZF32_MS2_2CS',
+ 0x83: 'ZF32_MS4_2CS',
+ 0x84: 'ZF32_MS8_2CS',
+ 0x85: 'ZF32_MS16_2CS',
+ 0x86: 'ZF32_2CZ',
+ 0x87: 'ZF32_MS2_2CZ',
+ 0x88: 'ZF32_MS4_2CZ',
+ 0x89: 'ZF32_MS8_2CZ',
+ 0x8A: 'ZF32_MS16_2CZ',
+ 0x8B: 'X8Z24_X16V8S8_MS4_VC12',
+ 0x8C: 'X8Z24_X16V8S8_MS4_VC4',
+ 0x8D: 'X8Z24_X16V8S8_MS8_VC8',
+ 0x8E: 'X8Z24_X16V8S8_MS8_VC24',
+ 0x8F: 'X8Z24_X16V8S8_MS4_VC12_1CS',
+ 0x90: 'X8Z24_X16V8S8_MS4_VC4_1CS',
+ 0x91: 'X8Z24_X16V8S8_MS8_VC8_1CS',
+ 0x92: 'X8Z24_X16V8S8_MS8_VC24_1CS',
+ 0x97: 'X8Z24_X16V8S8_MS4_VC12_1ZV',
+ 0x98: 'X8Z24_X16V8S8_MS4_VC4_1ZV',
+ 0x99: 'X8Z24_X16V8S8_MS8_VC8_1ZV',
+ 0x9A: 'X8Z24_X16V8S8_MS8_VC24_1ZV',
+ 0x9B: 'X8Z24_X16V8S8_MS4_VC12_1CZV',
+ 0x9C: 'X8Z24_X16V8S8_MS4_VC4_1CZV',
+ 0x9D: 'X8Z24_X16V8S8_MS8_VC8_1CZV',
+ 0x9E: 'X8Z24_X16V8S8_MS8_VC24_1CZV',
+ 0x9F: 'X8Z24_X16V8S8_MS4_VC12_2CS',
+ 0xA0: 'X8Z24_X16V8S8_MS4_VC4_2CS',
+ 0xA1: 'X8Z24_X16V8S8_MS8_VC8_2CS',
+ 0xA2: 'X8Z24_X16V8S8_MS8_VC24_2CS',
+ 0xA3: 'X8Z24_X16V8S8_MS4_VC12_2CSZV',
+ 0xA4: 'X8Z24_X16V8S8_MS4_VC4_2CSZV',
+ 0xA5: 'X8Z24_X16V8S8_MS8_VC8_2CSZV',
+ 0xA6: 'X8Z24_X16V8S8_MS8_VC24_2CSZV',
+ 0xA7: 'ZF32_X16V8S8_MS4_VC12',
+ 0xA8: 'ZF32_X16V8S8_MS4_VC4',
+ 0xA9: 'ZF32_X16V8S8_MS8_VC8',
+ 0xAA: 'ZF32_X16V8S8_MS8_VC24',
+ 0xAB: 'ZF32_X16V8S8_MS4_VC12_1CS',
+ 0xAC: 'ZF32_X16V8S8_MS4_VC4_1CS',
+ 0xAD: 'ZF32_X16V8S8_MS8_VC8_1CS',
+ 0xAE: 'ZF32_X16V8S8_MS8_VC24_1CS',
+ 0xB3: 'ZF32_X16V8S8_MS4_VC12_1ZV',
+ 0xB4: 'ZF32_X16V8S8_MS4_VC4_1ZV',
+ 0xB5: 'ZF32_X16V8S8_MS8_VC8_1ZV',
+ 0xB6: 'ZF32_X16V8S8_MS8_VC24_1ZV',
+ 0xB7: 'ZF32_X16V8S8_MS4_VC12_1CZV',
+ 0xB8: 'ZF32_X16V8S8_MS4_VC4_1CZV',
+ 0xB9: 'ZF32_X16V8S8_MS8_VC8_1CZV',
+ 0xBA: 'ZF32_X16V8S8_MS8_VC24_1CZV',
+ 0xBB: 'ZF32_X16V8S8_MS4_VC12_2CS',
+ 0xBC: 'ZF32_X16V8S8_MS4_VC4_2CS',
+ 0xBD: 'ZF32_X16V8S8_MS8_VC8_2CS',
+ 0xBE: 'ZF32_X16V8S8_MS8_VC24_2CS',
+ 0xBF: 'ZF32_X16V8S8_MS4_VC12_2CSZV',
+ 0xC0: 'ZF32_X16V8S8_MS4_VC4_2CSZV',
+ 0xC1: 'ZF32_X16V8S8_MS8_VC8_2CSZV',
+ 0xC2: 'ZF32_X16V8S8_MS8_VC24_2CSZV',
+ 0xC3: 'Z16_MS4_2C',
+ 0xC4: 'ZF32_X24S8_1CS',
+ 0xC5: 'ZF32_X24S8_MS2_1CS',
+ 0xC6: 'ZF32_X24S8_MS4_1CS',
+ 0xC7: 'ZF32_X24S8_MS8_1CS',
+ 0xC8: 'ZF32_X24S8_MS16_1CS',
+ 0xCB: 'SMHOST_MESSAGE',
+ 0xCC: 'C32_MS2_4CBRA',
+ 0xCD: 'C64_MS2_4CBRA',
+ 0xCE: 'ZF32_X24S8_2CSZV',
+ 0xCF: 'ZF32_X24S8_MS2_2CSZV',
+ 0xD0: 'ZF32_X24S8_MS4_2CSZV',
+ 0xD1: 'ZF32_X24S8_MS8_2CSZV',
+ 0xD2: 'ZF32_X24S8_MS16_2CSZV',
+ 0xD3: 'ZF32_X24S8_2CS',
+ 0xD4: 'ZF32_X24S8_MS2_2CS',
+ 0xD5: 'ZF32_X24S8_MS4_2CS',
+ 0xD6: 'ZF32_X24S8_MS8_2CS',
+ 0xD7: 'ZF32_X24S8_MS16_2CS',
+ 0xD8: 'C32_2C',
+ 0xD9: 'C32_2CBR',
+ 0xDA: 'C32_2CBA',
+ 0xDB: 'C32_2CRA',
+ 0xDC: 'C32_2BRA',
+ 0xDD: 'C32_MS2_2C',
+ 0xDE: 'C32_MS2_2CBR',
+ 0xDF: 'C32_MS4_2C',
+ 0xE0: 'C32_MS4_2CBR',
+ 0xE1: 'C32_MS4_2CBA',
+ 0xE2: 'C32_MS4_2CRA',
+ 0xE3: 'C32_MS4_2BRA',
+ 0xE4: 'C32_MS8_MS16_2C',
+ 0xE5: 'C32_MS8_MS16_2CRA',
+ 0xE6: 'C64_2C',
+ 0xE7: 'C64_2CBR',
+ 0xE8: 'C64_2CBA',
+ 0xE9: 'C64_2CRA',
+ 0xEA: 'C64_2BRA',
+ 0xEB: 'C64_MS2_2C',
+ 0xEC: 'C64_MS2_2CBR',
+ 0xED: 'C64_MS4_2C',
+ 0xEE: 'C64_MS4_2CBR',
+ 0xEF: 'C64_MS4_2CBA',
+ 0xF0: 'C64_MS4_2CRA',
+ 0xF1: 'C64_MS4_2BRA',
+ 0xF2: 'C64_MS8_MS16_2C',
+ 0xF3: 'C64_MS8_MS16_2CRA',
+ 0xF4: 'C128_2C',
+ 0xF5: 'C128_2CR',
+ 0xF6: 'C128_MS2_2C',
+ 0xF7: 'C128_MS2_2CR',
+ 0xF8: 'C128_MS4_2C',
+ 0xF9: 'C128_MS4_2CR',
+ 0xFA: 'C128_MS8_MS16_2C',
+ 0xFB: 'C128_MS8_MS16_2CR',
+ 0xFC: 'X8C24',
+ 0xFD: 'PITCH_NO_SWIZZLE',
+ 0xFE: 'GENERIC_16BX2',
+}
+
+# ---------------------------------------------------------------------------
+# Field positions (lo, hi), inclusive. A field is only listed when the
+# format definitions prove its position; there are no inferred fields.
+# ---------------------------------------------------------------------------
+
+_VER2_PTE_FIELDS = (
+ ("valid", 0, 0),
+ ("aperture", 1, 2),
+ ("volatile", 3, 3),
+ ("encrypted", 4, 4),
+ ("privilege", 5, 5),
+ ("read_only", 6, 6),
+ ("atomic_disable", 7, 7),
+ ("address_sys", 8, 53),
+ ("address_vid", 8, 32),
+ ("peer_id", 33, 35),
+ ("comptagline", 36, 55),
+ ("kind", 56, 63),
+)
+
+_VER2_PDE_FIELDS = (
+ ("is_pde_pointer", 0, 0),
+ ("aperture", 1, 2),
+ ("volatile", 3, 3),
+ ("no_ats", 5, 5),
+ ("address_sys", 8, 53),
+ ("address_vid", 8, 32),
+ ("peer_id", 33, 35),
+)
+
+# The dual PDE is a 16-byte (128-bit) entry: the BIG half is the low 64 bits,
+# the SMALL half is the high 64 bits. Bit 0 (IS_PDE) applies to the whole
+# entry and is stored in the low bits.
+_VER2_DUAL_PDE_FIELDS = (
+ ("is_pde_pointer", 0, 0),
+ ("aperture_big", 1, 2),
+ ("volatile_big", 3, 3),
+ ("no_ats", 5, 5),
+ ("address_big_sys", 4, 53),
+ ("address_big_vid", 4, 32),
+ ("peer_id_big", 33, 35),
+ ("aperture_small", 65, 66),
+ ("volatile_small", 67, 67),
+ ("address_small_sys", 72, 117),
+ ("address_small_vid", 72, 96),
+ ("peer_id_small", 97, 99),
+)
+
+# Address field shifts from the format definitions.
+_PTE_ADDRESS_SHIFT = 12
+_PDE_ADDRESS_SHIFT = 12
+_DUAL_ADDRESS_SHIFT = 12
+_DUAL_ADDRESS_BIG_SHIFT = 8
+
+
+def _field(value: int, lo: int, hi: int) -> int:
+ return (value >> lo) & ((1 << (hi - lo + 1)) - 1)
+
+
+def _named_mask(fields) -> int:
+ mask = 0
+ for _name, lo, hi in fields:
+ mask |= ((1 << (hi - lo + 1)) - 1) << lo
+ return mask
+
+
+def _reserved(value: int, fields, width: int) -> tuple[list[int], int]:
+ """Set bits not covered by any named field: (bit list, mask)."""
+ mask = _named_mask(fields)
+ leftover = value & ~mask
+ bits = [b for b in range(width) if (leftover >> b) & 1]
+ return bits, leftover
+
+
+def _check_value(value: int, width: int, what: str) -> None:
+ if not isinstance(value, int) or isinstance(value, bool):
+ raise GmmuDecodeError(E_BAD_VALUE, f"{what} must be an int")
+ if value < 0 or value >= (1 << width):
+ raise GmmuDecodeError(
+ E_BAD_VALUE, f"{what} {value:#x} does not fit in {width} bits")
+
+
+def _check_kind(kind: str, allowed, what: str) -> None:
+ if kind not in allowed:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"{what} {kind!r} is not supported (supported: {', '.join(allowed)})")
+
+
+def _check_level(level, allowed, what: str) -> None:
+ if level is None:
+ return
+ if not isinstance(level, int) or isinstance(level, bool):
+ raise GmmuDecodeError(E_BAD_VALUE, f"{what} level must be an int or None")
+ if level not in allowed:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"{what} level {level} is not supported "
+ f"(supported: {', '.join(str(x) for x in allowed)})")
+
+
+def _page_size_for_level(level) -> str | None:
+ if level == 3:
+ return "2m"
+ if level == 4:
+ return "4k_or_64k"
+ return None
+
+
+# ---------------------------------------------------------------------------
+# Leaf PTE (VER2, 8 bytes)
+# ---------------------------------------------------------------------------
+
+def decode_pte(value: int, *, pte_kind: str = KIND_VER2,
+ level: int | None = None) -> dict:
+ """Decode one 8-byte VER2 leaf PTE.
+
+ ``pte_kind`` selects the entry encoding; only ``ver2`` is a leaf encoding.
+ ``level``, when given, pins the page-tree depth (3 = 2 MB leaf, 4 =
+ 4 KB/64 KB leaf) and is otherwise reported as undetermined.
+ """
+ _check_kind(pte_kind, SUPPORTED_KINDS, "pte_kind")
+ if pte_kind != KIND_VER2:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED, f"{pte_kind} is a directory entry, not a leaf PTE")
+ _check_value(value, 64, "PTE value")
+ _check_level(level, (3, 4), "PTE")
+
+ valid = bool(_field(value, 0, 0))
+ aperture = _field(value, 1, 2)
+ aperture_name = APERTURE_NAMES_PTE[aperture]
+ address_sys = _field(value, 8, 53)
+ address_vid = _field(value, 8, 32)
+ peer_id = _field(value, 33, 35)
+ comptagline = _field(value, 36, 55)
+ kind = _field(value, 56, 63)
+
+ if aperture_name == "system_coherent_memory" or \
+ aperture_name == "system_non_coherent_memory":
+ address = address_sys << _PTE_ADDRESS_SHIFT
+ address_complete = True
+ elif aperture_name in ("video_memory", "peer_memory"):
+ # Bits 36:12 of the physical address are in address_vid. The high
+ # address bits are overloaded into comptagline only for the
+ # documented 47-bit peer case; otherwise comptagline is a
+ # compression tag, so a full address is not claimed here.
+ address = address_vid << _PTE_ADDRESS_SHIFT
+ address_complete = False
+ else: # pragma: no cover - all 4 aperture values are named
+ address = None
+ address_complete = False
+
+ reserved_bits, reserved_mask = _reserved(value, _VER2_PTE_FIELDS, 64)
+ return {
+ "role": "pte",
+ "level": level,
+ "level_candidates": [3, 4] if level is None else [level],
+ "page_size": _page_size_for_level(level),
+ "valid": valid,
+ "aperture": aperture,
+ "aperture_name": aperture_name,
+ "volatile": bool(_field(value, 3, 3)),
+ "encrypted": bool(_field(value, 4, 4)),
+ "privilege": bool(_field(value, 5, 5)),
+ "read_only": bool(_field(value, 6, 6)),
+ "writable": not bool(_field(value, 6, 6)),
+ "atomic_disable": bool(_field(value, 7, 7)),
+ "address": address,
+ "address_complete": address_complete,
+ "address_sys": address_sys,
+ "address_vid": address_vid,
+ "peer_id": peer_id,
+ "comptagline": comptagline,
+ "kind": kind,
+ "kind_name": PTE_KIND_NAMES.get(kind),
+ "kind_known": kind in PTE_KIND_NAMES,
+ "reserved_bits": reserved_bits,
+ "reserved_mask": reserved_mask,
+ }
+
+
+# ---------------------------------------------------------------------------
+# Directory PDE (VER2, 8 bytes)
+# ---------------------------------------------------------------------------
+
+def decode_pde(value: int, *, pde_kind: str = KIND_VER2,
+ level: int | None = None) -> dict:
+ """Decode one 8-byte VER2 directory entry.
+
+ Bit 0 is the leaf/pointer discriminator: the driver's encoder writes 0
+ for a pointer to a lower table and 1 for a 2 MB leaf PTE. A value whose
+ bit 0 is 1 is a leaf, not a directory entry, and is rejected here; use
+ :func:`decode_pte` (or :func:`decode_entry` with ``role='auto'``).
+ """
+ _check_kind(pde_kind, SUPPORTED_KINDS, "pde_kind")
+ if pde_kind != KIND_VER2:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED, f"{pde_kind} is a dual entry, not a single PDE")
+ _check_value(value, 64, "PDE value")
+ _check_level(level, (0, 1, 2, 3), "PDE")
+
+ if _field(value, 0, 0):
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ "PDE bit 0 is set: this is a leaf (2 MB) PTE, not a directory entry")
+
+ aperture = _field(value, 1, 2)
+ aperture_name = APERTURE_NAMES_PDE[aperture]
+ address_sys = _field(value, 8, 53)
+ address_vid = _field(value, 8, 32)
+ peer_id = _field(value, 33, 35)
+
+ if aperture_name in ("system_coherent_memory", "system_non_coherent_memory"):
+ address = address_sys << _PDE_ADDRESS_SHIFT
+ address_complete = True
+ elif aperture_name in ("video_memory", "peer_memory"):
+ address = address_vid << _PDE_ADDRESS_SHIFT
+ address_complete = False
+ else: # aperture 0 = invalid
+ address = None
+ address_complete = False
+
+ reserved_bits, reserved_mask = _reserved(value, _VER2_PDE_FIELDS, 64)
+ return {
+ "role": "pde",
+ "level": level,
+ "level_candidates": [0, 1, 2, 3] if level is None else [level],
+ "is_pde_pointer": True,
+ "is_leaf": False,
+ "aperture": aperture,
+ "aperture_name": aperture_name,
+ "volatile": bool(_field(value, 3, 3)),
+ "no_ats": bool(_field(value, 5, 5)),
+ "address": address,
+ "address_complete": address_complete,
+ "address_sys": address_sys,
+ "address_vid": address_vid,
+ "peer_id": peer_id,
+ "reserved_bits": reserved_bits,
+ "reserved_mask": reserved_mask,
+ }
+
+
+# ---------------------------------------------------------------------------
+# Dual PDE0 slot (VER2, 16 bytes)
+# ---------------------------------------------------------------------------
+
+def decode_dual_pde(value: int, *, pde_kind: str = KIND_VER2_DUAL,
+ level: int | None = None) -> dict:
+ """Decode one 16-byte VER2 dual PDE0 slot (BIG + SMALL halves).
+
+ ``value`` is the 128-bit little-endian slot value: the BIG half is the
+ low 64 bits, the SMALL half the high 64 bits. Bit 0 belongs to the whole
+ slot. The BIG half carries 64K-page-table state, the SMALL half 4K.
+ """
+ _check_kind(pde_kind, SUPPORTED_KINDS, "pde_kind")
+ if pde_kind != KIND_VER2_DUAL:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED, f"{pde_kind} is a single entry, not a dual PDE")
+ _check_value(value, 128, "dual PDE value")
+ _check_level(level, (3,), "dual PDE")
+
+ aperture_big = _field(value, 1, 2)
+ aperture_small = _field(value, 65, 66)
+ name_big = APERTURE_NAMES_PDE[aperture_big]
+ name_small = APERTURE_NAMES_PDE[aperture_small]
+
+ address_big_sys = _field(value, 4, 53)
+ address_big_vid = _field(value, 4, 32)
+ peer_id_big = _field(value, 33, 35)
+ address_small_sys = _field(value, 72, 117)
+ address_small_vid = _field(value, 72, 96)
+ peer_id_small = _field(value, 97, 99)
+
+ # Bit 5 of the entry is named NO_ATS and also lies inside the BIG
+ # address field (bits 53:4), i.e. at field bit 1 (entry bit 5). A valid
+ # depth-3 allocation is 4 KB aligned, so address bit 9 (the overlapping
+ # bit) is zero; exclude it from the reconstructed address and report it
+ # as the flag the definitions name.
+ _NO_ATS_FIELD_MASK = 1 << (5 - 4)
+ if name_big in ("system_coherent_memory", "system_non_coherent_memory"):
+ address_big = (address_big_sys & ~_NO_ATS_FIELD_MASK) << _DUAL_ADDRESS_BIG_SHIFT
+ complete_big = True
+ elif name_big in ("video_memory", "peer_memory"):
+ address_big = (address_big_vid & ~_NO_ATS_FIELD_MASK) << _DUAL_ADDRESS_BIG_SHIFT
+ complete_big = False
+ else:
+ address_big = None
+ complete_big = False
+
+ if name_small in ("system_coherent_memory", "system_non_coherent_memory"):
+ address_small = address_small_sys << _DUAL_ADDRESS_SHIFT
+ complete_small = True
+ elif name_small in ("video_memory", "peer_memory"):
+ address_small = address_small_vid << _DUAL_ADDRESS_SHIFT
+ complete_small = False
+ else:
+ address_small = None
+ complete_small = False
+
+ reserved_bits, reserved_mask = _reserved(value, _VER2_DUAL_PDE_FIELDS, 128)
+ return {
+ "role": "dual_pde",
+ "level": level,
+ "level_candidates": [3] if level is None else [level],
+ "is_pde_pointer": not bool(_field(value, 0, 0)),
+ "is_leaf": bool(_field(value, 0, 0)),
+ "no_ats": bool(_field(value, 5, 5)),
+ "big": {
+ "aperture": aperture_big,
+ "aperture_name": name_big,
+ "volatile": bool(_field(value, 3, 3)),
+ "address": address_big,
+ "address_complete": complete_big,
+ "address_sys": address_big_sys,
+ "address_vid": address_big_vid,
+ "peer_id": peer_id_big,
+ },
+ "small": {
+ "aperture": aperture_small,
+ "aperture_name": name_small,
+ "volatile": bool(_field(value, 67, 67)),
+ "address": address_small,
+ "address_complete": complete_small,
+ "address_sys": address_small_sys,
+ "address_vid": address_small_vid,
+ "peer_id": peer_id_small,
+ },
+ "reserved_bits": reserved_bits,
+ "reserved_mask": reserved_mask,
+ }
+
+
+# ---------------------------------------------------------------------------
+# Role dispatch
+# ---------------------------------------------------------------------------
+
+def decode_entry(value: int, *, kind: str = KIND_VER2, role: str = "auto",
+ level: int | None = None) -> dict:
+ """Decode one entry with an explicit or inferred role.
+
+ ``role`` is one of ``auto``, ``pte``, ``pde``, ``dual_pde``. ``auto``
+ uses the format's own bit-0 discriminator for 8-byte VER2 entries
+ (bit 0 set = leaf PTE, clear = directory PDE); the inferred role is
+ reported with ``role_inferred=True`` and the level stays undetermined
+ unless the caller pins it.
+ """
+ if role == "auto":
+ if kind == KIND_VER2_DUAL:
+ decoded = decode_dual_pde(value, pde_kind=kind, level=level)
+ decoded["role_inferred"] = level is None
+ return decoded
+ _check_value(value, 64, "entry value")
+ if _field(value, 0, 0):
+ decoded = decode_pte(value, pte_kind=kind, level=level)
+ else:
+ decoded = decode_pde(value, pde_kind=kind, level=level)
+ decoded["role_inferred"] = level is None
+ return decoded
+ if role == "pte":
+ return decode_pte(value, pte_kind=kind, level=level)
+ if role == "pde":
+ return decode_pde(value, pde_kind=kind, level=level)
+ if role == "dual_pde":
+ return decode_dual_pde(value, pde_kind=kind, level=level)
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"role {role!r} is not supported (auto, pte, pde, dual_pde)")
+
+
+# ---------------------------------------------------------------------------
+# Table decode
+# ---------------------------------------------------------------------------
+
+def _normalize_levels(levels) -> tuple[tuple[int, ...], int | None]:
+ """Return (allowed levels, pinned level or None) from ``levels``."""
+ if levels is None:
+ return tuple(SUPPORTED_LEVELS), None
+ requested: tuple[int, ...]
+ if isinstance(levels, int) and not isinstance(levels, bool):
+ requested = (levels,)
+ else:
+ try:
+ requested = tuple(int(x) for x in levels)
+ except (TypeError, ValueError):
+ raise GmmuDecodeError(
+ E_BAD_VALUE, "levels must be an int, a sequence of ints, or None")
+ if not requested:
+ raise GmmuDecodeError(E_BAD_VALUE, "levels must not be empty")
+ for level in requested:
+ if level not in SUPPORTED_LEVELS:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"level {level} is not supported "
+ f"(supported: {', '.join(str(x) for x in SUPPORTED_LEVELS)})")
+ pinned = requested[0] if len(set(requested)) == 1 else None
+ return tuple(sorted(set(requested))), pinned
+
+
+def _infer_role(bit0: int, allowed: tuple[int, ...]) -> str:
+ plausible_leaf = [lv for lv in allowed if lv in (3, 4)]
+ plausible_pde = [lv for lv in allowed if lv in (0, 1, 2, 3)]
+ if bit0:
+ if not plausible_leaf:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"entry bit 0 is set (leaf) but levels {allowed} have no leaf level")
+ return "pte"
+ if not plausible_pde:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"entry bit 0 is clear (directory) but levels {allowed} have no directory level")
+ return "pde"
+
+
+def _candidates_for(role: str, allowed: tuple[int, ...]) -> list[int]:
+ if role == "pte":
+ return [lv for lv in allowed if lv in (3, 4)]
+ if role == "pde":
+ return [lv for lv in allowed if lv in (0, 1, 2, 3)]
+ return list(allowed)
+
+
+def decode_table(blob: bytes, *, base_pa: int, entry_size: int = 8,
+ levels=None, kind: str = KIND_VER2) -> list[dict]:
+ """Decode a captured page-table blob into a list of entry dicts.
+
+ ``blob`` is read little-endian, ``entry_size`` bytes per entry.
+ ``base_pa`` is the GPU physical address of the blob's first byte and is
+ reported per entry as ``entry_pa`` (it is not dereferenced).
+
+ ``kind`` selects the entry encoding: ``ver2`` (8-byte PTE/PDE) or
+ ``ver2_dual`` (16-byte dual PDE0). ``entry_size`` must match the kind.
+
+ ``levels`` optionally pins the page-tree depth of this blob (0-4) or a
+ sequence of acceptable depths; ``None`` (the default) uses the full
+ supported set and lets each entry's bit-0 discriminator choose between
+ the leaf-PTE and directory-PDE views, reporting the level as
+ undetermined (``level=None`` plus ``level_candidates``) rather than
+ guessing one. Any depth outside the supported set raises
+ ``E_UNSUPPORTED``.
+
+ An empty blob decodes to ``[]``; a blob whose length is not a multiple
+ of ``entry_size`` raises ``E_TRUNCATED`` (trailing bytes are never
+ silently dropped).
+ """
+ _check_kind(kind, SUPPORTED_KINDS, "kind")
+ expected = 8 if kind == KIND_VER2 else 16
+ if entry_size != expected:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"kind {kind!r} uses {expected}-byte entries, got entry_size={entry_size}")
+ if not isinstance(base_pa, int) or isinstance(base_pa, bool) or base_pa < 0:
+ raise GmmuDecodeError(E_BAD_VALUE, "base_pa must be a non-negative int")
+ if not isinstance(blob, (bytes, bytearray, memoryview)):
+ raise GmmuDecodeError(E_BAD_VALUE, "blob must be bytes-like")
+ blob = bytes(blob)
+ allowed, pinned = _normalize_levels(levels)
+
+ if len(blob) == 0:
+ return []
+ if len(blob) % entry_size != 0:
+ raise GmmuDecodeError(
+ E_TRUNCATED,
+ f"blob length {len(blob)} is not a multiple of entry_size={entry_size}")
+
+ entries = []
+ for index in range(len(blob) // entry_size):
+ offset = index * entry_size
+ value = int.from_bytes(blob[offset:offset + entry_size], "little")
+
+ if kind == KIND_VER2_DUAL:
+ if pinned is not None and pinned != 3:
+ raise GmmuDecodeError(
+ E_UNSUPPORTED,
+ f"dual PDE entries live at level 3, got levels={levels}")
+ decoded = decode_dual_pde(value, level=3 if pinned == 3 else None)
+ level_reported = 3 if pinned == 3 else None
+ decoded["role_inferred"] = False
+ else:
+ bit0 = value & 1
+ if pinned is None:
+ role = _infer_role(bit0, allowed)
+ decoded = decode_entry(value, kind=kind, role=role, level=None)
+ level_reported = None
+ else:
+ level = pinned
+ if level == 4:
+ role = "pte"
+ elif level in (0, 1, 2):
+ role = "pde"
+ else: # level 3
+ role = "pte" if bit0 else "pde"
+ decoded = decode_entry(value, kind=kind, role=role, level=level)
+ level_reported = level
+ decoded["role_inferred"] = level_reported is None
+ if level_reported is None:
+ decoded["level_candidates"] = _candidates_for(
+ decoded["role"], allowed)
+
+ entry: dict[str, Any] = {
+ "index": index,
+ "entry_pa": base_pa + offset,
+ "value": value,
+ }
+ entry.update(decoded)
+ entry["encoding"] = kind
+ entries.append(entry)
+ return entries
+
+
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
+def _parse_int(text: str) -> int:
+ try:
+ return int(text, 0)
+ except ValueError:
+ raise argparse.ArgumentTypeError(f"not an integer: {text!r}")
+
+
+def _parse_levels(text: str):
+ if text is None:
+ return None
+ try:
+ return [int(part, 0) for part in text.split(",") if part != ""]
+ except ValueError:
+ raise argparse.ArgumentTypeError(f"not a level list: {text!r}")
+
+
+def _render_entry(entry: dict) -> str:
+ role = entry["role"]
+ head = (f"index={entry['index']} pa={entry['entry_pa']:#x} "
+ f"role={role} level={entry['level']} value={entry['value']:#x}")
+ if role == "pte":
+ fields = (f"valid={entry['valid']} aperture={entry['aperture_name']} "
+ f"volatile={entry['volatile']} encrypted={entry['encrypted']} "
+ f"privilege={entry['privilege']} read_only={entry['read_only']} "
+ f"atomic_disable={entry['atomic_disable']} "
+ f"address={entry['address'] if entry['address'] is None else hex(entry['address'])} "
+ f"address_complete={entry['address_complete']} "
+ f"peer_id={entry['peer_id']} comptagline={entry['comptagline']:#x} "
+ f"kind={entry['kind']:#x} kind_name={entry['kind_name']}")
+ elif role == "pde":
+ fields = (f"aperture={entry['aperture_name']} volatile={entry['volatile']} "
+ f"no_ats={entry['no_ats']} "
+ f"address={entry['address'] if entry['address'] is None else hex(entry['address'])} "
+ f"address_complete={entry['address_complete']}")
+ else:
+ big, small = entry["big"], entry["small"]
+ fields = (f"big=({big['aperture_name']}, "
+ f"{big['address'] if big['address'] is None else hex(big['address'])}) "
+ f"small=({small['aperture_name']}, "
+ f"{small['address'] if small['address'] is None else hex(small['address'])}) "
+ f"no_ats={entry['no_ats']}")
+ tail = (f"reserved_bits={entry['reserved_bits']} "
+ f"reserved_mask={entry['reserved_mask']:#x}")
+ return f"{head} {fields} {tail}"
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(
+ prog="python3 -m sim.gmmu_pte",
+ description=("Decode captured TU104 GMMU page-table entries (read-only, "
+ "stdlib-only; decode captured data only and validate "
+ "against a capture before trusting)."))
+ source = parser.add_mutually_exclusive_group(required=True)
+ source.add_argument("--value", type=_parse_int,
+ help="single entry value, e.g. --value 0x...")
+ source.add_argument("--blob", help="file containing a captured table blob")
+ parser.add_argument("--base-pa", type=_parse_int, default=0,
+ help="GPU physical address of the blob's first byte")
+ parser.add_argument("--kind", default=KIND_VER2, choices=SUPPORTED_KINDS,
+ help="entry encoding family (default: ver2)")
+ parser.add_argument("--entry-size", type=int, default=8,
+ help="bytes per entry (default: 8; ver2_dual uses 16)")
+ parser.add_argument("--levels", type=_parse_levels, default=None,
+ help="page-tree depth or comma-separated depths (0-4)")
+ parser.add_argument("--role", default="auto",
+ choices=("auto", "pte", "pde", "dual_pde"),
+ help="entry role for --value (default: auto)")
+ parser.add_argument("--json", action="store_true",
+ help="emit JSON (sorted keys; deterministic)")
+ args = parser.parse_args(argv)
+
+ try:
+ if args.value is not None:
+ level = None
+ if args.levels is not None:
+ if len(args.levels) != 1:
+ raise GmmuDecodeError(
+ E_BAD_VALUE, "--value accepts exactly one level")
+ level = args.levels[0]
+ entry = decode_entry(args.value, kind=args.kind, role=args.role,
+ level=level)
+ entry.setdefault("index", 0)
+ entry.setdefault("entry_pa", args.base_pa)
+ entry["value"] = args.value
+ entry["encoding"] = args.kind
+ entries = [entry]
+ else:
+ with open(args.blob, "rb") as handle:
+ blob = handle.read()
+ entries = decode_table(blob, base_pa=args.base_pa,
+ entry_size=args.entry_size,
+ levels=args.levels, kind=args.kind)
+ except OSError as exc:
+ print(f"gmmu_pte: cannot read {args.blob}: {exc}", file=sys.stderr)
+ return 2
+ except GmmuDecodeError as exc:
+ print(f"gmmu_pte: {exc}", file=sys.stderr)
+ return 2
+
+ if args.json:
+ print(json.dumps(entries, indent=2, sort_keys=True))
+ else:
+ for entry in entries:
+ print(_render_entry(entry))
+ print(f"# {len(entries)} entr{'y' if len(entries) == 1 else 'ies'} "
+ f"(kind={args.kind}, entry_size={args.entry_size})")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
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)
diff --git a/debug/risc-v-gsp-rm-vm/sim/libos.py b/debug/risc-v-gsp-rm-vm/sim/libos.py
new file mode 100644
index 0000000..e8280fd
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/sim/libos.py
@@ -0,0 +1,217 @@
+"""Minimal, honest libos syscall surface for the native GSP boot.
+
+Evidence kind: **firmware use + RTOS syscall table.** The GSP application is
+a libos task that requests kernel services with ``ecall`` (the syscall number
+travels in ``t0``); the native ``Machine`` stops on ``ecall`` by default, so
+booting the real image past kernel init needs a deterministic service model.
+
+Rules:
+
+* every requested syscall is recorded (number, name, arguments, pc,
+ instruction count) so the session shows exactly what the firmware asked for;
+* only *no-ops and modelled* services are acknowledged: task yield, cache
+ invalidation, external-interrupt arming, profiler enable/query, and the
+ timer read (returned from the modeled instruction count, labelled
+ modeled);
+* **waits are never faked**: ``PROCESSOR_SUSPEND``,
+ ``TASK_PORT_SEND_RECV_WAIT``, ``TASK_PANIC`` and every unimplemented
+ syscall return *not handled*, which stops the run with ``R_ECALL`` so the
+ caller can classify the stop (message-loop wait / panic / unsupported
+ service) instead of inventing progress;
+* task switching, DMA, memory queries, pagetable registration, partition
+ switch and interrupt delivery are not modelled.
+
+No timing, scheduling or security claim is made; this is development
+tooling for the boot ladder (docs/35 ยง3.3).
+"""
+from __future__ import annotations
+
+# Syscall numbers (RTOS ``libos_syscalls.h``).
+TASK_PORT_SEND_RECV_WAIT = 1
+TASK_YIELD = 2
+PROCESSOR_SUSPEND = 3
+DCACHE_INVALIDATE = 4
+MEMORY_QUERY = 5
+TASK_STATE_QUERY = 6
+SHUTTLE_RESET = 7
+TASK_DMA_REGISTER_TCM = 8
+TASK_DMA_COPY = 9
+TASK_DMA_FLUSH = 10
+TASK_MEMORY_READ = 11
+INIT_REGISTER_PAGETABLES = 12
+PROFILER_ENABLE = 13
+TASK_EXIT = 14
+TASK_PANIC = 15
+INTERRUPT_ARM_EXTERNAL = 16
+TIMER_GET_NS = 17
+DCACHE_INVALIDATE_ALL = 18
+PROFILER_GET_COUNTER_MASK = 19
+PARTITION_SWITCH = 20
+LIMIT_POW2 = 32
+
+NAMES = {
+ TASK_PORT_SEND_RECV_WAIT: "task_port_send_recv_wait",
+ TASK_YIELD: "task_yield",
+ PROCESSOR_SUSPEND: "processor_suspend",
+ DCACHE_INVALIDATE: "dcache_invalidate",
+ MEMORY_QUERY: "memory_query",
+ TASK_STATE_QUERY: "task_state_query",
+ SHUTTLE_RESET: "shuttle_reset",
+ TASK_DMA_REGISTER_TCM: "task_dma_register_tcm",
+ TASK_DMA_COPY: "task_dma_copy",
+ TASK_DMA_FLUSH: "task_dma_flush",
+ TASK_MEMORY_READ: "task_memory_read",
+ INIT_REGISTER_PAGETABLES: "init_register_pagetables",
+ PROFILER_ENABLE: "profiler_enable",
+ TASK_EXIT: "task_exit",
+ TASK_PANIC: "task_panic",
+ INTERRUPT_ARM_EXTERNAL: "interrupt_arm_external",
+ TIMER_GET_NS: "timer_get_ns",
+ DCACHE_INVALIDATE_ALL: "dcache_invalidate_all",
+ PROFILER_GET_COUNTER_MASK: "profiler_get_counter_mask",
+ PARTITION_SWITCH: "partition_switch",
+ LIMIT_POW2: "limit_pow2",
+}
+
+#: Services acknowledged as no-ops (result 0); everything else stops the run.
+_NOOP_SYSCALLS = frozenset({
+ TASK_YIELD, DCACHE_INVALIDATE, DCACHE_INVALIDATE_ALL, PROFILER_ENABLE,
+})
+
+
+class LibosSyscalls:
+ """Deterministic libos service model: record, acknowledge, or stop."""
+
+ name = "libos-syscalls"
+
+ def __init__(self) -> None:
+ self.calls: list[dict] = []
+ self.counts: dict[int, int] = {}
+ self.armed_mask = 0
+ #: Last registered TCM bounce buffer (``task_dma_register_tcm``).
+ self.tcm: dict | None = None
+ #: Last inverted-pagetable registration (``init_register_pagetables``).
+ self.pagetables: dict | None = None
+ self.dma_copies = 0
+ self.dma_bytes = 0
+ #: (number, a0, a1, pc, insns) of the last request.
+ self.last: tuple | None = None
+ #: The request that stopped the run (not handled), if any.
+ self.stopped: dict | None = None
+
+ def handle(self, machine, pc: int = 0):
+ """``Machine`` ecall hook: ``(handled, result)``.
+
+ Unhandled requests leave ``machine.run`` stopping with ``R_ECALL``;
+ the number is still recorded here and in ``stopped``.
+ """
+ num = machine.x[5]
+ a0, a1 = machine.x[10], machine.x[11]
+ entry = {
+ "num": num,
+ "name": NAMES.get(num, f"unknown_{num}"),
+ "a0": a0,
+ "a1": a1,
+ "pc": pc,
+ "insns": machine.insns,
+ }
+ self.calls.append(entry)
+ self.counts[num] = self.counts.get(num, 0) + 1
+ self.last = (num, a0, a1, pc, machine.insns)
+ if num == INTERRUPT_ARM_EXTERNAL:
+ self.armed_mask |= a0
+ return True, 0
+ if num == TIMER_GET_NS:
+ # Modeled time: instruction count * 1 ns (docs/36: modeled, not
+ # calibrated; no timing claim).
+ return True, machine.insns
+ if num == TASK_DMA_REGISTER_TCM:
+ # The kernel validates the VA against its pagetable and stores it
+ # as the DMA bounce buffer; without a paging model the region is
+ # recorded and the observed result (LIBOS_OK) returned.
+ self.tcm = {"va": a0, "size": a1}
+ return True, 0
+ if num == TASK_DMA_COPY:
+ return self._dma_copy(machine, a0, a1, machine.x[12], pc)
+ if num == INIT_REGISTER_PAGETABLES:
+ # The kernel builds its inverted page table from this registration
+ # and returns LIBOS_OK. The simulator does not model the
+ # inverted table (DMA resolution goes through the flat RAM + MPU
+ # + aperture seam); the registration is recorded as evidence.
+ self.pagetables = {"base": a0, "entries": a1, "probe_count": machine.x[12]}
+ return True, 0
+ if num == TASK_DMA_FLUSH:
+ return True, 0
+ if num in _NOOP_SYSCALLS:
+ return True, 0
+ if num == PROFILER_GET_COUNTER_MASK:
+ return True, 0
+ self.stopped = entry
+ return False, None
+
+ # -- DMA service model -------------------------------------------------
+
+ def _resolve(self, machine, va: int):
+ """VA -> simulator RAM offset through the MPU and aperture seam."""
+ if machine.mpu.active:
+ translated = machine.mpu.translate(va)
+ if translated is not None:
+ va = translated
+ off = va - machine.ram_base
+ if 0 <= off < machine.ram_size:
+ return off
+ translator = machine.translator
+ if translator is not None:
+ sim = translator.translate(va)
+ if sim is not None:
+ off = sim - machine.ram_base
+ if 0 <= off < machine.ram_size:
+ return off
+ return None
+
+ def _dma_copy(self, machine, dst_va: int, src_va: int, size: int, pc: int = 0):
+ """Modeled DMA copy: equivalent to a RAM memcpy in the flat model.
+
+ The real kernel drives the falcon DMA engine through TCM; the
+ simulator keeps all regions in one RAM, so the copy effect is the
+ same. Both sides must resolve or the run stops (never a faked copy).
+ """
+ if size == 0:
+ return True, 0
+ dst = self._resolve(machine, dst_va)
+ src = self._resolve(machine, src_va)
+ if dst is None or src is None:
+ self.stopped = {"num": TASK_DMA_COPY, "name": "task_dma_copy",
+ "a0": dst_va, "a1": src_va, "size": size,
+ "reason": "unresolved", "pc": pc}
+ return False, None
+ ram = machine.ram
+ if (0 <= dst <= machine.ram_size - size
+ and 0 <= src <= machine.ram_size - size):
+ ram[dst:dst + size] = bytes(ram[src:src + size])
+ else:
+ # Straddling or out-of-range: byte-wise through the bounds.
+ for i in range(size):
+ if dst + i >= machine.ram_size or src + i >= machine.ram_size:
+ self.stopped = {"num": TASK_DMA_COPY, "name": "task_dma_copy",
+ "a0": dst_va, "a1": src_va, "size": size,
+ "reason": "out-of-range", "pc": pc}
+ return False, None
+ ram[dst + i] = ram[src + i]
+ machine.invalidate()
+ self.dma_copies += 1
+ self.dma_bytes += size
+ return True, 0
+
+ def state(self) -> dict:
+ return {
+ "calls": len(self.calls),
+ "counts": {str(k): v for k, v in sorted(self.counts.items())},
+ "armed_mask": self.armed_mask,
+ "tcm": self.tcm,
+ "pagetables": self.pagetables,
+ "dma_copies": self.dma_copies,
+ "dma_bytes": self.dma_bytes,
+ "last": list(self.last) if self.last else None,
+ "stopped": self.stopped,
+ }
diff --git a/debug/risc-v-gsp-rm-vm/sim/machine.py b/debug/risc-v-gsp-rm-vm/sim/machine.py
index 9908753..7634ce1 100644
--- a/debug/risc-v-gsp-rm-vm/sim/machine.py
+++ b/debug/risc-v-gsp-rm-vm/sim/machine.py
@@ -19,6 +19,7 @@ import mmap
import struct
from .cycles import DEFAULT_MODEL
+from .mpu import MSTATUS, WATCHED_CSRS, RiscvMpu
from .decode import (
A_ADD,
A_AND,
@@ -84,6 +85,9 @@ R_ECALL = 6
#: Accepted ``unknown_mmio`` policies for accesses outside RAM/UART/the map.
UNKNOWN_MMIO_MODES = ("fault", "lenient")
+#: CSRs whose writes update the MMU model (M-mode protection unit + mstatus).
+_MPU_CSRS = frozenset(WATCHED_CSRS) | {MSTATUS}
+
UART_BASE = 0x10000000
UART_SIZE = 0x100
@@ -232,6 +236,10 @@ class Machine:
self.device_map = device_map
self.devices = devices
self.translator = translator # aperture -> RAM seam (sim.devices.dma)
+ self.mpu = RiscvMpu() # M-mode protection unit (sim.mpu)
+ #: Optional guest-ECALL service (``handle(machine) -> (handled, ret)``);
+ #: when absent (or unhandled) ``run`` stops with ``R_ECALL`` as before.
+ self.syscall_handler = None
self.unknown_mmio = unknown_mmio
self.learn_mmio = bool(learn_mmio)
self.bias = bias # PC relocation for learned-map firmware addresses
@@ -506,6 +514,10 @@ class Machine:
a = b = 0
traced = trace is not None
active = False
+ # M-mode protection unit (sim.mpu): translation is consulted only when
+ # an enabled entry exists; no match is treated as identity.
+ mpu = self.mpu
+ mpu_on = mpu.active
# Modeled cycle accounting (sim.cycles): pure table lookups, no timing.
cycle_base = self._cycle_base
cycle_op = self._cycle_op
@@ -520,7 +532,12 @@ class Machine:
a = pc
break
- off = pc - base
+ fetch_pc = pc
+ if mpu_on:
+ translated = mpu.translate(pc)
+ if translated is not None:
+ fetch_pc = translated
+ off = fetch_pc - base
if off < 0 or off + 4 > limit:
reason = R_FAULT
a = 1
@@ -558,6 +575,12 @@ class Machine:
b = hit
break
o = ea - base
+ mem_ea = ea
+ if mpu_on:
+ translated = mpu.translate(ea)
+ if translated is not None:
+ mem_ea = translated
+ o = mem_ea - base
if 0 <= o <= limit - size:
if active:
trace.mem(insn_pc, ea, size, False, self.insns + n)
@@ -587,7 +610,7 @@ class Machine:
pc = nxt
else:
try:
- v = self._mmio_load(ea, size, pc)
+ v = self._mmio_load(mem_ea, size, pc)
except Fault as f:
reason = R_FAULT
a = f.cause
@@ -613,6 +636,12 @@ class Machine:
b = hit
break
o = ea - base
+ mem_ea = ea
+ if mpu_on:
+ translated = mpu.translate(ea)
+ if translated is not None:
+ mem_ea = translated
+ o = mem_ea - base
value = x[r2]
if 0 <= o <= limit - size:
if active:
@@ -627,17 +656,20 @@ class Machine:
ram[o] = value & 0xFF
# Self-modifying code: drop any cached decode overlapping
# the written range (firmware patches code at runtime).
- p = ea & ~1
- end = ea + size
- while p < end:
- cache.pop(p, None)
- p += 2
+ # Both the virtual address and its translated backing are
+ # dropped so a fetch through either mapping re-decodes.
+ for target in ((ea, mem_ea) if mem_ea != ea else (ea,)):
+ p = target & ~1
+ end = target + size
+ while p < end:
+ cache.pop(p, None)
+ p += 2
if counting:
cycle_sum += cycle_mem
pc = nxt
else:
try:
- self._mmio_store(ea, size, value, pc)
+ self._mmio_store(mem_ea, size, value, pc)
except Fault as f:
reason = R_FAULT
a = f.cause
@@ -864,6 +896,9 @@ class Machine:
a = 2
b = csr
break
+ if csr in _MPU_CSRS:
+ mpu.on_csr_write(csr, self.csrs.get(csr, 0))
+ mpu_on = mpu.active
pc = nxt
elif kind == K_AMO:
@@ -938,9 +973,20 @@ class Machine:
elif kind == K_ECALL:
if single_step:
pc = nxt
+ elif self.syscall_handler is not None:
+ handled, result = self.syscall_handler(self, pc)
+ if not handled:
+ reason = R_ECALL
+ a = pc
+ b = x[5] # syscall number (t0), for diagnostics
+ break
+ if result is not None:
+ x[10] = result & MASK64
+ pc = nxt
else:
reason = R_ECALL
a = pc
+ b = x[5]
break
elif kind == K_MRET:
@@ -975,15 +1021,25 @@ class Machine:
# -- guest memory helpers (AMO) ----------------------------------------
def _guest_load(self, addr: int, width: int, pc: int = 0) -> int:
- off = addr - self.ram_base
+ mem = addr
+ if self.mpu.active:
+ translated = self.mpu.translate(addr)
+ if translated is not None:
+ mem = translated
+ off = mem - self.ram_base
if 0 <= off <= self.ram_size - width:
if width == 8:
return self._u64.unpack_from(self.ram, off)[0]
return self._u32.unpack_from(self.ram, off)[0]
- return self._mmio_load(addr, width, pc)
+ return self._mmio_load(mem, width, pc)
def _guest_store(self, addr: int, width: int, value: int, pc: int = 0) -> None:
- off = addr - self.ram_base
+ mem = addr
+ if self.mpu.active:
+ translated = self.mpu.translate(addr)
+ if translated is not None:
+ mem = translated
+ off = mem - self.ram_base
if 0 <= off <= self.ram_size - width:
if width == 8:
self._u64p(self.ram, off, value)
@@ -995,7 +1051,7 @@ class Machine:
self._cache.pop(p, None)
p += 2
return
- self._mmio_store(addr, width, value, pc)
+ self._mmio_store(mem, width, value, pc)
#: Task-facing alias: the native simulator machine is :class:`Machine`.
diff --git a/debug/risc-v-gsp-rm-vm/sim/mpu.py b/debug/risc-v-gsp-rm-vm/sim/mpu.py
new file mode 100644
index 0000000..3f24358
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/sim/mpu.py
@@ -0,0 +1,154 @@
+"""RISC-V MMU (NVMPU) CSR model for the GSP core.
+
+Evidence kind: **hardware CSR/field header + firmware use + observed access
+sequence.**
+
+The GSP RISC-V core boots in *bare* mode and then enables a small protection
+unit through CSRs; the firmware maps resident code/data into TCM and uses a
+scratch window to copy it there. Without this model the flat-RAM simulator
+serves those accesses at their virtual addresses and the relocated copy reads
+the wrong backing (the B2 ceiling in ``docs/36-GSP-DEVICE-MODEL.md``).
+
+Modelled CSRs (M-mode values from the hardware CSR header; the firmware writes
+exactly these - observed at the 570.144 bootstrap ``0x4007000``..``0x4007C00``):
+
+============================================ ====== =========================
+CSR value semantics
+============================================ ====== =========================
+``MMPUIDX`` (0x7C8) entry INDEX field 5:0
+``MMPUVA`` (0x7C9) VA BASE 63:12, VLD bit 0
+``MMPUPA`` (0x7CA) PA BASE 63:12
+``MMPURNG`` (0x7CB) range RANGE 63:12 (a size)
+``MMPUATTR`` (0x7CC) attr recorded, not enforced
+``mstatus.VM`` (0x300) mode 0x1E = NVMPU (enabled)
+============================================ ====== =========================
+
+Semantics modelled:
+
+* an entry is *valid* when ``MMPUVA.VLD`` is set; writes to ``MMPUVA`` with
+ VLD clear disable the selected entry (the firmware's clear sequence);
+* translation requires ``mstatus.VM == NVMPU`` (``0x1E``, field bits 28:24);
+ before the enable write the CSR entries exist but no access is translated;
+* lookup is **lowest-index-first** (the firmware's own bootstrap comment:
+ "the MPU gives precedence to lower mappings"); a matching entry maps
+ ``addr`` to ``pa + (addr - va_base)`` when
+ ``va_base <= addr < va_base + range``;
+* **no match is not a fault here**: the machine treats "no matching entry" as
+ identity. This is deliberately more lenient than silicon (which would
+ fault under the kernel's management) because the simulator keeps local
+ memory flat and the boot path's identity mappings are self-consistent; the
+ choice is recorded in the session and in ``docs/36`` ยง6.
+
+Permissions and cache/coherence attributes are recorded for evidence but not
+enforced; no timing or side-channel claim is made. This module has no device
+or host dependency and is deterministic.
+"""
+from __future__ import annotations
+
+#: CSR numbers (M-mode protection unit; hardware CSR header values).
+MMPUIDX = 0x7C8
+MMPUVA = 0x7C9
+MMPUPA = 0x7CA
+MMPURNG = 0x7CB
+MMPUATTR = 0x7CC
+
+#: CSRs this model consumes when written by the guest.
+WATCHED_CSRS = (MMPUIDX, MMPUVA, MMPUPA, MMPURNG, MMPUATTR)
+
+MSTATUS = 0x300
+MSTATUS_VM_SHIFT = 24
+MSTATUS_VM_MASK = 0x1F << MSTATUS_VM_SHIFT
+#: mstatus.VM value that enables the protection unit (header addendum).
+MSTATUS_VM_NVMPU = 0x1E
+
+#: Field masks (header: BASE/RANGE fields start at bit 12, 4 KiB granularity).
+_PAGE_MASK = ~0xFFF & ((1 << 64) - 1)
+_INDEX_MASK = 0x3F
+_MAX_ENTRIES = 64
+
+
+class RiscvMpu:
+ """MMPU entry table + mstatus enable state (deterministic, serialisable)."""
+
+ def __init__(self) -> None:
+ self.index = 0
+ self.mstatus = 0
+ self.entries: dict[int, dict] = {}
+ #: Ascending [(va_base, pa, range)] of valid entries; kept in sync on
+ #: every CSR write so translation is a short loop.
+ self._active: list[tuple[int, int, int]] = []
+ self.enabled = False
+
+ # -- guest CSR writes --------------------------------------------------
+
+ def on_csr_write(self, csr: int, value: int) -> None:
+ """Apply one guest CSR write (``value`` already masked to 64 bits)."""
+ if csr == MMPUIDX:
+ self.index = value & _INDEX_MASK
+ elif csr == MMPUVA:
+ entry = self.entries.setdefault(self.index, self._blank())
+ entry["va"] = value & _PAGE_MASK
+ entry["valid"] = bool(value & 1)
+ self._refresh()
+ elif csr == MMPUPA:
+ entry = self.entries.setdefault(self.index, self._blank())
+ entry["pa"] = value & _PAGE_MASK
+ elif csr == MMPURNG:
+ entry = self.entries.setdefault(self.index, self._blank())
+ entry["range"] = value & _PAGE_MASK
+ elif csr == MMPUATTR:
+ entry = self.entries.setdefault(self.index, self._blank())
+ entry["attr"] = value
+ elif csr == MSTATUS:
+ self.mstatus = value & ((1 << 64) - 1)
+ self.enabled = ((self.mstatus & MSTATUS_VM_MASK) >> MSTATUS_VM_SHIFT
+ == MSTATUS_VM_NVMPU)
+
+ @staticmethod
+ def _blank() -> dict:
+ return {"va": 0, "pa": 0, "range": 0, "attr": 0, "valid": False}
+
+ def _refresh(self) -> None:
+ self._active = [
+ (e["va"], e["pa"], e["range"])
+ for _, e in sorted(self.entries.items())
+ if e.get("valid") and e.get("range")
+ ]
+
+ # -- translation -------------------------------------------------------
+
+ @property
+ def active(self) -> bool:
+ """True when a matching entry could translate (fast-path switch)."""
+ return self.enabled and bool(self._active)
+
+ def translate(self, addr: int) -> int | None:
+ """Virtual -> physical for ``addr``, or ``None`` when unmatched."""
+ if not self.enabled:
+ return None
+ for va, pa, rng in self._active:
+ off = addr - va
+ if 0 <= off < rng:
+ return pa + off
+ return None
+
+ # -- evidence / snapshot ----------------------------------------------
+
+ def state(self) -> dict:
+ return {
+ "enabled": self.enabled,
+ "mstatus": self.mstatus,
+ "index": self.index,
+ "entries": [
+ {"index": i, **{k: e[k] for k in ("va", "pa", "range", "attr", "valid")}}
+ for i, e in sorted(self.entries.items())
+ if e.get("valid") or e.get("pa") or e.get("range") or e.get("attr")
+ ],
+ }
+
+ def reset(self) -> None:
+ self.index = 0
+ self.mstatus = 0
+ self.entries.clear()
+ self._active = []
+ self.enabled = False
diff --git a/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json b/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json
index 919dffe..91f4ed5 100644
--- a/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json
+++ b/debug/risc-v-gsp-rm-vm/sim/profiles/tu104-570.144.json
@@ -69,6 +69,808 @@
"evidence": "PMU stub: no PMU register has been observed; every access stays unhandled and logged.",
"windows": []
},
+ "gmmu": {
+ "evidence": "GMMU/PRI-MMU register surface: NV_PFB_PRI_MMU_* offsets inside the PFB block (block range 0x00100000-0x00100FFF), the WPR/VPR/lock cluster (0x1FA7D8-0x1FA830) and the HSMMU block (0x001FAC00-0x001FAFFF) from the hardware register/field headers. Observe-only: no register semantics are modelled and no read value is invented; the B0-B2 runs did not touch these windows; the B3 wave's WPR2 read pair (0x1FA824/0x1FA828) falls in the wpr-lock window and is recorded observe-only (0 served).",
+ "mode": "observe-only",
+ "windows": [
+ {"base": "0x2000000000100C80", "size": "0x280", "label": "gmmu-pri-mmu", "registers": {"0x0": {"name": "NV_PFB_PRI_MMU_CTRL", "fields": {"VM_PG_SIZE": [0, 0], "VOL_FAULT": [1, 1], "COMP_FAULT": [2, 2], "MISS_GRAN": [4, 3], "CACHE_MODE": [6, 5], "PDE_CACHE_MODE": [8, 7], "L2_SEND_MODE": [9, 9], "WORK_CREATION_DISABLE": [10, 10], "USE_PDB_BIG_PAGE_SIZE": [11, 11], "USE_FULL_COMP_TAG_LINE": [12, 12], "PRI_FIFO_ERROR_CLEAR": [13, 13], "PRI_FIFO_ERROR": [14, 14], "PRI_FIFO_EMPTY": [15, 15], "PRI_FIFO_SPACE": [23, 16], "ATOMIC_CAPABILITY_MODE": [25, 24], "STRONG_BIT_ATOMIC_CYA": [26, 26], "STRONG_BIT_OVERRIDE_ATOMIC_DISABLE": [26, 26], "ATOMIC_CAPABILITY_SYS_NCOH_MODE": [27, 27], "MMU_APERTURE": [29, 28], "MMU_VOL": [30, 30], "MMU_DISABLE": [31, 31]}}, "0x4": {"name": "NV_PFB_PRI_MMU_PM"}, "0x8": {"name": "NV_PFB_PRI_MMU_PM_ADDR_LOW"}, "0xc": {"name": "NV_PFB_PRI_MMU_PM_ADDR_HIGH"}, "0x10": {"name": "NV_PFB_PRI_MMU_PM_UNIT_MASK"}, "0x14": {"name": "NV_PFB_PRI_MMU_PM_REQ_MASK"}, "0x18": {"name": "NV_PFB_PRI_MMU_CG"}, "0x1c": {"name": "NV_PFB_PRI_MMU_CG1"}, "0x20": {"name": "NV_PFB_PRI_MMU_PG"}, "0x24": {"name": "NV_PFB_PRI_MMU_TLB_CTRL", "fields": {"L2_SIZE": [1, 0], "L2_FA_SIZE": [3, 2], "L2_RB_SIZE": [5, 4], "HUB_SIZE": [7, 6], "HUB_FA_SIZE": [9, 8], "HUB_RB_SIZE": [11, 10], "FILL_SIZE_PTE": [13, 12], "FILL_SIZE_PDE": [15, 14], "FILL_SIZE_RB": [17, 16], "L2_SET_INDEX_BIG_PG": [19, 18], "HUB_SET_INDEX_BIG_PG": [21, 20], "L2_SET_INDEX_SMALL_PG": [23, 22], "HUB_SET_INDEX_SMALL_PG": [25, 24], "HUB_LOCKED_LIMIT": [28, 26], "ATSD_PROP_FLUSH": [26, 26], "INVAL_PROP_FLUSH": [27, 27], "BIND_PROP_FLUSH": [28, 28], "HUB_RSRVD_VM": [31, 29]}}, "0x28": {"name": "NV_PFB_PRI_MMU_RSRVD"}, "0x2c": {"name": "NV_PFB_PRI_MMU_BIND_IMB"}, "0x30": {"name": "NV_PFB_PRI_MMU_BIND"}, "0x34": {"name": "NV_PFB_PRI_MMU_INVALIDATE_VADDR"}, "0x38": {"name": "NV_PFB_PRI_MMU_INVALIDATE_PDB"}, "0x3c": {"name": "NV_PFB_PRI_MMU_INVALIDATE", "fields": {"VADDR_BITS": [31, 4], "UPPER_VADDR_BITS": [19, 0], "PDB_APERTURE": [1, 1], "PDB_ADDR": [31, 4], "UPPER_PDB_ADDR": [19, 0], "PASID_VAL": [19, 0], "SIZE_VAL": [5, 0], "SIZE_GFID": [31, 26], "ALL_VA": [0, 0], "ALL_PDB": [1, 1], "HUBTLB_ONLY": [2, 2], "REPLAY": [5, 3], "SYS_MEMBAR": [6, 6], "ACK": [8, 7], "CANCEL_CLIENT_ID": [14, 9], "CANCEL_GPC_ID": [19, 15], "CANCEL_CLIENT_TYPE": [20, 20], "USE_PASID": [21, 21], "USE_SIZE": [22, 22], "PROP_FLUSH": [23, 23], "CACHE_LEVEL": [26, 24], "TRIGGER": [31, 31], "VADDR_VIRT_BITS": [31, 4], "UPPER_VADDR_VIRT_BITS": [19, 0], "PDB_VIRT_APERTURE": [1, 1], "PDB_VIRT_ADDR": [31, 4], "UPPER_PDB_VIRT_ADDR": [19, 0], "PASID_VIRT_VAL": [19, 0], "SIZE_VIRT_VAL": [5, 0], "SIZE_VIRT_GFID": [31, 26], "VIRT_ALL_VA": [0, 0], "VIRT_ALL_PDB": [1, 1], "VIRT_HUBTLB_ONLY": [2, 2], "VIRT_REPLAY": [5, 3], "VIRT_SYS_MEMBAR": [6, 6], "VIRT_ACK": [8, 7], "VIRT_CANCEL_CLIENT_ID": [14, 9], "VIRT_CANCEL_GPC_ID": [19, 15], "VIRT_CANCEL_CLIENT_TYPE": [20, 20], "VIRT_USE_PASID": [21, 21], "VIRT_USE_SIZE": [22, 22], "VIRT_PROP_FLUSH": [23, 23], "VIRT_CACHE_LEVEL": [26, 24], "VIRT_TRIGGER": [31, 31]}}, "0x40": {"name": "NV_PFB_PRI_MMU_PREFETCH"}, "0x44": {"name": "NV_PFB_PRI_MMU_DEBUG_CTRL"}, "0x48": {"name": "NV_PFB_PRI_MMU_DEBUG_WR"}, "0x4c": {"name": "NV_PFB_PRI_MMU_DEBUG_RD"}, "0x50": {"name": "NV_PFB_PRI_MMU_VPR_INFO"}, "0x54": {"name": "NV_PFB_PRI_MMU_WPR_INFO"}, "0x58": {"name": "NV_PFB_PRI_MMU_VPR_WPR_WRITE"}, "0x5c": {"name": "NV_PFB_PRI_MMU_PRIV_LEVEL_MASK", "fields": {"READ_PROTECTION": [3, 0], "READ_PROTECTION_LEVEL0": [0, 0], "READ_PROTECTION_LEVEL1": [1, 1], "READ_PROTECTION_LEVEL2": [2, 2], "READ_PROTECTION_LEVEL3": [3, 3], "WRITE_PROTECTION": [7, 4], "WRITE_PROTECTION_LEVEL0": [4, 4], "WRITE_PROTECTION_LEVEL1": [5, 5], "WRITE_PROTECTION_LEVEL2": [6, 6], "WRITE_PROTECTION_LEVEL3": [7, 7], "READ_VIOLATION": [8, 8], "WRITE_VIOLATION": [9, 9], "SOURCE_READ_CONTROL": [10, 10], "SOURCE_WRITE_CONTROL": [11, 11], "SOURCE_ENABLE": [31, 12]}}, "0x60": {"name": "NV_PFB_PRI_MMU_LOCAL_MEMORY_RANGE"}, "0x64": {"name": "NV_PFB_PRI_MMU_PHYS_SECURE"}, "0x68": {"name": "NV_PFB_PRI_MMU_INVALIDATE_UPPER_VADDR"}, "0x6c": {"name": "NV_PFB_PRI_MMU_INVALIDATE_UPPER_PDB"}, "0x70": {"name": "NV_PFB_PRI_MMU_PM_UPPER_ADDR_LOW"}, "0x74": {"name": "NV_PFB_PRI_MMU_PM_UPPER_ADDR_HIGH"}, "0x78": {"name": "NV_PFB_PRI_MMU_PAGE_FAULT_CTRL", "fields": {"PRF_FILTER": [1, 0], "VIRT_PRF_FILTER": [1, 0]}}, "0x7c": {"name": "NV_PFB_PRI_MMU_PG1"}, "0x180": {"name": "NV_PFB_PRI_MMU_ATS_ADDR_RANGE_LOCAL"}, "0x1cc": {"name": "NV_PFB_PRI_MMU_FAULT_ADDR_LO"}, "0x1d0": {"name": "NV_PFB_PRI_MMU_FAULT_ADDR_HI"}, "0x1d4": {"name": "NV_PFB_PRI_MMU_FAULT_INST_LO"}, "0x1d8": {"name": "NV_PFB_PRI_MMU_FAULT_INST_HI"}, "0x1dc": {"name": "NV_PFB_PRI_MMU_FAULT_INFO"}, "0x1e0": {"name": "NV_PFB_PRI_MMU_FAULT_STATUS", "fields": {"DROPPED_BAR1_PHYS": [0, 0], "DROPPED_BAR1_VIRT": [1, 1], "DROPPED_BAR2_PHYS": [2, 2], "DROPPED_BAR2_VIRT": [3, 3], "DROPPED_IFB_PHYS": [4, 4], "DROPPED_IFB_VIRT": [5, 5], "DROPPED_OTHER_PHYS": [6, 6], "DROPPED_OTHER_VIRT": [7, 7], "REPLAYABLE": [8, 8], "NON_REPLAYABLE": [9, 9], "REPLAYABLE_ERROR": [10, 10], "NON_REPLAYABLE_ERROR": [11, 11], "REPLAYABLE_OVERFLOW": [12, 12], "NON_REPLAYABLE_OVERFLOW": [13, 13], "REPLAYABLE_GETPTR_CORRUPTED": [14, 14], "NON_REPLAYABLE_GETPTR_CORRUPTED": [15, 15], "BUSY_TYPE": [29, 28], "BUSY": [30, 30], "VALID": [31, 31], "VIRT_DROPPED_BAR1_PHYS": [0, 0], "VIRT_DROPPED_BAR1_VIRT": [1, 1], "VIRT_DROPPED_BAR2_PHYS": [2, 2], "VIRT_DROPPED_BAR2_VIRT": [3, 3], "VIRT_DROPPED_IFB_PHYS": [4, 4], "VIRT_DROPPED_IFB_VIRT": [5, 5], "VIRT_DROPPED_OTHER_PHYS": [6, 6], "VIRT_DROPPED_OTHER_VIRT": [7, 7], "VIRT_REPLAYABLE": [8, 8], "VIRT_NON_REPLAYABLE": [9, 9], "VIRT_REPLAYABLE_ERROR": [10, 10], "VIRT_NON_REPLAYABLE_ERROR": [11, 11], "VIRT_REPLAYABLE_OVERFLOW": [12, 12], "VIRT_NON_REPLAYABLE_OVERFLOW": [13, 13], "VIRT_REPLAYABLE_GETPTR_CORRUPTED": [14, 14], "VIRT_NON_REPLAYABLE_GETPTR_CORRUPTED": [15, 15], "VIRT_BUSY_TYPE": [29, 28], "VIRT_BUSY": [30, 30], "VIRT_VALID": [31, 31]}}, "0x1e4": {"name": "NV_PFB_PRI_MMU_INVALIDATE_PASID"}, "0x1e8": {"name": "NV_PFB_PRI_MMU_INVALIDATE_SIZE"}, "0x1ec": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_CONTROL"}, "0x1f0": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_STATUS"}, "0x1f4": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_CORRECTED_ERR_COUNT"}, "0x1f8": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_UNCORRECTED_ERR_COUNT"}, "0x1fc": {"name": "NV_PFB_PRI_MMU_L2TLB_ECC_ADDRESS"}, "0x200": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_CONTROL"}, "0x204": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_STATUS"}, "0x208": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_CORRECTED_ERR_COUNT"}, "0x20c": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_UNCORRECTED_ERR_COUNT"}, "0x210": {"name": "NV_PFB_PRI_MMU_HUBTLB_ECC_ADDRESS"}, "0x214": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_CONTROL"}, "0x218": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_STATUS"}, "0x21c": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_CORRECTED_ERR_COUNT"}, "0x220": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_UNCORRECTED_ERR_COUNT"}, "0x224": {"name": "NV_PFB_PRI_MMU_FILLUNIT_ECC_ADDRESS"}, "0x228": {"name": "NV_PFB_PRI_MMU_HUBMMU_GLOBAL_ESR"}, "0x22c": {"name": "NV_PFB_PRI_MMU_ECC_PRIV_LEVEL_MASK"}, "0x234": {"name": "NV_PFB_PRI_MMU_CBC_ADJ"}, "0x238": {"name": "NV_PFB_PRI_MMU_CBC_ADJ_PRIV_LEVEL_MASK"}, "0x23c": {"name": "NV_PFB_PRI_MMU_CBC_PRIV_LEVEL_MASK"}, "0x240": {"name": "NV_PFB_PRI_MMU_NUM_ACTIVE_LTCS"}, "0x244": {"name": "NV_PFB_PRI_MMU_CBC_BASE"}, "0x248": {"name": "NV_PFB_PRI_MMU_CBC_TOP"}, "0x24c": {"name": "NV_PFB_PRI_MMU_CBC_MAX"}, "0x250": {"name": "NV_PFB_PRI_MMU_HYPERVISOR_CTL"}, "0x254": {"name": "NV_PFB_PRI_MMU_PEER_SUPPORT"}, "0x258": {"name": "NV_PFB_PRI_MMU_BIND_EXT"}, "0x25c": {"name": "NV_PFB_PRI_MMU_INT_VECTOR_ECC_ERROR"}, "0x260": {"name": "NV_PFB_PRI_MMU_INT_VECTOR_INFO_FAULT"}, "0x27c": {"name": "NV_PFB_PRI_MMU_PHYS_SECURE_PRIV_LEVEL_MASK"}}},
+ {"base": "0x20000000001FA7D8", "size": "0x5C", "label": "gmmu-wpr-lock", "registers": {"0x0": {"name": "NV_PFB_PRI_MMU_PMU_PRIV_LEVEL_MASK"}, "0x4": {"name": "NV_PFB_PRI_MMU_SEC_PRIV_LEVEL_MASK"}, "0x8": {"name": "NV_PFB_PRI_MMU_NVDEC_PRIV_LEVEL_MASK"}, "0xc": {"name": "NV_PFB_PRI_MMU_NVENC_PRIV_LEVEL_MASK"}, "0x10": {"name": "NV_PFB_PRI_MMU_NVJPG_PRIV_LEVEL_MASK"}, "0x14": {"name": "NV_PFB_PRI_MMU_GSP_PRIV_LEVEL_MASK"}, "0x18": {"name": "NV_PFB_PRI_MMU_FECS_PRIV_LEVEL_MASK"}, "0x1c": {"name": "NV_PFB_PRI_MMU_GPCCS_PRIV_LEVEL_MASK"}, "0x20": {"name": "NV_PFB_PRI_MMU_FBFALCON_PRIV_LEVEL_MASK"}, "0x28": {"name": "NV_PFB_PRI_MMU_VPR_MODE", "fields": {"VPR_PROTECTED_MODE": [4, 4], "MEMORY_LOCKED": [5, 5], "VPR_NON_COMPRESSIBLE": [6, 6], "SYSWR_WHEN_MEMORY_LOCKED": [7, 7], "MEMORY_LOCK_FUSE": [8, 8], "WPR_COMPRESSIBLE": [9, 9], "SECURE_LOCK": [12, 12], "WPR_WITH_SECURE_LOCK": [13, 13], "FORCE_SPA_EQUAL_GPA": [20, 20], "VPR_FAULT_REPORT": [29, 29], "VPR_LOCK_HWPM": [30, 30], "VPR_LOCK_MISC": [28, 28]}}, "0x2c": {"name": "NV_PFB_PRI_MMU_VPR_ADDR_LO"}, "0x30": {"name": "NV_PFB_PRI_MMU_VPR_ADDR_HI"}, "0x34": {"name": "NV_PFB_PRI_MMU_VPR_CYA_LO"}, "0x38": {"name": "NV_PFB_PRI_MMU_VPR_CYA_HI"}, "0x3c": {"name": "NV_PFB_PRI_MMU_WPR_ALLOW_READ", "fields": {"ALL": [11, 4], "WPR1": [7, 4], "WPR1_SECURE0": [4, 4], "WPR1_SECURE1": [5, 5], "WPR1_SECURE2": [6, 6], "WPR1_SECURE3": [7, 7], "WPR2": [11, 8], "WPR2_SECURE0": [8, 8], "WPR2_SECURE1": [9, 9], "WPR2_SECURE2": [10, 10], "WPR2_SECURE3": [11, 11], "WPR_OK": [27, 27], "MIS_MATCH": [31, 28], "MIS_MATCH0": [28, 28], "MIS_MATCH1": [29, 29], "MIS_MATCH2": [30, 30], "MIS_MATCH3": [31, 31]}}, "0x40": {"name": "NV_PFB_PRI_MMU_WPR_ALLOW_WRITE", "fields": {"ALL": [11, 4], "WPR1": [7, 4], "WPR1_SECURE0": [4, 4], "WPR1_SECURE1": [5, 5], "WPR1_SECURE2": [6, 6], "WPR1_SECURE3": [7, 7], "WPR2": [11, 8], "WPR2_SECURE0": [8, 8], "WPR2_SECURE1": [9, 9], "WPR2_SECURE2": [10, 10], "WPR2_SECURE3": [11, 11], "WPR_ENABLE_FUSE": [27, 27], "MIS_MATCH": [31, 28], "MIS_MATCH0": [28, 28], "MIS_MATCH1": [29, 29], "MIS_MATCH2": [30, 30], "MIS_MATCH3": [31, 31]}}, "0x44": {"name": "NV_PFB_PRI_MMU_WPR1_ADDR_LO"}, "0x48": {"name": "NV_PFB_PRI_MMU_WPR1_ADDR_HI"}, "0x4c": {"name": "NV_PFB_PRI_MMU_WPR2_ADDR_LO"}, "0x50": {"name": "NV_PFB_PRI_MMU_WPR2_ADDR_HI"}, "0x54": {"name": "NV_PFB_PRI_MMU_LOCK_ADDR_LO"}, "0x58": {"name": "NV_PFB_PRI_MMU_LOCK_ADDR_HI"}}},
+ {"base": "0x20000000001FAC00", "size": "0x400", "label": "gmmu-hsmmu", "registers": {"0x80": {"name": "NV_PFB_HSMMU_PRI_MMU_CTRL", "fields": {"VM_PG_SIZE": [0, 0], "VOL_FAULT": [1, 1], "COMP_FAULT": [2, 2], "MISS_GRAN": [4, 3], "CACHE_MODE": [6, 5], "L2_SEND_MODE": [9, 9], "USE_FULL_COMP_TAG_LINE": [12, 12], "ATOMIC_CAPABILITY_MODE": [25, 24], "STRONG_BIT_ATOMIC_CYA": [26, 26], "STRONG_BIT_OVERRIDE_ATOMIC_DISABLE": [26, 26]}}, "0x84": {"name": "NV_PFB_HSMMU_PRI_MMU_PM"}, "0x88": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_ADDR_LOW"}, "0x8c": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_UPPER_ADDR_LOW"}, "0x90": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_ADDR_HIGH"}, "0x94": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_UPPER_ADDR_HIGH"}, "0x98": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_UNIT_MASK"}, "0x9c": {"name": "NV_PFB_HSMMU_PRI_MMU_PM_REQ_MASK"}, "0xa0": {"name": "NV_PFB_HSMMU_PRI_MMU_TLB_CTRL", "fields": {"UTLB_SIZE": [1, 0], "UTLB_RB_SIZE": [3, 2], "UTLB_FAST_PATH_DISABLE": [4, 4], "L1TLB_SIZE": [17, 16], "L1TLB_FA_SIZE": [19, 18], "L1TLB_RB_SIZE": [21, 20], "L1TLB_SET_INDEX_BIG_PG": [23, 22], "L1TLB_SET_INDEX_SMALL_PG": [25, 24], "L1TLB_FAST_PATH_DISABLE": [26, 26], "SECURE_LOCK": [30, 30], "FAST_BIG_PG_FAULT_DISABLE": [31, 31]}}, "0xa4": {"name": "NV_PFB_HSMMU_PRI_MMU_PREFETCH"}, "0xb0": {"name": "NV_PFB_HSMMU_PRI_MMU_CG"}, "0xb4": {"name": "NV_PFB_HSMMU_PRI_MMU_CG1"}, "0xb8": {"name": "NV_PFB_HSMMU_PRI_MMU_PG"}, "0xc0": {"name": "NV_PFB_HSMMU_PRI_MMU_PG1"}, "0xc4": {"name": "NV_PFB_HSMMU_PRI_MMU_DEBUG_CTRL"}, "0xc8": {"name": "NV_PFB_HSMMU_PRI_MMU_DEBUG_WR"}, "0xcc": {"name": "NV_PFB_HSMMU_PRI_MMU_DEBUG_RD"}}}
+ ],
+ "note": "gmmu-pri-mmu spans the NV_PFB_PRI_MMU_* cluster 0x100C80-0x100EFC (interleaved PFB PRI_FB/NISO offsets inside the span stay unclassified); gmmu-wpr-lock overlaps the fwsec WPR2 window at 0x1FA824/0x1FA828 and just declines, so the access follows exactly the same underlying path as a run without the observer (observed by tests, not an assertion that the WPR2 path serves)."
+ },
+ "fbif": {
+ "evidence": "FBIF/FBPA register surface: NV_PFB_FBPA block (0x009A0000-0x009A3FFF), the 16 per-partition FBPA instances (0x00900000 + i*0x4000) and the 3 MC instances (0x00980000 + i*0x4000) from the hardware register/field headers. Observe-only: FB access-path, compression and ECC semantics are not modelled; no read value is invented.",
+ "mode": "observe-only",
+ "registers": {
+ "0x0": {
+ "name": "NV_PFB_FBPA_PRI_FBPA_CG"
+ },
+ "0x4": {
+ "name": "NV_PFB_FBPA_PRI_FBPA_CG1"
+ },
+ "0x8": {
+ "name": "NV_PFB_FBPA_PRI_FBPA_PG"
+ },
+ "0xc": {
+ "name": "NV_PFB_FBPA_PRI_FBPA_PG1"
+ },
+ "0x50": {
+ "name": "NV_PFB_FBPA_SPARE"
+ },
+ "0x54": {
+ "name": "NV_PFB_FBPA_FALCON_MONITOR"
+ },
+ "0x60": {
+ "name": "NV_PFB_FBPA_CFG_PWRD"
+ },
+ "0x64": {
+ "name": "NV_PFB_FBPA_FALCON_CTRL_VER"
+ },
+ "0x80": {
+ "name": "NV_PFB_FBPA_DEBUG_0"
+ },
+ "0x84": {
+ "name": "NV_PFB_FBPA_DEBUG_1"
+ },
+ "0x90": {
+ "name": "NV_PFB_FBPA_TESTCMD",
+ "fields": {
+ "CKE": [
+ 0,
+ 0
+ ],
+ "CS0": [
+ 1,
+ 1
+ ],
+ "CS1": [
+ 2,
+ 2
+ ],
+ "RAS": [
+ 3,
+ 3
+ ],
+ "CAS": [
+ 4,
+ 4
+ ],
+ "WE": [
+ 5,
+ 5
+ ],
+ "RES": [
+ 6,
+ 6
+ ],
+ "BANK_4": [
+ 7,
+ 7
+ ],
+ "BANK": [
+ 11,
+ 8
+ ],
+ "ADR": [
+ 25,
+ 12
+ ],
+ "ADR_UPPER": [
+ 28,
+ 26
+ ],
+ "GO": [
+ 30,
+ 30
+ ],
+ "HOLD": [
+ 31,
+ 31
+ ],
+ "EXT_ACT": [
+ 0,
+ 0
+ ],
+ "EXT_VLD_DRAM0": [
+ 1,
+ 1
+ ],
+ "EXT_VLD_DRAM1": [
+ 2,
+ 2
+ ],
+ "EXT_ADR_DRAM1": [
+ 10,
+ 3
+ ],
+ "EXT_RW_CMD": [
+ 16,
+ 16
+ ],
+ "EXT_MRS_CMD": [
+ 17,
+ 17
+ ],
+ "EXT_DR_CMD": [
+ 18,
+ 18
+ ],
+ "EXT_4CK_CMD": [
+ 19,
+ 19
+ ],
+ "EXT_LAM_CMD": [
+ 20,
+ 20
+ ],
+ "EXT_1_ADR": [
+ 23,
+ 0
+ ],
+ "EXT_1_CSB0": [
+ 27,
+ 27
+ ],
+ "EXT_1_CSB1": [
+ 28,
+ 28
+ ],
+ "EXT_1_USE_ADR": [
+ 29,
+ 29
+ ]
+ }
+ },
+ "0x94": {
+ "name": "NV_PFB_FBPA_TESTCMD_EXT"
+ },
+ "0xa0": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE0"
+ },
+ "0xa4": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE1"
+ },
+ "0xa8": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE2"
+ },
+ "0xac": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP0BYTE3"
+ },
+ "0xb0": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE0"
+ },
+ "0xb4": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE1"
+ },
+ "0xb8": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE2"
+ },
+ "0xbc": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BIT_SUBP1BYTE3"
+ },
+ "0xc0": {
+ "name": "NV_PFB_FBPA_SWIZZLE_DBI"
+ },
+ "0xc4": {
+ "name": "NV_PFB_FBPA_SWIZZLE_BYTE"
+ },
+ "0xc8": {
+ "name": "NV_PFB_FBPA_SWIZZLE_ECC"
+ },
+ "0xcc": {
+ "name": "NV_PFB_FBPA_SPARE2"
+ },
+ "0xd0": {
+ "name": "NV_PFB_FBPA_REPLAY_RETRY_SUBP0"
+ },
+ "0xd4": {
+ "name": "NV_PFB_FBPA_REPLAY_RETRY_SUBP1"
+ },
+ "0xd8": {
+ "name": "NV_PFB_FBPA_REPLAY_CYCLES_SUBP0"
+ },
+ "0xdc": {
+ "name": "NV_PFB_FBPA_REPLAY_CYCLES_SUBP1"
+ },
+ "0x100": {
+ "name": "NV_PFB_FBPA_PM"
+ },
+ "0x148": {
+ "name": "NV_PFB_FBPA_HISEC_PRIV_LEVEL_MASK",
+ "fields": {
+ "READ_PROTECTION": [
+ 3,
+ 0
+ ],
+ "READ_PROTECTION_LEVEL0": [
+ 0,
+ 0
+ ],
+ "READ_PROTECTION_LEVEL1": [
+ 1,
+ 1
+ ],
+ "READ_PROTECTION_LEVEL2": [
+ 2,
+ 2
+ ],
+ "READ_PROTECTION_LEVEL3": [
+ 3,
+ 3
+ ],
+ "WRITE_PROTECTION": [
+ 7,
+ 4
+ ],
+ "WRITE_PROTECTION_LEVEL0": [
+ 4,
+ 4
+ ],
+ "WRITE_PROTECTION_LEVEL1": [
+ 5,
+ 5
+ ],
+ "WRITE_PROTECTION_LEVEL2": [
+ 6,
+ 6
+ ],
+ "WRITE_PROTECTION_LEVEL3": [
+ 7,
+ 7
+ ],
+ "READ_VIOLATION": [
+ 8,
+ 8
+ ],
+ "WRITE_VIOLATION": [
+ 9,
+ 9
+ ],
+ "SOURCE_READ_CONTROL": [
+ 10,
+ 10
+ ],
+ "SOURCE_WRITE_CONTROL": [
+ 11,
+ 11
+ ],
+ "SOURCE_ENABLE": [
+ 31,
+ 12
+ ]
+ }
+ },
+ "0x150": {
+ "name": "NV_PFB_FBPA_ASRCTRL_NONISO"
+ },
+ "0x154": {
+ "name": "NV_PFB_FBPA_TESTCMD_EXT_1"
+ },
+ "0x158": {
+ "name": "NV_PFB_FBPA_PTR_RESET"
+ },
+ "0x15c": {
+ "name": "NV_PFB_FBPA_CONFIG12"
+ },
+ "0x160": {
+ "name": "NV_PFB_FBPA_MISC_CTRL",
+ "fields": {
+ "INTERVAL_TIMER_PERIOD": [
+ 2,
+ 0
+ ],
+ "INTERVAL_TIMER_EN": [
+ 4,
+ 4
+ ]
+ }
+ },
+ "0x164": {
+ "name": "NV_PFB_FBPA_NUM_ACTIVE_FBPS"
+ },
+ "0x168": {
+ "name": "NV_PFB_FBPA_MEM_PRIV_LEVEL_MASK",
+ "fields": {
+ "READ_PROTECTION": [
+ 3,
+ 0
+ ],
+ "READ_PROTECTION_LEVEL0": [
+ 0,
+ 0
+ ],
+ "READ_PROTECTION_LEVEL1": [
+ 1,
+ 1
+ ],
+ "READ_PROTECTION_LEVEL2": [
+ 2,
+ 2
+ ],
+ "READ_PROTECTION_LEVEL3": [
+ 3,
+ 3
+ ],
+ "WRITE_PROTECTION": [
+ 7,
+ 4
+ ],
+ "WRITE_PROTECTION_LEVEL0": [
+ 4,
+ 4
+ ],
+ "WRITE_PROTECTION_LEVEL1": [
+ 5,
+ 5
+ ],
+ "WRITE_PROTECTION_LEVEL2": [
+ 6,
+ 6
+ ],
+ "WRITE_PROTECTION_LEVEL3": [
+ 7,
+ 7
+ ],
+ "READ_VIOLATION": [
+ 8,
+ 8
+ ],
+ "WRITE_VIOLATION": [
+ 9,
+ 9
+ ],
+ "SOURCE_READ_CONTROL": [
+ 10,
+ 10
+ ],
+ "SOURCE_WRITE_CONTROL": [
+ 11,
+ 11
+ ],
+ "SOURCE_ENABLE": [
+ 31,
+ 12
+ ]
+ }
+ },
+ "0x16c": {
+ "name": "NV_PFB_FBPA_NUM_ACTIVE_LTCS"
+ },
+ "0x170": {
+ "name": "NV_PFB_FBPA_ECC_PRIV_LEVEL_MASK",
+ "fields": {
+ "READ_PROTECTION": [
+ 3,
+ 0
+ ],
+ "READ_PROTECTION_LEVEL0": [
+ 0,
+ 0
+ ],
+ "READ_PROTECTION_LEVEL1": [
+ 1,
+ 1
+ ],
+ "READ_PROTECTION_LEVEL2": [
+ 2,
+ 2
+ ],
+ "READ_PROTECTION_LEVEL3": [
+ 3,
+ 3
+ ],
+ "WRITE_PROTECTION": [
+ 7,
+ 4
+ ],
+ "WRITE_PROTECTION_LEVEL0": [
+ 4,
+ 4
+ ],
+ "WRITE_PROTECTION_LEVEL1": [
+ 5,
+ 5
+ ],
+ "WRITE_PROTECTION_LEVEL2": [
+ 6,
+ 6
+ ],
+ "WRITE_PROTECTION_LEVEL3": [
+ 7,
+ 7
+ ],
+ "READ_VIOLATION": [
+ 8,
+ 8
+ ],
+ "WRITE_VIOLATION": [
+ 9,
+ 9
+ ],
+ "SOURCE_READ_CONTROL": [
+ 10,
+ 10
+ ],
+ "SOURCE_WRITE_CONTROL": [
+ 11,
+ 11
+ ],
+ "SOURCE_ENABLE": [
+ 31,
+ 12
+ ]
+ }
+ },
+ "0x174": {
+ "name": "NV_PFB_FBPA_WPRE0"
+ },
+ "0x178": {
+ "name": "NV_PFB_FBPA_WPRE1"
+ },
+ "0x17c": {
+ "name": "NV_PFB_FBPA_WPRE2"
+ },
+ "0x1d8": {
+ "name": "NV_PFB_FBPA_PM_CAPTURE_CTL"
+ },
+ "0x200": {
+ "name": "NV_PFB_FBPA_CFG0",
+ "fields": {
+ "BL": [
+ 1,
+ 0
+ ],
+ "EXTBANK": [
+ 2,
+ 2
+ ],
+ "X8": [
+ 4,
+ 4
+ ],
+ "CA_RETURN_TO_ZERO": [
+ 5,
+ 5
+ ],
+ "HBM_CMD_PARITY_OVERRIDE": [
+ 6,
+ 6
+ ],
+ "EDC_RATE": [
+ 7,
+ 7
+ ],
+ "GAPLESS_WTR": [
+ 8,
+ 8
+ ],
+ "DRAM_ACPD": [
+ 11,
+ 11
+ ],
+ "AUTO_ATR": [
+ 12,
+ 12
+ ],
+ "DATA_SCRAMBLE": [
+ 13,
+ 13
+ ],
+ "AUTO_SYNC": [
+ 14,
+ 14
+ ],
+ "LOW_POWER": [
+ 15,
+ 15
+ ],
+ "ABI": [
+ 16,
+ 16
+ ],
+ "BURST_LENGTH": [
+ 17,
+ 17
+ ],
+ "DM_ON_ADR": [
+ 18,
+ 18
+ ],
+ "TIMING_UNIT": [
+ 19,
+ 19
+ ],
+ "CMD_PARITY": [
+ 20,
+ 20
+ ],
+ "AUTO_ODT": [
+ 21,
+ 21
+ ],
+ "CLAMSHELL": [
+ 22,
+ 22
+ ],
+ "EDC_EARLY_VALUE": [
+ 23,
+ 23
+ ],
+ "BG_8": [
+ 24,
+ 24
+ ],
+ "AUTO_PRE": [
+ 25,
+ 25
+ ],
+ "SAME_QUSE_TWO_RANKS": [
+ 26,
+ 26
+ ],
+ "RDEDC": [
+ 27,
+ 27
+ ],
+ "WREDC": [
+ 28,
+ 28
+ ],
+ "REPLAY": [
+ 29,
+ 29
+ ],
+ "MS01_ECC": [
+ 30,
+ 30
+ ],
+ "DRAM_ACK": [
+ 31,
+ 31
+ ]
+ }
+ },
+ "0x204": {
+ "name": "NV_PFB_FBPA_CFG1",
+ "fields": {
+ "SUBP": [
+ 1,
+ 0
+ ],
+ "AP": [
+ 6,
+ 4
+ ],
+ "COL": [
+ 15,
+ 12
+ ],
+ "ROWA": [
+ 19,
+ 16
+ ],
+ "ROWB": [
+ 23,
+ 20
+ ],
+ "BANK": [
+ 25,
+ 24
+ ],
+ "BG_8_OVR": [
+ 26,
+ 26
+ ],
+ "NUM_BANKS_PER_BG": [
+ 28,
+ 27
+ ],
+ "NUM_BANKS_PER_ROW_BG": [
+ 30,
+ 29
+ ]
+ }
+ },
+ "0x20c": {
+ "name": "NV_PFB_FBPA_CSTATUS"
+ },
+ "0x210": {
+ "name": "NV_PFB_FBPA_REFCTRL",
+ "fields": {
+ "PUT": [
+ 6,
+ 0
+ ],
+ "GET": [
+ 14,
+ 8
+ ],
+ "RESET": [
+ 16,
+ 16
+ ],
+ "VALID": [
+ 31,
+ 31
+ ]
+ }
+ },
+ "0x214": {
+ "name": "NV_PFB_FBPA_ZQ"
+ },
+ "0x218": {
+ "name": "NV_PFB_FBPA_PIN"
+ },
+ "0x21c": {
+ "name": "NV_PFB_FBPA_PAD"
+ },
+ "0x220": {
+ "name": "NV_PFB_FBPA_TIMING0"
+ },
+ "0x224": {
+ "name": "NV_PFB_FBPA_TIMING1"
+ },
+ "0x228": {
+ "name": "NV_PFB_FBPA_TIMING2"
+ },
+ "0x22c": {
+ "name": "NV_PFB_FBPA_TIMING3"
+ },
+ "0x230": {
+ "name": "NV_PFB_FBPA_TIMING4"
+ },
+ "0x234": {
+ "name": "NV_PFB_FBPA_TIMING5"
+ },
+ "0x238": {
+ "name": "NV_PFB_FBPA_TIMING6"
+ },
+ "0x23c": {
+ "name": "NV_PFB_FBPA_TIMING7"
+ },
+ "0x240": {
+ "name": "NV_PFB_FBPA_TIMING8"
+ },
+ "0x244": {
+ "name": "NV_PFB_FBPA_TIMING9"
+ },
+ "0x248": {
+ "name": "NV_PFB_FBPA_TIMING10"
+ },
+ "0x24c": {
+ "name": "NV_PFB_FBPA_TIMING11"
+ },
+ "0x250": {
+ "name": "NV_PFB_FBPA_TIMING12"
+ },
+ "0x254": {
+ "name": "NV_PFB_FBPA_TIMING13"
+ },
+ "0x258": {
+ "name": "NV_PFB_FBPA_TIMING14"
+ },
+ "0x25c": {
+ "name": "NV_PFB_FBPA_TIMING15"
+ },
+ "0x260": {
+ "name": "NV_PFB_FBPA_TIMING16"
+ },
+ "0x264": {
+ "name": "NV_PFB_FBPA_TIMING17"
+ },
+ "0x268": {
+ "name": "NV_PFB_FBPA_TIMING18"
+ },
+ "0x26c": {
+ "name": "NV_PFB_FBPA_TIMING19"
+ },
+ "0x280": {
+ "name": "NV_PFB_FBPA_CG1"
+ },
+ "0x284": {
+ "name": "NV_PFB_FBPA_CG2"
+ },
+ "0x288": {
+ "name": "NV_PFB_FBPA_TIMING20_GEN"
+ },
+ "0x28c": {
+ "name": "NV_PFB_FBPA_TIMING20"
+ },
+ "0x290": {
+ "name": "NV_PFB_FBPA_CONFIG0"
+ },
+ "0x294": {
+ "name": "NV_PFB_FBPA_CONFIG1"
+ },
+ "0x298": {
+ "name": "NV_PFB_FBPA_CONFIG2"
+ },
+ "0x29c": {
+ "name": "NV_PFB_FBPA_CONFIG3"
+ },
+ "0x2a0": {
+ "name": "NV_PFB_FBPA_CONFIG4"
+ },
+ "0x2a4": {
+ "name": "NV_PFB_FBPA_CONFIG5"
+ },
+ "0x2a8": {
+ "name": "NV_PFB_FBPA_CONFIG6"
+ },
+ "0x2ac": {
+ "name": "NV_PFB_FBPA_CONFIG7"
+ },
+ "0x2b0": {
+ "name": "NV_PFB_FBPA_TIMING0_GEN"
+ },
+ "0x2b4": {
+ "name": "NV_PFB_FBPA_TIMING1_GEN"
+ },
+ "0x2b8": {
+ "name": "NV_PFB_FBPA_TIMING2_GEN"
+ },
+ "0x2bc": {
+ "name": "NV_PFB_FBPA_TIMING3_GEN"
+ },
+ "0x2c0": {
+ "name": "NV_PFB_FBPA_TIMING4_GEN"
+ },
+ "0x2c4": {
+ "name": "NV_PFB_FBPA_TIMING5_GEN"
+ },
+ "0x2c8": {
+ "name": "NV_PFB_FBPA_TIMING6_GEN"
+ },
+ "0x2cc": {
+ "name": "NV_PFB_FBPA_CONFIG8"
+ },
+ "0x2d0": {
+ "name": "NV_PFB_FBPA_TIMING7_GEN"
+ },
+ "0x2d4": {
+ "name": "NV_PFB_FBPA_TIMING8_GEN"
+ },
+ "0x2d8": {
+ "name": "NV_PFB_FBPA_TIMING9_GEN"
+ },
+ "0x2dc": {
+ "name": "NV_PFB_FBPA_TIMING15_GEN"
+ },
+ "0x2e0": {
+ "name": "NV_PFB_FBPA_TIMING16_GEN"
+ },
+ "0x2e4": {
+ "name": "NV_PFB_FBPA_TIMING17_GEN"
+ },
+ "0x2e8": {
+ "name": "NV_PFB_FBPA_CONFIG9"
+ },
+ "0x2ec": {
+ "name": "NV_PFB_FBPA_TIMING18_GEN"
+ },
+ "0x2f0": {
+ "name": "NV_PFB_FBPA_TIMING19_GEN"
+ },
+ "0x2f4": {
+ "name": "NV_PFB_FBPA_CONFIG10"
+ },
+ "0x2f8": {
+ "name": "NV_PFB_FBPA_ASR_WAKEUP"
+ },
+ "0x2fc": {
+ "name": "NV_PFB_FBPA_DRAM_ASR"
+ },
+ "0x300": {
+ "name": "NV_PFB_FBPA_GENERIC_MRS"
+ }
+ },
+ "windows": [
+ {"base": "0x20000000009A0000", "size": "0x4000", "label": "fbpa"},
+ {"base": "0x2000000000900000", "size": "0x4000", "label": "fbpa-partition", "count": 16, "stride": "0x4000"},
+ {"base": "0x2000000000980000", "size": "0x4000", "label": "fbpa-mc", "count": 3, "stride": "0x4000"}
+ ]
+ },
"dma": {
"evidence": "GSP RISC-V address map: SYSGPA/FBGPA apertures. In this flat model both apertures alias simulator RAM (documented assumption) so booter-style DMA and firmware aperture reads resolve; unmapped transfers are refused and recorded.",
"windows": [
@@ -80,7 +882,7 @@
"boot_shim": {
"note": "Reset-time state the real SEC2 booter/host provides. Every value here is an assumption recorded in the session; the device model never fakes a register read.",
"registers": {
- "a0": "0x80000",
+ "a0": "0x81000",
"a1": "0x80000",
"a2": "0x4000000",
"a3": "0x1b34ea0",
@@ -92,7 +894,16 @@
"core_dump_pa": "0x40000",
"core_dump_size": "0x10000",
"wpr_meta_pa": "0x90000",
- "image_at": "0x100000000",
+ "bootstrap_args": {
+ "pa": "0x81000",
+ "imem_base_pa": "0x0",
+ "imem_size": "0x10000",
+ "imem_bs_size": "0x0",
+ "dmem_base_pa": "0x100000000",
+ "dmem_size": "0x10000",
+ "dmem_bs_size": "0x0",
+ "evidence": "libos bootstrap-args layout (IMEM/DMEM base+size+bs, passed in a0 by the bootloader); the M-mode boot path (mstatus.VM=NVMPU, observed) uses the non-sep-kern TCM configuration (offsets 0, 64 KiB). Values are declared assumptions, not silicon reads."
+ },
"wpr": {
"elf_pa": "0x4000000",
"elf_size": "0x1b34ea0",