#!/usr/bin/env python3 """DDR blob emulator — runs a raw (plaintext) RK3588 DDR init blob in Unicorn, stubs MMIO reads to a constant byte. Intent: smoke-test a patched blob to catch crashes/hangs before committing to a real flash. Usage: blob_emu.py [--stub 0x00|0xFF] [--max N] [--trace] """ import argparse, sys from unicorn import * from unicorn.arm64_const import * # The blob is position-dependent: it expects to be loaded at 0xFF001000 # (the RK3588 bootrom's TPL SRAM slot). An integrity check at 0x14-0x1c # ANDs the BL return address with 0xFFFFFF00 and compares to 0xFF001000. SRAM_BASE = 0xFF000000 SRAM_SIZE = 0x00100000 # 1 MB SRAM window (covers 0xFF000000..0xFF100000) BLOB_BASE = 0xFF001000 # where the blob actually lives STACK_BASE = 0x00400000 STACK_SIZE = 0x00100000 RET_STUB = 0x00800000 RET_SIZE = 0x00001000 MMIO = [ (0xFD580000, 0x00020000, "GRF"), (0xFD5F0000, 0x00010000, "BUS_GRF"), (0xFD7C0000, 0x00040000, "CRU"), (0xFD8C0000, 0x00010000, "SCRU"), (0xFE010000, 0x00020000, "DDRC"), (0xFE030000, 0x00010000, "FW_DDR"), (0xFE050000, 0x00010000, "SGRF"), (0xFE0C0000, 0x00040000, "DDRPHY"), (0xFE400000, 0x00010000, "PMU"), (0xFECC0000, 0x00010000, "SCRAMBLE"), (0xFEB50000, 0x00010000, "UART2"), # debug UART — capture TX writes # SRAM_BOOT (0xFF000000..0xFF100000) is regular RWX memory where # the blob lives, NOT MMIO — not listed here. ] # Per-address stub values — ported from ddr_emu2.c. # Each entry: address → value to return on read. # Use base + offset; the MMIO read hook checks exact match first, # then falls back to region-specific patterns, then to the constant stub. ABS_STUB = { # SGRF (0xFE050000) 0xFE0500E0: 0x00000000, # status = ready 0xFE050054: 0x00000001, # CON21 = done 0xFE0500E4: 0x00000000, # enable # UART2 (0xFEB50000) — DesignWare DW_apb_uart + 8250-compatible 0xFEB50014: 0x00000060, # LSR: THRE + TEMT (8250 legacy TX-ready) 0xFEB5007C: 0x00000002, # USR: TFE (Transmit FIFO Empty, DW-specific) } # Region-offset patterns: (region_base, region_end, offset_mask, offset_value, return_value) REGION_OFF = [ # DDRPHY (0xFE0C0000..0xFE100000), offset masked to lower 12 bits (0xFE0C0000, 0xFE100000, 0xFFF, 0xA24, 0x00000002), # DfiStatus ready (0xFE0C0000, 0xFE100000, 0xFFF, 0x684, 0x00000000), # CalBusy idle (0xFE0C0000, 0xFE100000, 0xFFF, 0x090, 0x00000000), # MicroContMux (0xFE0C0000, 0xFE100000, 0xFFF, 0x080, 0x00000000), # MicroReset done (0xFE0C0000, 0xFE100000, 0xFFF, 0x514, 0x00000000), # training done ] # Region constants: entire region returns this by default REGION_CONST = [ (0xFD8C0000, 0xFD8D0000, 0x00000001), # SCRU — PLL locked everywhere ] def run(blob_path, stub_byte, max_insn, trace, entry=0): blob = open(blob_path, "rb").read() print(f"blob: {blob_path} size=0x{len(blob):x} stub=0x{stub_byte:02x}") uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM) uc.mem_map(SRAM_BASE, SRAM_SIZE, UC_PROT_ALL) # SRAM window uc.mem_write(BLOB_BASE, blob) # blob at 0xFF001000 uc.mem_map(STACK_BASE, STACK_SIZE, UC_PROT_ALL) uc.mem_map(RET_STUB, RET_SIZE, UC_PROT_ALL) uc.mem_write(RET_STUB, b"\x00\x00\x20\xd4") # brk #0 for base, sz, _ in MMIO: uc.mem_map(base, sz, UC_PROT_ALL) state = {"count": 0, "last_pc": 0, "same_pc": 0, "max_pc": 0} stub_word = int.from_bytes(bytes([stub_byte]) * 8, "little") def hook_code(uc, addr, size, ud): state["count"] += 1 if addr == state["last_pc"]: state["same_pc"] += 1 if state["same_pc"] > 5000: print(f"HANG at PC=0x{addr:x} after {state['count']} insns") uc.emu_stop() else: state["same_pc"] = 0 state["last_pc"] = addr if addr > state["max_pc"]: state["max_pc"] = addr if state["count"] >= max_insn: print(f"LIMIT at PC=0x{addr:x} (max_pc=0x{state['max_pc']:x}, {state['count']} insns)") uc.emu_stop() if trace and state["count"] % 10000 == 0: print(f"[{state['count']}] PC=0x{addr:x}") def stub_value(addr): # Exact-address overrides first if addr in ABS_STUB: return ABS_STUB[addr] # Region + offset patterns for rbase, rend, mask, off_val, ret_val in REGION_OFF: if rbase <= addr < rend and (addr & mask) == off_val: return ret_val # Whole-region constants for rbase, rend, ret_val in REGION_CONST: if rbase <= addr < rend: return ret_val # Fall through to constant-byte stub return stub_word mmio_reads = [] def hook_mmio_read(uc, typ, addr, size, val, ud): v = stub_value(addr) & ((1 << (size * 8)) - 1) uc.mem_write(addr, v.to_bytes(size, "little")) if len(mmio_reads) < 50: mmio_reads.append((uc.reg_read(UC_ARM64_REG_PC), addr, v)) uc.hook_add(UC_HOOK_CODE, hook_code, begin=SRAM_BASE, end=SRAM_BASE + SRAM_SIZE) for base, sz, _ in MMIO: uc.hook_add(UC_HOOK_MEM_READ, hook_mmio_read, begin=base, end=base + sz) # UART2 TX capture: writes to 0xFEB50000 (THR/DR register) are characters uart_buf = bytearray() def hook_uart_write(uc, typ, addr, size, val, ud): if addr == 0xFEB50000: c = val & 0xFF uart_buf.append(c) # Echo printable characters immediately so we see live trace if 0x20 <= c < 0x7F or c in (0x09, 0x0A, 0x0D): sys.stdout.write(chr(c)) sys.stdout.flush() uc.hook_add(UC_HOOK_MEM_WRITE, hook_uart_write, begin=0xFEB50000, end=0xFEB50000 + 0x10000) # (UART UART2 status registers are in ABS_STUB so they get served # by the single hook_mmio_read path — avoids hook-ordering races.) # Catch any unmapped read/write and log what the blob wanted unmapped = [] def hook_unmapped(uc, typ, addr, size, val, ud): pc = uc.reg_read(UC_ARM64_REG_PC) unmapped.append((pc, typ, addr, size, val)) # Lazily map a 64KB page containing the address and stub it page = addr & ~0xFFFF try: uc.mem_map(page, 0x10000, UC_PROT_ALL) except UcError: pass v = stub_value(addr) & ((1 << (size * 8)) - 1) uc.mem_write(addr, v.to_bytes(size, "little")) return True # tell Unicorn to continue uc.hook_add(UC_HOOK_MEM_UNMAPPED, hook_unmapped) uc.reg_write(UC_ARM64_REG_SP, STACK_BASE + STACK_SIZE - 16) uc.reg_write(UC_ARM64_REG_X30, RET_STUB) # Register-index → Unicorn constant table (X0..X30) XREG = [getattr(__import__("unicorn.arm64_const", fromlist=["X"]), f"UC_ARM64_REG_X{i}") for i in range(31)] sysreg_log = [] def is_mrs(insn): # 1101_0101_0011 … MRS Xt, sysreg return (insn >> 20) == 0xD53 def is_msr_reg(insn): # 1101_0101_0001 … MSR sysreg, Xt return (insn >> 20) == 0xD51 def is_msr_imm(insn): # 1101_0101_0000 … MSR pstate / HINT-like return (insn >> 20) == 0xD50 def decode_sysreg(insn): # bits 20..5 are op0:op1:CRn:CRm:op2 op0 = ((insn >> 19) & 0x3) | 2 # MRS/MSR implies op0 in {2,3} op1 = (insn >> 16) & 0x7 crn = (insn >> 12) & 0xF crm = (insn >> 8) & 0xF op2 = (insn >> 5) & 0x7 return (op0, op1, crn, crm, op2) pc = BLOB_BASE + entry remaining = max_insn while remaining > 0: try: uc.emu_start(pc, RET_STUB, count=remaining) break # normal return except UcError as e: pc = uc.reg_read(UC_ARM64_REG_PC) try: insn = int.from_bytes(uc.mem_read(pc, 4), "little") except UcError: print(f"EXC at PC=0x{pc:x}: {e} (couldn't fetch insn) (max_pc=0x{state['max_pc']:x}, {state['count']} insns)") return 1 if is_mrs(insn): rt = insn & 0x1F sr = decode_sysreg(insn) sysreg_log.append(("MRS", pc, sr, rt)) if rt < 31: uc.reg_write(XREG[rt], 0) # stub = 0 pc += 4 uc.reg_write(UC_ARM64_REG_PC, pc) remaining -= 1 continue if is_msr_reg(insn) or is_msr_imm(insn): sr = decode_sysreg(insn) if is_msr_reg(insn) else None sysreg_log.append(("MSR", pc, sr, insn & 0x1F)) pc += 4 uc.reg_write(UC_ARM64_REG_PC, pc) remaining -= 1 continue print(f"EXC at PC=0x{pc:x}: {e} (insn=0x{insn:08x}) (max_pc=0x{state['max_pc']:x}, {state['count']} insns)") return 1 print(f"HALT insns={state['count']} max_pc=0x{state['max_pc']:x}") print(f"--- first MMIO reads ({len(mmio_reads)}) ---") for pc, addr, v in mmio_reads[:20]: print(f" PC=0x{pc:x} RD 0x{addr:x} -> 0x{v:x}") if unmapped: print(f"--- unmapped accesses ({len(unmapped)}) ---") for pc, typ, addr, size, val in unmapped[:20]: print(f" PC=0x{pc:x} type={typ} 0x{addr:x} size={size}") if sysreg_log: print(f"--- sysreg accesses stubbed ({len(sysreg_log)}) ---") for kind, pc, sr, rt in sysreg_log[:20]: srstr = f"S{sr[0]}_{sr[1]}_C{sr[2]}_C{sr[3]}_{sr[4]}" if sr else "?" print(f" PC=0x{pc:x} {kind} {srstr} Rt=x{rt}") if uart_buf: print(f"\n--- UART capture ({len(uart_buf)} bytes) ---") try: print(uart_buf.decode("utf-8", errors="replace")) except Exception: print(uart_buf.hex()) return 0 if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("blob") ap.add_argument("--stub", default="0x00") ap.add_argument("--max", type=int, default=200_000) ap.add_argument("--trace", action="store_true") ap.add_argument("--entry", default="0x0", help="start PC (0x40 skips version check)") a = ap.parse_args() sys.exit(run(a.blob, int(a.stub, 0), a.max, a.trace, int(a.entry, 0)))