From e20563e2efc6121d7a294c5b0f58afd0e1976814 Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Wed, 15 Apr 2026 16:05:45 +0200 Subject: [PATCH] blob_emu: sysreg skip + UART capture -> real DDR banner emulated Two extensions that finally get the emu producing useful output: 1. Catch UC_ERR_EXCEPTION on MSR/MRS access, decode the instruction, stub the destination register to 0 (for MRS) or silently accept (for MSR), advance PC, resume. Opaque sysregs the blob touches (CNTFRQ_EL0 etc.) no longer halt the emu. 2. Map UART2 (0xFEB50000), hook writes to THR (offset 0), collect printable bytes. Stub LSR (+0x14 = 0x60 THRE|TEMT) and USR (+0x7C = 0x02 TFE) in ABS_STUB so the blob|s putc polling loops resolve. Result: stock AND patched v3fb blob each emit the full 52-byte cold-boot banner under stub=0x00 -- DDR ff1a08bde6 typ 25/04/21-14:31.26,fwver: v1.19 -- byte-identical to what comes out of the GenBook|s real UART. Under stub=0xFF both progress further, also identically: DDR ff1a08bde6 typ 25/04/21-14:31.26,fwver: v1.19 pd/pu vd_ddr Patched matches stock in both stub regimes. That|s the regression gate we wanted: a patcher change that breaks the DDR blob|s visible behavior now shows up as banner-divergence before any hardware flash. --- blob_emu.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/blob_emu.py b/blob_emu.py index 6cd9988..e13456a 100755 --- a/blob_emu.py +++ b/blob_emu.py @@ -31,6 +31,7 @@ MMIO = [ (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. ] @@ -44,6 +45,9 @@ ABS_STUB = { 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 = [ @@ -120,6 +124,21 @@ def run(blob_path, stub_byte, max_insn, trace, entry=0): 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): @@ -139,12 +158,58 @@ def run(blob_path, stub_byte, max_insn, trace, entry=0): uc.reg_write(UC_ARM64_REG_SP, STACK_BASE + STACK_SIZE - 16) uc.reg_write(UC_ARM64_REG_X30, RET_STUB) - try: - uc.emu_start(BLOB_BASE + entry, RET_STUB, count=max_insn) - except UcError as e: - pc = uc.reg_read(UC_ARM64_REG_PC) - print(f"EXC at PC=0x{pc:x}: {e} (max_pc=0x{state['max_pc']:x}, {state['count']} insns)") - return 1 + # 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)}) ---") @@ -154,6 +219,17 @@ def run(blob_path, stub_byte, max_insn, trace, entry=0): 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__":