diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-02 10:58:21 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-10-02 10:58:21 -0400 |
| commit | 002e3d7d13c414ea874abbee8d82803dd3a72fe5 (patch) | |
| tree | 6627c12fa07f6a1cf73e5aaeb7390e698dd3a896 /debug/risc-v-gsp-rm-vm/sim | |
| parent | 2f249817df327f5dcccf0464ad517fa60d6071d3 (diff) | |
| download | open-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.tar.gz open-iov-002e3d7d13c414ea874abbee8d82803dd3a72fe5.zip | |
debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/sim')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/__init__.py | 43 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/decode.py | 399 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/machine.py | 825 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/sim/trace.py | 145 |
4 files changed, 1412 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/sim/__init__.py b/debug/risc-v-gsp-rm-vm/sim/__init__.py new file mode 100644 index 0000000..5d7ab1f --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/__init__.py @@ -0,0 +1,43 @@ +"""green-eye native RV64IMAC simulator (M1). + +Pure-Python, deterministic, stdlib-only. :class:`sim.machine.Machine` executes +RV64IMAC from a 256 MiB RAM region with an NS16550A UART; the debugger-facing +adapter lives in ``greeneye.sim_backend``. A machine can be given a +``greeneye.device_map.DeviceMap`` and ``unknown_mmio="lenient"`` to serve +unmodelled MMIO accesses from harvested first-touch data instead of faulting. +""" +from .decode import decode16, decode32 # noqa: F401 +from .machine import ( # noqa: F401 + R_BP, + R_BUDGET, + R_EBREAK, + R_ECALL, + R_FAULT, + R_STEP, + R_WATCH, + UNKNOWN_MMIO_MODES, + Fault, + Machine, + SimMachine, + Uart16550, +) +from .trace import Trace, make_trace # noqa: F401 + +__all__ = [ + "Machine", + "SimMachine", + "Uart16550", + "Fault", + "decode16", + "decode32", + "Trace", + "make_trace", + "R_BUDGET", + "R_BP", + "R_WATCH", + "R_EBREAK", + "R_FAULT", + "R_STEP", + "R_ECALL", + "UNKNOWN_MMIO_MODES", +] diff --git a/debug/risc-v-gsp-rm-vm/sim/decode.py b/debug/risc-v-gsp-rm-vm/sim/decode.py new file mode 100644 index 0000000..0f3ef3a --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/decode.py @@ -0,0 +1,399 @@ +"""RV64IMAC instruction decoder (pure Python, stdlib only). + +The decoder canonicalises every compressed instruction into the equivalent +32-bit operation, so breakpoints, single-step and watchpoints behave uniformly +regardless of encoding. ``decode16``/``decode32`` return a 7-tuple + + (kind, sub, rd, rs1, rs2, imm, next_pc) + +where ``next_pc`` is the fall-through (or branch/jump target for the +unconditional control transfers), ``imm`` is already sign/zero-normalised for +its use, and branch/jump targets are absolute virtual addresses. Results are +cached per PC by the Machine; the cache is dropped when the debugger writes +memory (M1 does not model self-modifying code). +""" +from __future__ import annotations + +MASK64 = (1 << 64) - 1 + +# Instruction kinds. +( + K_LUI, + K_AUIPC, + K_JAL, + K_JALR, + K_BRANCH, + K_LOAD, + K_STORE, + K_OPI, + K_OP, + K_OPI32, + K_OP32, + K_FENCE, + K_CSR, + K_ECALL, + K_EBREAK, + K_MRET, + K_AMO, + K_ILLEGAL, +) = range(18) + +# ALU ops shared by K_OP and (width-truncated) K_OP32. +( + A_ADD, + A_SLL, + A_SLT, + A_SLTU, + A_XOR, + A_SRL, + A_OR, + A_AND, + A_SUB, + A_SRA, + A_MUL, + A_MULH, + A_MULHSU, + A_MULHU, + A_DIV, + A_DIVU, + A_REM, + A_REMU, +) = range(18) + +# K_OPI / K_OPI32 immediate ops (funct3 values, with srai/sraiw offset). +I_ADDI, I_SLLI, I_SLTI, I_SLTIU, I_XORI, I_SRLI, I_ORI, I_ANDI = range(8) +I_SRAI = 0x15 +I_ADDIW, I_SLLIW, I_SRLIW, I_SRAIW = 0, 1, 5, 0x15 + +# LOAD/STORE funct3. +LOAD_SIZE = (1, 2, 4, 8, 1, 2, 4) +STORE_SIZE = (1, 2, 4, 8) + +_ILLEGAL = (K_ILLEGAL, 0, 0, 0, 0, 0, 0) + + +def sext(value: int, bits: int) -> int: + sign = 1 << (bits - 1) + return (value & (sign - 1)) - (value & sign) + + +def _bits(value: int, hi: int, lo: int) -> int: + return (value >> lo) & ((1 << (hi - lo + 1)) - 1) + + +def _op_alu(sub: int) -> int: + """Map an R-type funct3 to the shared ALU op code (non-M).""" + return { + 0: A_ADD, + 1: A_SLL, + 2: A_SLT, + 3: A_SLTU, + 4: A_XOR, + 5: A_SRL, + 6: A_OR, + 7: A_AND, + }[sub] + + +def decode32(pc: int, insn: int): + opcode = insn & 0x7F + rd = (insn >> 7) & 0x1F + funct3 = (insn >> 12) & 7 + rs1 = (insn >> 15) & 0x1F + rs2 = (insn >> 20) & 0x1F + funct7 = (insn >> 25) & 0x7F + nxt = pc + 4 + + if opcode == 0x37: # LUI + return (K_LUI, 0, rd, 0, 0, (insn & 0xFFFFF000) & MASK64, nxt) + if opcode == 0x17: # AUIPC + return (K_AUIPC, 0, rd, 0, 0, sext(insn & 0xFFFFF000, 32) & MASK64, nxt) + if opcode == 0x6F: # JAL + imm = sext( + (_bits(insn, 31, 31) << 20) + | (_bits(insn, 30, 21) << 1) + | (_bits(insn, 20, 20) << 11) + | (_bits(insn, 19, 12) << 12), + 21, + ) + return (K_JAL, 0, rd, 0, 0, (pc + imm) & MASK64, nxt) + if opcode == 0x67: # JALR + return (K_JALR, funct3, rd, rs1, 0, sext(_bits(insn, 31, 20), 12), nxt) + if opcode == 0x63: # BRANCH + imm = sext( + (_bits(insn, 31, 31) << 12) + | (_bits(insn, 30, 25) << 5) + | (_bits(insn, 11, 8) << 1) + | (_bits(insn, 7, 7) << 11), + 13, + ) + return (K_BRANCH, funct3, 0, rs1, rs2, (pc + imm) & MASK64, nxt) + if opcode == 0x03: # LOAD + if funct3 == 7: + return _ILLEGAL + return (K_LOAD, funct3, rd, rs1, 0, sext(_bits(insn, 31, 20), 12), nxt) + if opcode == 0x23: # STORE + if funct3 > 3: + return _ILLEGAL + imm = sext((_bits(insn, 31, 25) << 5) | _bits(insn, 11, 7), 12) + return (K_STORE, funct3, 0, rs1, rs2, imm, nxt) + if opcode == 0x13: # OP-IMM + shamt = (insn >> 20) & 0x3F + top = (insn >> 26) & 0x3F + if funct3 == 1: + if top: + return _ILLEGAL + return (K_OPI, I_SLLI, rd, rs1, 0, shamt, nxt) + if funct3 == 5: + if top == 0: + sub = I_SRLI + elif top == 0x10: + sub = I_SRAI + else: + return _ILLEGAL + return (K_OPI, sub, rd, rs1, 0, shamt, nxt) + return (K_OPI, funct3, rd, rs1, 0, sext(_bits(insn, 31, 20), 12), nxt) + if opcode == 0x33: # OP + if funct7 == 0x01: # M extension + return (K_OP, A_MUL + funct3, rd, rs1, rs2, 0, nxt) + if funct7 == 0x00: + sub = _op_alu(funct3) + elif funct7 == 0x20 and funct3 == 0: + sub = A_SUB + elif funct7 == 0x20 and funct3 == 5: + sub = A_SRA + else: + return _ILLEGAL + return (K_OP, sub, rd, rs1, rs2, 0, nxt) + if opcode == 0x1B: # OP-IMM-32 + if funct3 == 0: + return (K_OPI32, I_ADDIW, rd, rs1, 0, sext(_bits(insn, 31, 20), 12), nxt) + if funct3 == 1: + if (insn >> 25) & 0x7F: + return _ILLEGAL + return (K_OPI32, I_SLLIW, rd, rs1, 0, (insn >> 20) & 0x1F, nxt) + if funct3 == 5: + top = (insn >> 25) & 0x7F + if top == 0: + sub = I_SRLIW + elif top == 0x20: + sub = I_SRAIW + else: + return _ILLEGAL + return (K_OPI32, sub, rd, rs1, 0, (insn >> 20) & 0x1F, nxt) + return _ILLEGAL + if opcode == 0x3B: # OP-32 + if funct7 == 0x00: + if funct3 == 0: + sub = A_ADD + elif funct3 == 1: + sub = A_SLL + elif funct3 == 5: + sub = A_SRL + else: + return _ILLEGAL + elif funct7 == 0x20: + if funct3 == 0: + sub = A_SUB + elif funct3 == 5: + sub = A_SRA + else: + return _ILLEGAL + elif funct7 == 0x01: + if funct3 == 0: + sub = A_MUL + elif funct3 == 4: + sub = A_DIV + elif funct3 == 5: + sub = A_DIVU + elif funct3 == 6: + sub = A_REM + elif funct3 == 7: + sub = A_REMU + else: + return _ILLEGAL + else: + return _ILLEGAL + return (K_OP32, sub, rd, rs1, rs2, 0, nxt) + if opcode == 0x0F: # FENCE / FENCE.I: ordering is not modelled (single core) + if funct3 in (0, 1): + return (K_FENCE, 0, 0, 0, 0, 0, nxt) + return _ILLEGAL + if opcode == 0x73: # SYSTEM + if funct3 == 0: + imm12 = insn >> 20 + if imm12 == 0: + return (K_ECALL, 0, 0, 0, 0, 0, nxt) + if imm12 == 1: + return (K_EBREAK, 0, 0, 0, 0, 0, nxt) + if imm12 in (0x302, 0x102): # mret / sret (treated alike, M1) + return (K_MRET, 0, 0, 0, 0, 0, nxt) + return _ILLEGAL + return (K_CSR, funct3, rd, rs1, 0, insn >> 20, nxt) + if opcode == 0x2F: # AMO + if funct3 not in (2, 3): + return _ILLEGAL + funct5 = funct7 >> 2 + if funct5 not in (0x00, 0x01, 0x02, 0x03, 0x04, 0x08, 0x0C, 0x10, 0x14, 0x18, 0x1C): + return _ILLEGAL + sub = funct5 | (0x40 if funct3 == 3 else 0) + return (K_AMO, sub, rd, rs1, rs2, 0, nxt) + return _ILLEGAL + + +def _rvc_j_imm(insn: int) -> int: + return sext( + (_bits(insn, 12, 12) << 11) + | (_bits(insn, 11, 11) << 4) + | (_bits(insn, 10, 9) << 8) + | (_bits(insn, 8, 8) << 10) + | (_bits(insn, 7, 7) << 6) + | (_bits(insn, 6, 6) << 7) + | (_bits(insn, 5, 3) << 1) + | (_bits(insn, 2, 2) << 5), + 12, + ) + + +def _rvc_b_imm(insn: int) -> int: + return sext( + (_bits(insn, 12, 12) << 8) + | (_bits(insn, 11, 10) << 3) + | (_bits(insn, 6, 5) << 6) + | (_bits(insn, 4, 3) << 1) + | (_bits(insn, 2, 2) << 5), + 9, + ) + + +def decode16(pc: int, insn: int): + op = insn & 3 + funct3 = (insn >> 13) & 7 + nxt = pc + 2 + rd = (insn >> 7) & 0x1F + + if op == 0: + if funct3 == 0: # c.addi4spn + imm = ( + ((insn >> 7) & 0x30) + | ((insn >> 1) & 0x3C0) + | ((insn >> 4) & 0x4) + | ((insn >> 2) & 0x8) + ) + if imm == 0: + return _ILLEGAL + return (K_OPI, I_ADDI, 8 + ((insn >> 2) & 7), 2, 0, imm, nxt) + if funct3 == 2: # c.lw + imm = ((insn >> 7) & 0x38) | ((insn >> 4) & 0x4) | ((insn << 1) & 0x40) + return (K_LOAD, 2, 8 + ((insn >> 2) & 7), 8 + ((insn >> 7) & 7), 0, imm, nxt) + if funct3 == 3: # c.ld + imm = ((insn >> 7) & 0x38) | ((insn << 1) & 0xC0) + return (K_LOAD, 3, 8 + ((insn >> 2) & 7), 8 + ((insn >> 7) & 7), 0, imm, nxt) + if funct3 == 6: # c.sw + imm = ((insn >> 7) & 0x38) | ((insn >> 4) & 0x4) | ((insn << 1) & 0x40) + return (K_STORE, 2, 0, 8 + ((insn >> 7) & 7), 8 + ((insn >> 2) & 7), imm, nxt) + if funct3 == 7: # c.sd + imm = ((insn >> 7) & 0x38) | ((insn << 1) & 0xC0) + return (K_STORE, 3, 0, 8 + ((insn >> 7) & 7), 8 + ((insn >> 2) & 7), imm, nxt) + return _ILLEGAL + + if op == 1: + if funct3 == 0: # c.addi / c.nop + imm = sext((_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2), 6) + return (K_OPI, I_ADDI, rd, rd, 0, imm, nxt) + if funct3 == 1: # c.addiw + if rd == 0: + return _ILLEGAL + imm = sext((_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2), 6) + return (K_OPI32, I_ADDIW, rd, rd, 0, imm, nxt) + if funct3 == 2: # c.li + imm = sext((_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2), 6) + return (K_OPI, I_ADDI, rd, 0, 0, imm, nxt) + if funct3 == 3: + if rd == 2: # c.addi16sp + imm = sext( + ((insn >> 3) & 0x200) + | ((insn >> 2) & 0x10) + | ((insn << 1) & 0x40) + | ((insn << 4) & 0x180) + | ((insn << 3) & 0x20), + 10, + ) + if imm == 0: + return _ILLEGAL + return (K_OPI, I_ADDI, 2, 2, 0, imm, nxt) + imm6 = sext((_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2), 6) + if rd == 0 or imm6 == 0: # c.lui + return _ILLEGAL + return (K_LUI, 0, rd, 0, 0, (imm6 << 12) & MASK64, nxt) + if funct3 == 4: + sub3 = (insn >> 10) & 3 + rd_p = 8 + ((insn >> 7) & 7) + if sub3 == 0: # c.srli + shamt = (_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2) + return (K_OPI, I_SRLI, rd_p, rd_p, 0, shamt, nxt) + if sub3 == 1: # c.srai + shamt = (_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2) + return (K_OPI, I_SRAI, rd_p, rd_p, 0, shamt, nxt) + if sub3 == 2: # c.andi + imm = sext((_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2), 6) + return (K_OPI, I_ANDI, rd_p, rd_p, 0, imm, nxt) + f2 = (insn >> 5) & 3 + if _bits(insn, 12, 12) == 0: + sub = (A_SUB, A_XOR, A_OR, A_AND)[f2] + return (K_OP, sub, rd_p, rd_p, 8 + ((insn >> 2) & 7), 0, nxt) + if f2 == 0: # c.subw + return (K_OP32, A_SUB, rd_p, rd_p, 8 + ((insn >> 2) & 7), 0, nxt) + if f2 == 1: # c.addw + return (K_OP32, A_ADD, rd_p, rd_p, 8 + ((insn >> 2) & 7), 0, nxt) + return _ILLEGAL + if funct3 == 5: # c.j + return (K_JAL, 0, 0, 0, 0, (pc + _rvc_j_imm(insn)) & MASK64, nxt) + if funct3 == 6: # c.beqz + return (K_BRANCH, 0, 0, 8 + ((insn >> 7) & 7), 0, (pc + _rvc_b_imm(insn)) & MASK64, nxt) + if funct3 == 7: # c.bnez + return (K_BRANCH, 1, 0, 8 + ((insn >> 7) & 7), 0, (pc + _rvc_b_imm(insn)) & MASK64, nxt) + return _ILLEGAL + + if op == 2: + if funct3 == 0: # c.slli + if rd == 0: + return _ILLEGAL + shamt = (_bits(insn, 12, 12) << 5) | _bits(insn, 6, 2) + return (K_OPI, I_SLLI, rd, rd, 0, shamt, nxt) + if funct3 == 2: # c.lwsp + if rd == 0: + return _ILLEGAL + imm = ((insn >> 7) & 0x20) | ((insn >> 2) & 0x1C) | ((insn << 4) & 0xC0) + return (K_LOAD, 2, rd, 2, 0, imm, nxt) + if funct3 == 3: # c.ldsp + if rd == 0: + return _ILLEGAL + imm = ((insn >> 7) & 0x20) | ((insn >> 2) & 0x18) | ((insn << 4) & 0x1C0) + return (K_LOAD, 3, rd, 2, 0, imm, nxt) + if funct3 == 4: + rs2 = (insn >> 2) & 0x1F + if _bits(insn, 12, 12) == 0: + if rs2 == 0: # c.jr + if rd == 0: + return _ILLEGAL + return (K_JALR, 0, 0, rd, 0, 0, nxt) + if rd == 0: # c.mv + return _ILLEGAL + return (K_OP, A_ADD, rd, 0, rs2, 0, nxt) + if rs2 == 0 and rd == 0: # c.ebreak + return (K_EBREAK, 0, 0, 0, 0, 0, nxt) + if rs2 == 0: # c.jalr + if rd == 0: + return _ILLEGAL + return (K_JALR, 0, 1, rd, 0, 0, nxt) + return (K_OP, A_ADD, rd, rd, rs2, 0, nxt) # c.add + if funct3 == 6: # c.swsp + imm = ((insn >> 7) & 0x3C) | ((insn >> 1) & 0xC0) + return (K_STORE, 2, 0, 2, (insn >> 2) & 0x1F, imm, nxt) + if funct3 == 7: # c.sdsp + imm = ((insn >> 7) & 0x38) | ((insn >> 1) & 0x1C0) + return (K_STORE, 3, 0, 2, (insn >> 2) & 0x1F, imm, nxt) + return _ILLEGAL + + return _ILLEGAL diff --git a/debug/risc-v-gsp-rm-vm/sim/machine.py b/debug/risc-v-gsp-rm-vm/sim/machine.py new file mode 100644 index 0000000..8d77dd8 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/machine.py @@ -0,0 +1,825 @@ +"""Deterministic RV64IMAC machine: RAM, NS16550A UART, debugger-visible state. + +The execution loop is a monolithic dispatch over the cached decode tuples from +``sim.decode``. Everything guest-visible is a pure function of the loaded +image and the instruction budget handed to ``run``: no host clock, no threads, +no entropy. RAM is an anonymous ``mmap`` (lazily paged, so a 256 MiB region is +cheap); all other addresses resolve to the UART or fault. +""" +from __future__ import annotations + +import mmap +import struct + +from .decode import ( + A_ADD, + A_AND, + A_DIV, + A_DIVU, + A_MUL, + A_MULH, + A_MULHSU, + A_MULHU, + A_OR, + A_REM, + A_REMU, + A_SLL, + A_SLT, + A_SLTU, + A_SRA, + A_SRL, + A_SUB, + A_XOR, + I_ADDI, + I_ADDIW, + I_ANDI, + I_ORI, + I_SLLI, + I_SLLIW, + I_SLTI, + I_SLTIU, + I_SRAI, + I_SRAIW, + I_SRLI, + I_SRLIW, + I_XORI, + K_AMO, + K_AUIPC, + K_BRANCH, + K_CSR, + K_EBREAK, + K_ECALL, + K_FENCE, + K_ILLEGAL, + K_JAL, + K_JALR, + K_LOAD, + K_LUI, + K_MRET, + K_OP, + K_OP32, + K_OPI, + K_OPI32, + K_STORE, + LOAD_SIZE, + MASK64, + STORE_SIZE, + decode16, + decode32, +) + +# Stop reasons returned by Machine.run(). +R_BUDGET = 0 +R_BP = 1 +R_WATCH = 2 +R_EBREAK = 3 +R_FAULT = 4 +R_STEP = 5 +R_ECALL = 6 + +#: Accepted ``unknown_mmio`` policies for accesses outside RAM/UART/the map. +UNKNOWN_MMIO_MODES = ("fault", "lenient") + +UART_BASE = 0x10000000 +UART_SIZE = 0x100 + +# RV64I M A C advertised in misa (MXL=2, I=bit8, M=bit12, A=bit0, C=bit2). +MISA_RV64IMAC = 0x8000000000001105 + + +class Fault(Exception): + """Guest-visible trap: unmapped access or illegal instruction.""" + + def __init__(self, cause: int, tval: int = 0): + super().__init__(f"cause={cause:#x} tval={tval:#x}") + self.cause = cause + self.tval = tval + + +class Uart16550: + """Behavioural NS16550A: TX is captured, LSR always reports THR empty.""" + + def __init__(self): + self.tx = bytearray() + self.ier = 0 + self.lcr = 0 + self.mcr = 0 + self.scr = 0 + self.dll = 0 + self.dlm = 0 + + def read(self, off: int, size: int = 1) -> int: + if off == 0: + return self.dll # RBR (no RX modelled) / DLL when DLAB=1 + if off == 1: + return self.ier + if off == 2: + return 0x01 # IIR: no pending interrupt + if off == 3: + return self.lcr + if off == 4: + return self.mcr + if off == 5: + return 0x60 # LSR: THRE | TEMT + if off == 6: + return 0xB0 # MSR: CTS|DSR|DCD + if off == 7: + return self.scr + return 0 + + def write(self, off: int, value: int, size: int = 1) -> None: + value &= 0xFF + if off == 0: + if self.lcr & 0x80: + self.dll = value + else: + self.tx.append(value) + elif off == 1: + self.ier = value + elif off == 3: + self.lcr = value + elif off == 4: + self.mcr = value + elif off == 7: + self.scr = value + # FCR (2) and reserved offsets are write-only/no-ops. + + def text(self) -> str: + return self.tx.decode("utf-8", "replace") + + +def _signed64(value: int) -> int: + return value - 0x10000000000000000 if value & 0x8000000000000000 else value + + +def _signed32(value: int) -> int: + value &= 0xFFFFFFFF + return value - 0x100000000 if value & 0x80000000 else value + + +def _sext32(value: int) -> int: + value &= 0xFFFFFFFF + return (value ^ 0x80000000) - 0x80000000 + + +def _watch_match(watchers, ea: int, size: int, rw_bit: int): + end = ea + size + for wa, we, wk, mask in watchers: + if (mask & rw_bit) and ea < we and wa < end: + return wk + return None + + +class Machine: + """One deterministic RV64IMAC hart plus RAM and UART. + + ``device_map`` is a :class:`greeneye.device_map.DeviceMap` (or any object + with the same ``lookup``/``record`` surface) describing modelled MMIO. + ``unknown_mmio`` selects what happens to an access that is neither RAM, + UART nor in the map: ``"fault"`` (default, M1 parity) stops as before; + ``"lenient"`` returns the mapped behaviour or zero, records the access in + :attr:`mmio_log`, counts it in :attr:`mmio_unknown`, and, with + ``learn_mmio=True``, writes the observation back into the map. Nothing is + logged or counted when lenient mode is off, and debugger ``read_mem``/ + ``write_mem`` stay strict. + """ + + def __init__( + self, + memory_mb: int = 256, + ram_base: int = 0x80000000, + device_map=None, + unknown_mmio: str = "fault", + learn_mmio: bool = False, + bias: int = 0, + ): + if unknown_mmio not in UNKNOWN_MMIO_MODES: + raise ValueError(f"unknown_mmio must be one of {UNKNOWN_MMIO_MODES}, got {unknown_mmio!r}") + self.ram_base = ram_base + self.ram_size = memory_mb * 1024 * 1024 + self.ram = mmap.mmap(-1, self.ram_size) + self.uart = Uart16550() + self.device_map = device_map + self.unknown_mmio = unknown_mmio + self.learn_mmio = bool(learn_mmio) + self.bias = bias # PC relocation for learned-map firmware addresses + self.mmio_log: list[dict] = [] + self.mmio_unknown = 0 + self.x = [0] * 32 + self.pc = ram_base + self.insns = 0 + self.csrs: dict[int, int] = {0x301: MISA_RV64IMAC} + self._cache: dict[int, tuple] = {} + self._u16 = struct.Struct("<H") + self._u32 = struct.Struct("<I") + self._i8 = struct.Struct("<b") + self._i16 = struct.Struct("<h") + self._i32 = struct.Struct("<i") + self._u64 = struct.Struct("<Q") + self._u16p = self._u16.pack_into + self._u32p = self._u32.pack_into + self._u64p = self._u64.pack_into + + # -- image loading ----------------------------------------------------- + + def load_segments(self, plan, entry: int) -> None: + """Place ``(addr, bytes)`` PT_LOAD results and set the PC.""" + for addr, data in plan: + offset = addr - self.ram_base + if offset < 0 or offset + len(data) > self.ram_size: + raise Fault(1, addr) + self.ram[offset : offset + len(data)] = data + self.pc = entry & MASK64 + self.invalidate() + + def invalidate(self) -> None: + """Drop the decode cache after debugger-visible code changes.""" + self._cache.clear() + + # -- register access (debugger surface) -------------------------------- + + def reg(self, index: int) -> int: + if index == 0: + return 0 + return self.x[index] + + def set_reg(self, index: int, value: int) -> None: + if index != 0: + self.x[index] = value & MASK64 + + # -- raw memory access (debugger surface) ------------------------------ + + def read_mem(self, addr: int, size: int) -> bytes: + out = bytearray() + while size > 0: + if self.ram_base <= addr < self.ram_base + self.ram_size: + take = min(size, self.ram_base + self.ram_size - addr) + off = addr - self.ram_base + out += self.ram[off : off + take] + addr += take + size -= take + elif UART_BASE <= addr < UART_BASE + UART_SIZE: + out.append(self.uart.read(addr - UART_BASE)) + addr += 1 + size -= 1 + else: + raise Fault(5, addr) + return bytes(out) + + def write_mem(self, addr: int, data: bytes) -> None: + self.invalidate() + offset = 0 + while offset < len(data): + here = addr + offset + remaining = len(data) - offset + if self.ram_base <= here < self.ram_base + self.ram_size: + take = min(remaining, self.ram_base + self.ram_size - here) + ro = here - self.ram_base + self.ram[ro : ro + take] = data[offset : offset + take] + offset += take + elif UART_BASE <= here < UART_BASE + UART_SIZE: + self.uart.write(here - UART_BASE, data[offset]) + offset += 1 + else: + raise Fault(7, here) + + # -- MMIO (guest side) ------------------------------------------------- + + def _mapped_mmio(self, addr: int, size: int, write: bool) -> tuple[bool, int]: + """Consult the device map; ``(handled, value)`` for a modelled access.""" + dm = self.device_map + if dm is None: + return False, 0 + spec = dm.lookup(addr, size, write) + if spec == "unimplemented": + return False, 0 + if spec.startswith("value:"): + return True, int(spec[6:], 16) + return True, 0 # "zero": mapped but no read value observed + + def _log_unknown(self, pc: int, addr: int, size: int, rw: str, value: int = 0) -> None: + """Lenient fallback: log, count, and optionally learn one access.""" + self.mmio_unknown += 1 + self.mmio_log.append({"pc": pc, "addr": addr, "size": size, "rw": rw}) + if self.learn_mmio: + if self.device_map is None: + from greeneye.device_map import DeviceMap # lazy: sim stays usable alone + + self.device_map = DeviceMap(meta={"source": "learn"}) + self.device_map.record(addr, size, rw, value, pc=pc, bias=self.bias, source="trace") + + def _mmio_load(self, addr: int, size: int, pc: int = 0) -> int: + if UART_BASE <= addr < UART_BASE + UART_SIZE: + return self.uart.read(addr - UART_BASE) + handled, value = self._mapped_mmio(addr, size, False) + if handled: + return value + if self.unknown_mmio == "lenient": + self._log_unknown(pc, addr, size, "r") + return 0 + raise Fault(5, addr) + + def _mmio_store(self, addr: int, size: int, value: int, pc: int = 0) -> None: + if UART_BASE <= addr < UART_BASE + UART_SIZE: + self.uart.write(addr - UART_BASE, value) + return + handled, _ = self._mapped_mmio(addr, size, True) + if handled: + if self.learn_mmio and self.device_map is not None: + self.device_map.record(addr, size, "w", value, pc=pc, bias=self.bias, source="trace") + return + if self.unknown_mmio == "lenient": + self._log_unknown(pc, addr, size, "w", value) + return + raise Fault(7, addr) + + # -- execution --------------------------------------------------------- + + def step(self, watchers=(), trace=None): + return self.run(1, breakpoints=(), watchers=watchers, single_step=True, trace=trace) + + def run(self, budget: int, breakpoints=(), watchers=(), single_step: bool = False, + trace=None): + """Execute up to ``budget`` instructions. + + Returns ``(reason, pc, a, b)``; ``a``/``b`` carry the watched address + and kind (R_WATCH) or the trap cause and tval (R_FAULT). When + ``trace`` is a :class:`sim.trace.Trace`, every executed instruction + and every completed memory access in its address range is recorded. + """ + x = self.x + ram = self.ram + base = self.ram_base + limit = self.ram_size + cache = self._cache + i8 = self._i8.unpack_from + i16 = self._i16.unpack_from + i32 = self._i32.unpack_from + u16 = self._u16.unpack_from + u32 = self._u32.unpack_from + u64 = self._u64.unpack_from + u16p = self._u16p + u32p = self._u32p + u64p = self._u64p + has_bps = bool(breakpoints) + pc = self.pc + n = 0 + reason = R_BUDGET + a = b = 0 + traced = trace is not None + active = False + + while n < budget: + if has_bps and pc in breakpoints: + reason = R_BP + a = pc + break + + off = pc - base + if off < 0 or off + 4 > limit: + reason = R_FAULT + a = 1 + b = pc + break + hw = ram[off] | (ram[off + 1] << 8) + if hw & 3 == 3: + d = cache.get(pc) + if d is None: + d = decode32(pc, hw | (ram[off + 2] << 16) | (ram[off + 3] << 24)) + cache[pc] = d + else: + d = cache.get(pc) + if d is None: + d = decode16(pc, hw) + cache[pc] = d + kind, sub, rd, r1, r2, imm, nxt = d + n += 1 + if traced: + insn_pc = pc + active = trace.begin(insn_pc, self.insns + n) + + # -- loads / stores (most frequent in firmware hot loops) ------ + if kind == K_LOAD: + ea = (x[r1] + imm) & MASK64 + size = LOAD_SIZE[sub] + if watchers: + hit = _watch_match(watchers, ea, size, 2) + if hit is not None: + reason = R_WATCH + a = ea + b = hit + break + o = ea - base + if 0 <= o <= limit - size: + if active: + trace.mem(insn_pc, ea, size, False, self.insns + n) + if sub == 3: + if rd: + x[rd] = u64(ram, o)[0] + elif sub == 2: + if rd: + x[rd] = i32(ram, o)[0] + elif sub == 4: + if rd: + x[rd] = ram[o] + elif sub == 0: + if rd: + x[rd] = i8(ram, o)[0] + elif sub == 5: + if rd: + x[rd] = u16(ram, o)[0] + elif sub == 6: + if rd: + x[rd] = u32(ram, o)[0] + else: + if rd: + x[rd] = i16(ram, o)[0] + pc = nxt + else: + try: + v = self._mmio_load(ea, size, pc) + except Fault as f: + reason = R_FAULT + a = f.cause + b = f.tval + break + if rd: + x[rd] = v + if active: + trace.mem(insn_pc, ea, size, False, self.insns + n) + pc = nxt + continue + + if kind == K_STORE: + ea = (x[r1] + imm) & MASK64 + size = STORE_SIZE[sub] + if watchers: + hit = _watch_match(watchers, ea, size, 1) + if hit is not None: + reason = R_WATCH + a = ea + b = hit + break + o = ea - base + value = x[r2] + if 0 <= o <= limit - size: + if active: + trace.mem(insn_pc, ea, size, True, self.insns + n) + if sub == 3: + u64p(ram, o, value) + elif sub == 2: + u32p(ram, o, value & 0xFFFFFFFF) + elif sub == 1: + u16p(ram, o, value & 0xFFFF) + else: + ram[o] = value & 0xFF + pc = nxt + else: + try: + self._mmio_store(ea, size, value, pc) + except Fault as f: + reason = R_FAULT + a = f.cause + b = f.tval + break + if active: + trace.mem(insn_pc, ea, size, True, self.insns + n) + pc = nxt + continue + + # -- control flow ---------------------------------------------- + if kind == K_BRANCH: + va = x[r1] + vb = x[r2] + if sub == 0: + take = va == vb + elif sub == 1: + take = va != vb + elif sub == 4: + take = _signed64(va) < _signed64(vb) + elif sub == 5: + take = _signed64(va) >= _signed64(vb) + elif sub == 6: + take = va < vb + else: + take = va >= vb + pc = imm if take else nxt + elif kind == K_JAL: + if rd: + x[rd] = nxt + pc = imm + elif kind == K_JALR: + target = (x[r1] + imm) & MASK64 + target &= ~1 + if rd: + x[rd] = nxt + pc = target + elif kind == K_AUIPC: + if rd: + x[rd] = (pc + imm) & MASK64 + pc = nxt + elif kind == K_LUI: + if rd: + x[rd] = imm + pc = nxt + + # -- integer ALU ----------------------------------------------- + elif kind == K_OPI: + va = x[r1] if r1 else 0 + if sub == I_ADDI: + rv = (va + imm) & MASK64 + elif sub == I_SLLI: + rv = (va << (imm & 63)) & MASK64 + elif sub == I_SLTI: + rv = 1 if _signed64(va) < imm else 0 + elif sub == I_SLTIU: + rv = 1 if va < (imm & MASK64) else 0 + elif sub == I_XORI: + rv = va ^ (imm & MASK64) + elif sub == I_ORI: + rv = va | (imm & MASK64) + elif sub == I_ANDI: + rv = va & (imm & MASK64) + elif sub == I_SRLI: + rv = va >> (imm & 63) + else: # I_SRAI + rv = (_signed64(va) >> (imm & 63)) & MASK64 + if rd: + x[rd] = rv + pc = nxt + + elif kind == K_OP: + va = x[r1] + vb = x[r2] + if sub == A_ADD: + rv = (va + vb) & MASK64 + elif sub == A_SLL: + rv = (va << (vb & 63)) & MASK64 + elif sub == A_SLT: + rv = 1 if _signed64(va) < _signed64(vb) else 0 + elif sub == A_SLTU: + rv = 1 if va < vb else 0 + elif sub == A_XOR: + rv = va ^ vb + elif sub == A_SRL: + rv = va >> (vb & 63) + elif sub == A_OR: + rv = va | vb + elif sub == A_AND: + rv = va & vb + elif sub == A_SUB: + rv = (va - vb) & MASK64 + elif sub == A_SRA: + rv = (_signed64(va) >> (vb & 63)) & MASK64 + elif sub == A_MUL: + rv = (va * vb) & MASK64 + elif sub == A_MULH: + rv = ((_signed64(va) * _signed64(vb)) >> 64) & MASK64 + elif sub == A_MULHSU: + rv = ((_signed64(va) * vb) >> 64) & MASK64 + elif sub == A_MULHU: + rv = ((va * vb) >> 64) & MASK64 + elif sub == A_DIV: + sa, sb = _signed64(va), _signed64(vb) + if sb == 0: + rv = MASK64 + elif sa == -(1 << 63) and sb == -1: + rv = 1 << 63 + else: + rv = (abs(sa) // abs(sb)) & MASK64 + if (sa < 0) != (sb < 0): + rv = (-rv) & MASK64 + elif sub == A_DIVU: + rv = MASK64 if vb == 0 else va // vb + elif sub == A_REM: + sa, sb = _signed64(va), _signed64(vb) + if sb == 0 or (sa == -(1 << 63) and sb == -1): + rv = 0 + else: + rv = (abs(sa) % abs(sb)) & MASK64 + if sa < 0: + rv = (-rv) & MASK64 + else: # A_REMU + rv = va if vb == 0 else va % vb + if rd: + x[rd] = rv + pc = nxt + + elif kind == K_OPI32: + va = x[r1] if r1 else 0 + if sub == I_ADDIW: + rv = (va + imm) & 0xFFFFFFFF + elif sub == I_SLLIW: + rv = (va << (imm & 31)) & 0xFFFFFFFF + elif sub == I_SRLIW: + rv = (va & 0xFFFFFFFF) >> (imm & 31) + else: # I_SRAIW + rv = (_signed32(va) >> (imm & 31)) & 0xFFFFFFFF + if rd: + x[rd] = _sext32(rv) & MASK64 + pc = nxt + + elif kind == K_OP32: + va = x[r1] + vb = x[r2] + if sub == A_ADD: + rv = (va + vb) & 0xFFFFFFFF + elif sub == A_SUB: + rv = (va - vb) & 0xFFFFFFFF + elif sub == A_SLL: + rv = (va << (vb & 31)) & 0xFFFFFFFF + elif sub == A_SRL: + rv = (va & 0xFFFFFFFF) >> (vb & 31) + elif sub == A_SRA: + rv = (_signed32(va) >> (vb & 31)) & 0xFFFFFFFF + elif sub == A_MUL: + rv = (va * vb) & 0xFFFFFFFF + elif sub == A_DIV: + sa, sb = _signed32(va), _signed32(vb) + if sb == 0: + rv = 0xFFFFFFFF + elif sa == -(1 << 31) and sb == -1: + rv = 1 << 31 + else: + rv = (abs(sa) // abs(sb)) & 0xFFFFFFFF + if (sa < 0) != (sb < 0): + rv = (-rv) & 0xFFFFFFFF + elif sub == A_DIVU: + b32 = vb & 0xFFFFFFFF + rv = 0xFFFFFFFF if b32 == 0 else (va & 0xFFFFFFFF) // b32 + elif sub == A_REM: + sa, sb = _signed32(va), _signed32(vb) + if sb == 0 or (sa == -(1 << 31) and sb == -1): + rv = 0 + else: + rv = (abs(sa) % abs(sb)) & 0xFFFFFFFF + if sa < 0: + rv = (-rv) & 0xFFFFFFFF + else: # A_REMU + b32 = vb & 0xFFFFFFFF + rv = (va & 0xFFFFFFFF) if b32 == 0 else (va & 0xFFFFFFFF) % b32 + if rd: + x[rd] = _sext32(rv) & MASK64 + pc = nxt + + # -- system / atomics / traps ---------------------------------- + elif kind == K_FENCE: + pc = nxt + + elif kind == K_CSR: + csr = imm + old = self.csrs.get(csr, 0) + if sub in (5, 6, 7): + src = r1 # zimm + if sub == 5: + if rd: + x[rd] = old + self.csrs[csr] = src + else: + if rd: + x[rd] = old + if src: + self.csrs[csr] = (old | src) if sub == 6 else (old & ~src) + else: + src = x[r1] + if sub == 1: + if rd: + x[rd] = old + self.csrs[csr] = src & MASK64 + elif sub == 2: + if rd: + x[rd] = old + if r1: + self.csrs[csr] = (old | src) & MASK64 + elif sub == 3: + if rd: + x[rd] = old + if r1: + self.csrs[csr] = (old & ~src) & MASK64 + else: + reason = R_FAULT + a = 2 + b = csr + break + pc = nxt + + elif kind == K_AMO: + width = 8 if sub & 0x40 else 4 + op5 = sub & 0x3F + ea = x[r1] & MASK64 + if watchers: + hit = _watch_match(watchers, ea, width, 1) + if hit is not None: + reason = R_WATCH + a = ea + b = hit + break + try: + old = self._guest_load(ea, width, pc) + if active: + trace.mem(insn_pc, ea, width, False, self.insns + n) + if op5 == 0x02: # lr + if rd: + x[rd] = old + elif op5 == 0x03: # sc + self._guest_store(ea, width, x[r2], pc) + if active: + trace.mem(insn_pc, ea, width, True, self.insns + n) + if rd: + x[rd] = 0 + else: + rhs = x[r2] + mask = 0xFFFFFFFF if width == 4 else MASK64 + ov = old & mask + rv = rhs & mask + if op5 == 0x00: + new = (ov + rv) & mask + elif op5 == 0x01: + new = rv + elif op5 == 0x04: + new = ov ^ rv + elif op5 == 0x08: + new = ov | rv + elif op5 == 0x0C: + new = ov & rv + elif op5 == 0x10: + new = ov if _signed64(ov) < _signed64(rv) else rv + elif op5 == 0x14: + new = ov if _signed64(ov) > _signed64(rv) else rv + elif op5 == 0x18: + new = ov if ov < rv else rv + else: # 0x1C + new = ov if ov > rv else rv + self._guest_store(ea, width, new, pc) + if active: + trace.mem(insn_pc, ea, width, True, self.insns + n) + if rd: + x[rd] = (_sext32(old) & MASK64) if width == 4 else old + pc = nxt + except Fault as f: + reason = R_FAULT + a = f.cause + b = f.tval + break + + elif kind == K_EBREAK: + if single_step: + pc = nxt + else: + reason = R_EBREAK + a = pc + break + + elif kind == K_ECALL: + if single_step: + pc = nxt + else: + reason = R_ECALL + a = pc + break + + elif kind == K_MRET: + target = self.csrs.get(0x341, 0) & MASK64 + if target: + pc = target + else: + reason = R_FAULT + a = 2 + b = 0 + break + + else: # K_ILLEGAL + reason = R_FAULT + a = 2 + b = 0 + break + + self.pc = pc + self.insns += n + if single_step and reason == R_BUDGET: + reason = R_STEP + return (reason, pc, a, b) + + # -- guest memory helpers (AMO) ---------------------------------------- + + def _guest_load(self, addr: int, width: int, pc: int = 0) -> int: + off = addr - 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) + + def _guest_store(self, addr: int, width: int, value: int, pc: int = 0) -> None: + off = addr - self.ram_base + if 0 <= off <= self.ram_size - width: + if width == 8: + self._u64p(self.ram, off, value) + else: + self._u32p(self.ram, off, value & 0xFFFFFFFF) + return + self._mmio_store(addr, width, value, pc) + + +#: Task-facing alias: the native simulator machine is :class:`Machine`. +SimMachine = Machine diff --git a/debug/risc-v-gsp-rm-vm/sim/trace.py b/debug/risc-v-gsp-rm-vm/sim/trace.py new file mode 100644 index 0000000..04373b1 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/sim/trace.py @@ -0,0 +1,145 @@ +"""Native execution/access trace (roadmap R4): the in-process equivalent of +``qemu/greeneye_trace.c``. + +The QEMU TCG plugin records every translated instruction and every memory +access as JSONL. This module does the same from inside +:class:`sim.machine.Machine`, with the same knobs and line shape: + +* ``exec=on|off`` - emit one ``exec`` line per executed instruction; +* ``start=``/``end=`` - only instrument PCs in the inclusive range (matching + the plugin, which filters at translation time, so memory accesses are also + filtered by the *instruction* PC); +* a bounded ring budget - once full, further lines are counted as ``dropped`` + instead of growing without limit (deterministically). + +Lines are JSON objects in the TCG plugin's schema (``t``/``pc``/``addr``/ +``size``, hex strings), plus a ``vt`` virtual-time field (the machine's +instruction count). Nothing here reads the host clock, so the payload is +byte-identical for identical execution. +""" +from __future__ import annotations + +#: Default cap on buffered trace lines (~50 bytes each in memory). +DEFAULT_MAX_EVENTS = 1_000_000 + + +class Trace: + """Collects compact exec/access lines for one native session.""" + + def __init__(self, *, exec_enabled: bool = True, start: int | None = None, + end: int | None = None, max_events: int | None = None): + self.exec_enabled = bool(exec_enabled) + self.start = start + self.end = end + self.max_events = DEFAULT_MAX_EVENTS if max_events is None else max(0, int(max_events)) + self.execs = 0 + self.reads = 0 + self.writes = 0 + self.events = 0 + self.dropped = 0 + self._lines: list[str] = [] + self._flushed = False + + # -- configuration ----------------------------------------------------- + + def in_range(self, pc: int) -> bool: + if self.start is not None and pc < self.start: + return False + if self.end is not None and pc > self.end: + return False + return True + + def config(self) -> dict: + return { + "exec": self.exec_enabled, + "start": self.start, + "end": self.end, + "max_events": self.max_events, + } + + def counts(self) -> dict: + return { + "exec": self.execs, + "read": self.reads, + "write": self.writes, + "events": self.events, + "dropped": self.dropped, + } + + # -- collection (called from Machine.run's hot loop) -------------------- + + def begin(self, pc: int, vt: int) -> bool: + """Called once per executed instruction; returns access-capture.""" + if not self.in_range(pc): + return False + if self.exec_enabled: + self._append(f'{{"t":"exec","vcpu":0,"pc":"0x{pc:x}","vt":{vt}}}') + self.execs += 1 + return True + + def mem(self, pc: int, addr: int, size: int, is_write: bool, vt: int) -> None: + kind = "write" if is_write else "read" + if is_write: + self.writes += 1 + else: + self.reads += 1 + self._append( + f'{{"t":"{kind}","vcpu":0,"addr":"0x{addr:x}","size":{size},' + f'"pc":"0x{pc:x}","vt":{vt}}}' + ) + + def _append(self, line: str) -> None: + self.events += 1 + if self.max_events and len(self._lines) >= self.max_events: + self.dropped += 1 + return + self._lines.append(line) + + # -- output ------------------------------------------------------------ + + def payload(self) -> bytes: + """The complete JSONL document, including the plugin's summary line.""" + body = "\n".join(self._lines) + if body: + body += "\n" + summary = ( + f'{{"t":"summary","insns":{self.execs},"mem":{self.reads + self.writes},' + f'"events":{self.events},"dropped":{self.dropped},' + f'"exec":{str(self.exec_enabled).lower()},' + f'"start":{_json_addr(self.start)},"end":{_json_addr(self.end)}}}' + ) + return (body + summary + "\n").encode("utf-8") + + def flush(self, recorder) -> str | None: + """Write the trace to ``recorder`` as ``blobs/trace.jsonl`` (once).""" + if self._flushed: + return None + self._flushed = True + if recorder is None or getattr(recorder, "dir", None) is None: + return None + rel = recorder.blob("trace.jsonl", self.payload()) + recorder.meta["trace_blob"] = rel + recorder.meta["trace"] = {**self.config(), **self.counts()} + return rel + + +def make_trace(spec): + """Normalise a trace spec (None/False, True, dict or Trace) to a Trace.""" + if spec is None or spec is False: + return None + if isinstance(spec, Trace): + return spec + if spec is True: + return Trace() + if isinstance(spec, dict): + return Trace( + exec_enabled=spec.get("exec", spec.get("exec_enabled", True)), + start=spec.get("start"), + end=spec.get("end"), + max_events=spec.get("max_events", spec.get("max")), + ) + raise ValueError(f"trace spec must be None, bool, dict or Trace, not {type(spec).__name__}") + + +def _json_addr(value: int | None) -> str: + return "null" if value is None else f"\"0x{value:x}\"" |
