b18: S7 exit router + dstub trap + docs

This commit is contained in:
SashegDev
2026-09-12 10:30:18 +00:00
parent 67539f7351
commit 049f98122b
6 changed files with 1097 additions and 50 deletions
+23
View File
@@ -41,3 +41,26 @@
1. Добить баннер (см. свежий in_asm-хвост). 1. Добить баннер (см. свежий in_asm-хвост).
2. Заменить стаблеты настоящим PBL (нужны дампы с живого тела!). 2. Заменить стаблеты настоящим PBL (нужны дампы с живого тела!).
3. UEFI (img3, EDK2) — отдельный фронт после SBL1. 3. UEFI (img3, EDK2) — отдельный фронт после SBL1.
## b18: S7 и цикл delay-helper (2026-09-12)
Механизм зацикливания (b17, доказан Unicorn-harness `tools/sbl_uni.py`):
- sbl_main → delay-функция `0x8013B78` (256-итераций table-walk, `bl 0x8017AF4`
каждую итерацию) → b14 (`0x8017AFC: b.w 0x8005B9E`) → helper `0x8005BB0`
→ выход `ldr pc,[sp],#0x14` (0x8005BCA) → S3-слот (0x8013BA5, рестарт).
- Эпилог delay (`0x8013BAC: pop {r3-r7,pc}`) снимает 0x18 без push; каждый
S3-рестарт съедает 0x18 живого стека. Маршрут: слоты → fill (STUBV →
pop pc = maze) → pristine IMEM (нули → null-jump → hook_null → maze →
stale lr) → переполнение IMEM (0x8610008).
- Heap SBL (stm в 0x8006684) затирает maze/S6/banner (0, 0x7a, STUBV).
QEMU: dstub-overlay `[0x08006B80,0x08006C50)` (read = снапшот ELF+стабы,
write = игнор). Uni: deferred-restore guard (MEM_WRITE-хук срабатывает
ДО записи, restore надо делать в hook_code!).
- Maze-blob: `movs r0,#0; lsr ip,lr,#24; cmp #8; bne hang; bx lr`
(возврат 0 только для lr из DDRLOW 0x08..., иначе hang).
Фикс b18: патч BAC (`f8bd0a4a``f3f730b8`, `b.w S7`) + S7-стаблет
(52B @ 0x08006C10): `cmp r4,#0xff` → iterate (`0x8013BA5`) или done
(restore r4/r5/sl/fp, возврат в sbl_main `0x802f681`). BAE-путь
(`[r7]==2`) принесён в жертву (r7=0, недостижим).
Фронтир b18: sbl_main возвращается, новый код `0x801Bxxx`
(`strb [r7,#2]`-паттерн, b17-NOP на 0x801B71E), падеж pop'ом пустого
стека у верха IMEM — следующий фронт.
+8 -14
View File
@@ -1,22 +1,16 @@
.syntax unified .syntax unified
.cpu cortex-a7 .cpu cortex-a7
.thumb .thumb
@ maze entry logger: ring buffer of 8 (lr,sp) at MAILBOX, return 0. @ maze entry: return 0. Guard: bad lr (high byte != 0x08) spins
@ Preserves all regs except r0 (return value) and r12 (IP, clobber-ok). @ with IRQs off instead of jumping into the void.
.text .text
.global maze_stub .global maze_stub
maze_stub: maze_stub:
push {r0-r3}
movw r12, #:lower16:0x08006890
movt r12, #:upper16:0x08006890
ldr r0, [r12, #32]
add r0, #1
and r0, #7
str r0, [r12, #32]
lsl r0, #3
add r0, r12
str lr, [r0]
str sp, [r0, #4]
pop {r0-r3}
movs r0, #0 movs r0, #0
lsr r12, lr, #24
cmp r12, #8
bne maze_hang
bx lr bx lr
maze_hang:
cpsid i
b maze_hang
+22
View File
@@ -0,0 +1,22 @@
.syntax unified
.cpu cortex-a7
.thumb
@ Minimal PBL stub (position-independent): BLX to SBL1 entry (seg1),
@ then canonical regs + jump to sbl_main. Mirrors PBL call-then-jump.
.text
.global pbl_stub
pbl_stub:
push {r4-r7, lr}
movw r12, #0x6b81
movt r12, #0x800
blx r12 @ SBL1 entry (odd: stay Thumb)
movw r0, #0x0000
movt r0, #0x0022 @ r0 = pbl_shared 0x00220000
mov r5, r0
movw sl, #0x0000
movt sl, #0x0806 @ sl = table base 0x08060000
movw fp, #0x1000
movt fp, #0x0806 @ fp = 0x08061000
movw lr, #0xf63f
movt lr, #0x0802 @ lr = sbl_main odd (noreturn marker)
bx lr
+135 -31
View File
@@ -68,10 +68,13 @@ struct SaimaaTmrState {
static uint64_t saimaa_tmr_read(void *opaque, hwaddr off, unsigned size) static uint64_t saimaa_tmr_read(void *opaque, hwaddr off, unsigned size)
{ {
/* Deterministic tick (+1/read): advances delay loops but keeps runs
* reproducible (qemu_clock would diverge run to run). */
static uint64_t tick;
(void)opaque; (void)opaque;
(void)off; (void)off;
(void)size; (void)size;
return (uint64_t)(qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) >> 12); return ++tick;
} }
static void saimaa_tmr_write(void *opaque, hwaddr off, uint64_t val, static void saimaa_tmr_write(void *opaque, hwaddr off, uint64_t val,
@@ -101,6 +104,54 @@ static void saimaa_tmr_init(Object *obj)
/* Stubs region handle for set_readonly after scaffold implant. */ /* Stubs region handle for set_readonly after scaffold implant. */
static MemoryRegion *saimaa_stubs_mr; static MemoryRegion *saimaa_stubs_mr;
/* DDRLOW stub trap [0x08006B80,0x08006C50): serves the scaffolded bytes
* (S/maze/S6/banner/S7 stubs) on read/fetch, ignores SBL heap-clobber
* writes. Uni parity: deferred-restore guard in sbl_uni.py (wider:
* [0x080068C0,0x08006C50), also covers PBL stub).
* Snapshot is taken AFTER ELF load + all stub writes (readback).
* PBL stub [0x080068C0,0x080068F4) and the ELF-text gap below stay plain
* RAM: PBL runs once (if at all), gap holds real hot SBL code. */
#define SAIMAA_DSTUB_BASE 0x08006B80
#define SAIMAA_DSTUB_SIZE 0x000000D0
static uint8_t saimaa_dstub_mem[SAIMAA_DSTUB_SIZE];
static uint64_t saimaa_dstub_read(void *opaque, hwaddr off, unsigned size)
{
uint64_t v = 0;
unsigned i;
(void)opaque;
if (off >= SAIMAA_DSTUB_SIZE) {
return 0;
}
if (off + size > SAIMAA_DSTUB_SIZE) {
size = SAIMAA_DSTUB_SIZE - off;
}
for (i = 0; i < size; i++) {
v |= (uint64_t)saimaa_dstub_mem[off + i] << (8 * i);
}
return v;
}
static void saimaa_dstub_write(void *opaque, hwaddr off, uint64_t val,
unsigned size)
{
static int n = 0;
(void)opaque;
(void)val;
(void)size;
if (n < 8) {
fprintf(stderr, "saimaa-dstub: ignored heap write @0x%x\n",
SAIMAA_DSTUB_BASE + (unsigned)off);
n++;
}
}
static const MemoryRegionOps saimaa_dstub_ops = {
.read = saimaa_dstub_read,
.write = saimaa_dstub_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};
/* ---- minimal MSM UARTDM stub: TX prints, status always ready ---- */ /* ---- minimal MSM UARTDM stub: TX prints, status always ready ---- */
#define TYPE_SAIMAA_VEC "saimaa-vec" #define TYPE_SAIMAA_VEC "saimaa-vec"
#define TYPE_SAIMAA_UART "saimaa-uart" #define TYPE_SAIMAA_UART "saimaa-uart"
@@ -120,17 +171,9 @@ static uint64_t saimaa_vec_read(void *opaque, hwaddr off, unsigned size)
ARMCPU *cpu = ARM_CPU(current_cpu); ARMCPU *cpu = ARM_CPU(current_cpu);
uint64_t pc = cpu ? cpu->env.regs[15] : 0; uint64_t pc = cpu ? cpu->env.regs[15] : 0;
int th = cpu ? cpu->env.thumb : 0; int th = cpu ? cpu->env.thumb : 0;
uint32_t mb0 = 0, mb1 = 0;
if (saimaa_vec_n < 3) {
cpu_physical_memory_read(0x08006890, &mb0, 4);
cpu_physical_memory_read(0x08006894, &mb1, 4);
}
fprintf(stderr, "saimaa-vec: %s off=0x%x pc=0x%lx", fprintf(stderr, "saimaa-vec: %s off=0x%x pc=0x%lx",
size == 2 ? "fetch?" : "read", size == 2 ? "fetch?" : "read",
(unsigned)off, (unsigned long)pc - (th ? 4 : 8)); (unsigned)off, (unsigned long)pc - (th ? 4 : 8));
if (saimaa_vec_n < 3) {
fprintf(stderr, " maze-mailbox: lr=0x%x sp=0x%x", mb0, mb1);
}
if (cpu) { if (cpu) {
int b, r; int b, r;
fprintf(stderr, " regs="); fprintf(stderr, " regs=");
@@ -402,6 +445,12 @@ static void saimaa_machine_init(MachineState *machine)
ARM_CPU(first_cpu)->env.banked_r13[b] = 0x085FFF00; ARM_CPU(first_cpu)->env.banked_r13[b] = 0x085FFF00;
} }
ARM_CPU(first_cpu)->env.regs[13] = 0x085FFF00; ARM_CPU(first_cpu)->env.regs[13] = 0x085FFF00;
/* Stale lr (never bl'd on synthetic paths) returns into maze
* (returns 0 = SBL "empty" convention) instead of jumping to 0. */
ARM_CPU(first_cpu)->env.regs[14] = 0x08006B8D;
for (int b = 0; b < 8; b++) {
ARM_CPU(first_cpu)->env.banked_r14[b] = 0x08006B8D;
}
ARM_CPU(first_cpu)->env.regs[0] = 0x00220000; /* fake pbl_shared */ ARM_CPU(first_cpu)->env.regs[0] = 0x00220000; /* fake pbl_shared */
/* sbl_main expects r5 = mem-table pointer (PBL trampoline does /* sbl_main expects r5 = mem-table pointer (PBL trampoline does
* mov r5, r0 before jumping here); point it at pbl_shared too. */ * mov r5, r0 before jumping here); point it at pbl_shared too. */
@@ -447,7 +496,10 @@ static void saimaa_machine_init(MachineState *machine)
tgt & ~1u, (tgt & 1) ? " thumb" : " arm", tgt & ~1u, (tgt & 1) ? " thumb" : " arm",
(unsigned long)entry); (unsigned long)entry);
} else { } else {
cpu_set_pc(first_cpu, entry); /* Default: start at PBL stub (faithful call-then-jump flow) */
cpu_set_pc(first_cpu, 0x080068C1);
printf("saimaa: PBL-stub start -> entry 0x%lx then sbl_main\n",
(unsigned long)entry);
} }
/* Boot scaffolding for SBL1 BringUp (WIP stub-driven PBL): /* Boot scaffolding for SBL1 BringUp (WIP stub-driven PBL):
* static continuation slots for shared-epilogue pops. * static continuation slots for shared-epilogue pops.
@@ -455,14 +507,14 @@ static void saimaa_machine_init(MachineState *machine)
* (sp values from cpu traces, b10 image) */ * (sp values from cpu traces, b10 image) */
{ {
uint32_t magic = 0x00000000; /* IMEM poll spins WHILE equal */ uint32_t magic = 0x00000000; /* IMEM poll spins WHILE equal */
uint32_t S = 0x08006820, STUB = 0x08006830; /* in-code zeros, safe from heap */ uint32_t S = 0x08006B80, STUB = 0x08006B8C; /* dead PBL-entry code */
uint32_t STUBV = 0x08006831; /* odd: blx stays Thumb */ uint32_t STUBV = 0x08006B8D; /* odd: blx stays Thumb */
uint32_t w; uint32_t w;
uint16_t h0 = 0x2000, h1 = 0x4770; /* movs r0,#0; bx lr */ uint16_t h0 = 0x2000, h1 = 0x4770; /* movs r0,#0; bx lr */
uint32_t v; uint32_t v;
/* pointer maze in owned RAM for SBL1 struct chains. /* pointer maze in owned RAM for SBL1 struct chains.
* STUB = mailbox logger + return-0: records lr/sp at entry * STUB = minimal logger + return-0 (movs r0,#0; bx lr);
* to 0x08006964 for post-mortem, then movs r0,#0; bx lr. */ * mailbox at 0x08006BD0 records last (lr,sp). */
w = S + 8; w = S + 8;
cpu_physical_memory_write(S, &w, 4); cpu_physical_memory_write(S, &w, 4);
/* [struct+4] is another fn ptr (e.g. 0x801b72e ldr r3,[r0,#4] /* [struct+4] is another fn ptr (e.g. 0x801b72e ldr r3,[r0,#4]
@@ -471,16 +523,13 @@ static void saimaa_machine_init(MachineState *machine)
cpu_physical_memory_write(S + 4, &STUBV, 4); cpu_physical_memory_write(S + 4, &STUBV, 4);
cpu_physical_memory_write(S + 8, &STUBV, 4); cpu_physical_memory_write(S + 8, &STUBV, 4);
{ {
/* from qemu/maze.S: mailbox logger + lr-range guard. /* Minimal maze: movs r0,#0; bx lr (4B). No mailbox, no
* Bad lr (not 0x08xxxxxx) spins with IRQs off instead of * guard — vec trap already reports fault state. Less
* jumping into the void. 36 bytes. */ * surface for SBL heap clobbers. */
static const uint8_t mb[] = { static const uint8_t mb[] = {
0x0f, 0xb4, 0x46, 0xf6, 0x90, 0x0c, 0xc0, 0xf6, 0x00, 0x20, 0x4f, 0xea, 0x1e, 0x6c, 0xbc, 0xf1,
0x00, 0x0c, 0xdc, 0xf8, 0x20, 0x00, 0x00, 0xf1, 0x08, 0x0f, 0x00, 0xd1, 0x70, 0x47, 0x72, 0xb6,
0x01, 0x00, 0x00, 0xf0, 0x07, 0x00, 0xcc, 0xf8, 0xfd, 0xe7,
0x20, 0x00, 0x4f, 0xea, 0xc0, 0x00, 0x60, 0x44,
0xc0, 0xf8, 0x00, 0xe0, 0xc0, 0xf8, 0x04, 0xd0,
0x0f, 0xbc, 0x00, 0x20, 0x70, 0x47,
}; };
size_t k; size_t k;
for (k = 0; k < sizeof(mb); k += 4) { for (k = 0; k < sizeof(mb); k += 4) {
@@ -493,7 +542,7 @@ static void saimaa_machine_init(MachineState *machine)
cpu_physical_memory_write(0x087c29d18, &S, 4); cpu_physical_memory_write(0x087c29d18, &S, 4);
cpu_physical_memory_write(0x08050b28, &S, 4); cpu_physical_memory_write(0x08050b28, &S, 4);
cpu_physical_memory_write(0x08070000, &S, 4); cpu_physical_memory_write(0x08070000, &S, 4);
/* S6 stublet at 0x0800685e: rebuild popped frame regs, /* S6 stublet at 0x08006B9E: rebuild popped frame regs,
* return to 0x8013ba5 (b14 redirects 0x8017afc pop here) */ * return to 0x8013ba5 (b14 redirects 0x8017afc pop here) */
{ {
static const uint8_t s6[] = { static const uint8_t s6[] = {
@@ -507,10 +556,10 @@ static void saimaa_machine_init(MachineState *machine)
uint32_t word = 0; uint32_t word = 0;
size_t n = sizeof(s6) - k < 4 ? sizeof(s6) - k : 4; size_t n = sizeof(s6) - k < 4 ? sizeof(s6) - k : 4;
__builtin_memcpy(&word, &s6[k], n); __builtin_memcpy(&word, &s6[k], n);
cpu_physical_memory_write(0x0800685e + k, &word, 4); cpu_physical_memory_write(0x08006B9E + k, &word, 4);
} }
} }
/* Banner stub at 0x0800686c: print r0 string via UARTDM /* Banner stub at 0x08006BAC: print r0 string via UARTDM
* (b16 redirects 0x802f690 blx here). Built from qemu/banner.S: * (b16 redirects 0x802f690 blx here). Built from qemu/banner.S:
* push frame, print loop, ldr pc,=0x802f694 (callee noreturn). * push frame, print loop, ldr pc,=0x802f694 (callee noreturn).
* 28 bytes total. */ * 28 bytes total. */
@@ -527,13 +576,54 @@ static void saimaa_machine_init(MachineState *machine)
uint32_t word = 0; uint32_t word = 0;
size_t n = sizeof(bn) - k < 4 ? sizeof(bn) - k : 4; size_t n = sizeof(bn) - k < 4 ? sizeof(bn) - k : 4;
__builtin_memcpy(&word, &bn[k], n); __builtin_memcpy(&word, &bn[k], n);
cpu_physical_memory_write(0x0800686c + k, &word, 4); cpu_physical_memory_write(0x08006BAC + k, &word, 4);
} }
} }
/* stack zone defaults to plain-stub addr: every register /* PBL stub at 0x080068C0 (52B, from qemu/pbl.S): BLX to SBL1
* popped from untouched stack becomes callable (returns 0). */ * entry, then canonical regs + jump to sbl_main. Used when
* sblmain prop is unset (default): faithful PBL call flow. */
{ {
uint32_t fill = 0x08006831; static const uint8_t pbl[] = {
0xf0, 0xb5, 0x46, 0xf6, 0x81, 0x3c, 0xc0, 0xf6,
0x00, 0x0c, 0xe0, 0x47, 0x40, 0xf2, 0x00, 0x00,
0xc0, 0xf2, 0x22, 0x00, 0x05, 0x46, 0x40, 0xf2,
0x00, 0x0a, 0xc0, 0xf6, 0x06, 0x0a, 0x41, 0xf2,
0x00, 0x0b, 0xc0, 0xf6, 0x06, 0x0b, 0x4f, 0xf2,
0x3f, 0x6e, 0xc0, 0xf6, 0x02, 0x0e, 0x70, 0x47,
0x00, 0x00, 0x00, 0x00,
};
size_t k;
for (k = 0; k < sizeof(pbl); k += 4) {
uint32_t word = 0;
size_t n = sizeof(pbl) - k < 4 ? sizeof(pbl) - k : 4;
__builtin_memcpy(&word, &pbl[k], n);
cpu_physical_memory_write(0x080068C0 + k, &word, 4);
}
}
/* S7 stublet at 0x08006C10 (52B, b18): delay-epilogue exit
* router. cmp r4,#0xff: iterate (r4<=0xff -> 0x8013ba5) or
* done (restore r4/r5/sl/fp, return to sbl_main 0x802f681).
* Assembled with keystone (see docs/sbl-bringup.md). */
{
static const uint8_t s7[] = {
0xff, 0x2c, 0x13, 0xd9, 0x40, 0xf2, 0x00, 0x04,
0xc0, 0xf2, 0x22, 0x04, 0x40, 0xf2, 0x00, 0x05,
0xc0, 0xf2, 0x22, 0x05, 0x40, 0xf2, 0x00, 0x0a,
0xc0, 0xf6, 0x06, 0x0a, 0x41, 0xf2, 0x00, 0x0b,
0xc0, 0xf6, 0x06, 0x0b, 0xdf, 0xf8, 0x00, 0xf0,
0x81, 0xf6, 0x02, 0x08, 0xdf, 0xf8, 0x00, 0xf0,
0xa5, 0x3b, 0x01, 0x08,
};
size_t k;
for (k = 0; k < sizeof(s7); k += 4) {
uint32_t word = 0;
size_t n = sizeof(s7) - k < 4 ? sizeof(s7) - k : 4;
__builtin_memcpy(&word, &s7[k], n);
cpu_physical_memory_write(0x08006C10 + k, &word, 4);
}
}
{
uint32_t fill = 0x08006B8D;
uint32_t a; uint32_t a;
for (a = 0x085F0000; a < 0x08600000; a += 4) { for (a = 0x085F0000; a < 0x08600000; a += 4) {
cpu_physical_memory_write(a, &fill, 4); cpu_physical_memory_write(a, &fill, 4);
@@ -612,6 +702,20 @@ static void saimaa_machine_init(MachineState *machine)
printf("saimaa: uefi tables live\n"); printf("saimaa: uefi tables live\n");
} }
printf("saimaa: slots live\n"); printf("saimaa: slots live\n");
/* DDRLOW stub trap overlay: snapshot ELF+stubs, serve on
* read/fetch, ignore heap writes (uni parity). Must come
* AFTER all stub writes above. Priority 2 > RAMs' 1. */
cpu_physical_memory_read(SAIMAA_DSTUB_BASE, saimaa_dstub_mem,
SAIMAA_DSTUB_SIZE);
{
MemoryRegion *dstub = g_new(MemoryRegion, 1);
memory_region_init_io(dstub, NULL, &saimaa_dstub_ops,
NULL, "saimaa-dstub",
SAIMAA_DSTUB_SIZE);
memory_region_add_subregion_overlap(sysmem,
SAIMAA_DSTUB_BASE,
dstub, 2);
}
/* Freeze stubs: any SBL write here now faults loudly /* Freeze stubs: any SBL write here now faults loudly
* (data abort with DFAR) instead of silent corruption. */ * (data abort with DFAR) instead of silent corruption. */
memory_region_set_readonly(saimaa_stubs_mr, true); memory_region_set_readonly(saimaa_stubs_mr, true);
@@ -624,7 +728,7 @@ static void saimaa_machine_init(MachineState *machine)
static void saimaa_post_reset(void *opaque) static void saimaa_post_reset(void *opaque)
{ {
uint32_t S = 0x08006820; uint32_t S = 0x08006B80;
(void)opaque; (void)opaque;
/* Re-apply pokes that live inside ROM-covered ranges (rom_reset /* Re-apply pokes that live inside ROM-covered ranges (rom_reset
* restores file bytes over them). */ * restores file bytes over them). */
+403 -5
View File
@@ -1,263 +1,661 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""sbl_patch.py — reproduce all WIP file patches for SBL1 bring-up. """sbl_patch.py — reproduce all WIP file patches for SBL1 bring-up.
Usage: ./tools/sbl_patch.py fw/sbl/img0.elf out-dir/ Usage: ./tools/sbl_patch.py fw/sbl/img0.elf out-dir/
Produces img0_bl.elf, img0_b2..b8.elf (cumulative variants, see STEPS). Produces img0_bl.elf, img0_b2..b8.elf (cumulative variants, see STEPS).
Each step is documented with WHY (all WIP, to be replaced by real PBL). Each step is documented with WHY (all WIP, to be replaced by real PBL).
""" """
import os import os
import sys import sys
SEG_OFF, SEG_VA = 0x3000, 0x8005800 SEG_OFF, SEG_VA = 0x3000, 0x8005800
BLX2BL = { # blx-imm -> bl (stay Thumb): set bit12 of 2nd halfword BLX2BL = { # blx-imm -> bl (stay Thumb): set bit12 of 2nd halfword
0x802F658: ('d8f7c2ed', 'd8f7c2fd'), 0x802F658: ('d8f7c2ed', 'd8f7c2fd'),
0x802F660: ('d8f702ec', 'd8f702fc'), 0x802F660: ('d8f702ec', 'd8f702fc'),
} }
NOPS = { # call -> NOP NOP (skip unmodellable init/assert; r0/r5 ignored) NOPS = { # call -> NOP NOP (skip unmodellable init/assert; r0/r5 ignored)
0x802F664: 'e4f77efc', # bl walker (result ignored by caller) 0x802F664: 'e4f77efc', # bl walker (result ignored by caller)
0x802F672: 'e6f7bbfa', # bl 0x8015bec (assert-hang) 0x802F672: 'e6f7bbfa', # bl 0x8015bec (assert-hang)
0x802F678: None, # bl 0x8018a78, see REDIRECT (installer runs!) 0x802F678: None, # bl 0x8018a78, see REDIRECT (installer runs!)
0x801B71E: '9847fee7', # blx r3 + b.n self (terminal assert, 4B) 0x801B71E: '9847fee7', # blx r3 + b.n self (terminal assert, 4B)
0x8016184: 'fdf790fc', # bl 0x8013aa8 (subtree nopped, see b8) 0x8016184: 'fdf790fc', # bl 0x8013aa8 (subtree nopped, see b8)
} }
REDIRECT = { REDIRECT = {
# bl target off-by-N fixes (verified by disasm + QEMU traces): # bl target off-by-N fixes (verified by disasm + QEMU traces):
0x8013AA8: ('07f026be', '07f027be'), # jump-table case0: +2 0x8013AA8: ('07f026be', '07f027be'), # jump-table case0: +2
0x802F678: ('e9f7fef9', 'e9f700fa'), # bl 0x8018a78 -> 0x8018a7c (+4) 0x802F678: ('e9f7fef9', 'e9f700fa'), # bl 0x8018a78 -> 0x8018a7c (+4)
} }
POOLS = { POOLS = {
# literal pool words redirected to owned scratch (orig struct garbage): # literal pool words redirected to owned scratch (orig struct garbage):
0x801B76C: ('280b0508', '00000708'), # -> 0x08070000 0x801B76C: ('280b0508', '00000708'), # -> 0x08070000
} }
def off(va): def off(va):
return SEG_OFF + (va - SEG_VA) return SEG_OFF + (va - SEG_VA)
def hx(d, va, n=4): def hx(d, va, n=4):
return d[off(va):off(va) + n].hex() return d[off(va):off(va) + n].hex()
def set4(d, va, hexbytes): def set4(d, va, hexbytes):
d[off(va):off(va) + 4] = bytes.fromhex(hexbytes) d[off(va):off(va) + 4] = bytes.fromhex(hexbytes)
def main(): def main():
src, outdir = sys.argv[1], sys.argv[2] src, outdir = sys.argv[1], sys.argv[2]
os.makedirs(outdir, exist_ok=True) os.makedirs(outdir, exist_ok=True)
base = bytearray(open(src, 'rb').read()) base = bytearray(open(src, 'rb').read())
def save(d, name): def save(d, name):
p = os.path.join(outdir, name) p = os.path.join(outdir, name)
open(p, 'wb').write(bytes(d)) open(p, 'wb').write(bytes(d))
print('wrote', p, len(d)) print('wrote', p, len(d))
d = bytearray(base) d = bytearray(base)
for va, (a, b) in BLX2BL.items(): for va, (a, b) in BLX2BL.items():
assert hx(d, va) == a, (hex(va), hx(d, va)) assert hx(d, va) == a, (hex(va), hx(d, va))
set4(d, va, b) set4(d, va, b)
save(d, 'img0_bl.elf') save(d, 'img0_bl.elf')
# b2 = bl + walker NOP # b2 = bl + walker NOP
o = off(0x802F664) o = off(0x802F664)
assert d[o:o + 4].hex() == 'e4f77efc' assert d[o:o + 4].hex() == 'e4f77efc'
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b2.elf') save(d, 'img0_b2.elf')
# b3 = b2 + 0x8015bec NOP # b3 = b2 + 0x8015bec NOP
o = off(0x802F672) o = off(0x802F672)
assert d[o:o + 4].hex() == 'e6f7bbfa', hx(d, 0x802F672) assert d[o:o + 4].hex() == 'e6f7bbfa', hx(d, 0x802F672)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b3.elf') save(d, 'img0_b3.elf')
# b4 = b3 + 0x8018a78 NOP # b4 = b3 + 0x8018a78 NOP
o = off(0x802F678) o = off(0x802F678)
assert d[o:o + 4].hex() == 'e9f7fef9', hx(d, 0x802F678) assert d[o:o + 4].hex() == 'e9f7fef9', hx(d, 0x802F678)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b4.elf') save(d, 'img0_b4.elf')
# b5 = b4 + jump-table +2 # b5 = b4 + jump-table +2
o = off(0x8013AA8) o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be' assert d[o:o + 4].hex() == '07f026be'
d[o:o + 4] = bytes.fromhex('07f027be') d[o:o + 4] = bytes.fromhex('07f027be')
save(d, 'img0_b5.elf') save(d, 'img0_b5.elf')
# b6 = b5 + terminal assert NOP (4B) # b6 = b5 + terminal assert NOP (4B)
o = off(0x801B71E) o = off(0x801B71E)
assert d[o:o + 4].hex() == '9847fee7' assert d[o:o + 4].hex() == '9847fee7'
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b6.elf') save(d, 'img0_b6.elf')
# b7 = b6 + redirect bl to installer entry (undoes b4 NOP) # b7 = b6 + redirect bl to installer entry (undoes b4 NOP)
o = off(0x802F678) o = off(0x802F678)
assert d[o:o + 4].hex() == 'c046c046' assert d[o:o + 4].hex() == 'c046c046'
d[o:o + 4] = bytes.fromhex('e9f700fa') d[o:o + 4] = bytes.fromhex('e9f700fa')
save(d, 'img0_b7.elf') save(d, 'img0_b7.elf')
# b8 = b7 + subtree NOP + pool redirect # b8 = b7 + subtree NOP + pool redirect
o = off(0x8016184) o = off(0x8016184)
assert d[o:o + 4].hex() == 'fdf790fc', hx(d, 0x8016184) assert d[o:o + 4].hex() == 'fdf790fc', hx(d, 0x8016184)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
o = off(0x801B76C) o = off(0x801B76C)
assert d[o:o + 4].hex() == '280b0508' assert d[o:o + 4].hex() == '280b0508'
d[o:o + 4] = bytes.fromhex('00000708') d[o:o + 4] = bytes.fromhex('00000708')
save(d, 'img0_b8.elf') save(d, 'img0_b8.elf')
# b9 = b8 + hole-call NOP (EDL-dead padding slide at 0x80068FC) # b9 = b8 + hole-call NOP (EDL-dead padding slide at 0x80068FC)
o = off(0x8008396) o = off(0x8008396)
assert d[o:o + 4].hex() == 'fef780f9', hx(d, 0x8008396) assert d[o:o + 4].hex() == 'fef780f9', hx(d, 0x8008396)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b9.elf') save(d, 'img0_b9.elf')
# b10 = b9 with installer-bl reverted to NOP (S2 lands mid-NOPs safely, # b10 = b9 with installer-bl reverted to NOP (S2 lands mid-NOPs safely,
# falls into the 0x8013b7c call; installer skipped, structs via maze) # falls into the 0x8013b7c call; installer skipped, structs via maze)
o = off(0x802F678) o = off(0x802F678)
assert d[o:o + 4].hex() == 'e9f700fa', hx(d, 0x802F678) assert d[o:o + 4].hex() == 'e9f700fa', hx(d, 0x802F678)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b10.elf') save(d, 'img0_b10.elf')
# b11 = b10 with installer-bl restored (redirect, installer runs again) # b11 = b10 with installer-bl restored (redirect, installer runs again)
o = off(0x802F678) o = off(0x802F678)
assert d[o:o + 4].hex() == 'c046c046' assert d[o:o + 4].hex() == 'c046c046'
d[o:o + 4] = bytes.fromhex('e9f700fa') d[o:o + 4] = bytes.fromhex('e9f700fa')
save(d, 'img0_b11.elf') save(d, 'img0_b11.elf')
# b12: b10-variant with jump-table entry0 NOP (fall into entry1/case-1); # b12: b10-variant with jump-table entry0 NOP (fall into entry1/case-1);
# rebuilt from b10 base (installer stays skipped) # rebuilt from b10 base (installer stays skipped)
d = bytearray(base) d = bytearray(base)
for va, (a, b) in BLX2BL.items(): for va, (a, b) in BLX2BL.items():
set4(d, va, b) set4(d, va, b)
for va, exp, new in [ for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'), (0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'), (0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'), (0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'), (0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'), (0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'), (0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'), (0x801B76C, '280b0508', '00000708'),
]: ]:
assert hx(d, va) == exp, (hex(va), hx(d, va)) assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new) set4(d, va, new)
o = off(0x8013AA8) o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8) assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b12.elf') save(d, 'img0_b12.elf')
# b15 = b14 + force table-loop count (0x8013ba6 ldrh r0,[r7,#4] -> # b15 = b14 + force table-loop count (0x8013ba6 ldrh r0,[r7,#4] ->
# movs r0,#0xff): exit pop reads clobbered slot (->0); looping builds # movs r0,#0xff): exit pop reads clobbered slot (->0); looping builds
# tables via helpers instead. 2-byte patch. # tables via helpers instead. 2-byte patch.
d = bytearray(base) d = bytearray(base)
for va, (a, b) in BLX2BL.items(): for va, (a, b) in BLX2BL.items():
set4(d, va, b) set4(d, va, b)
for va, exp, new in [ for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'), (0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'), (0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'), (0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'), (0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'), (0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'), (0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'), (0x801B76C, '280b0508', '00000708'),
(0x8006684, '01f807bd', '46c046c0'), (0x8006684, '01f807bd', '46c046c0'),
(0x8017AFC, 'bde8fc87', 'eef7afbe'),
(0x8017AFC, 'bde8fc87', 'eef74fb8'),
]: ]:
assert hx(d, va) == exp, (hex(va), hx(d, va)) assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new) set4(d, va, new)
o = off(0x8013AA8) o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8) assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
o = off(0x8013BA6) o = off(0x8013BA6)
assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2)) assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2))
d[o:o + 2] = bytes.fromhex('ff20') d[o:o + 2] = bytes.fromhex('ff20')
save(d, 'img0_b15.elf') save(d, 'img0_b15.elf')
# b16 = b15 + banner call redirect (0x802f690 blx r3 -> bl banner stub # b16 = b15 + banner call redirect (0x802f690 blx r3 -> bl banner stub
# at 0x08062150; table[0] is garbage 0x690ce0ee). Stub prints r0 string # at 0x08062150; table[0] is garbage 0x690ce0ee). Stub prints r0 string
# via UARTDM, returns 0 -> falls into cbz path (banner done). # via UARTDM, returns 0 -> falls into cbz path (banner done).
d = bytearray(base) d = bytearray(base)
for va, (a, b) in BLX2BL.items(): for va, (a, b) in BLX2BL.items():
set4(d, va, b) set4(d, va, b)
for va, exp, new in [ for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'), (0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'), (0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'), (0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'), (0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'), (0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'), (0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'), (0x801B76C, '280b0508', '00000708'),
(0x8006684, '01f807bd', '46c046c0'), (0x8006684, '01f807bd', '46c046c0'),
(0x8017AFC, 'bde8fc87', 'eef7afbe'),
(0x802F690, '9847fee7', 'd7f7ecb8'), (0x8017AFC, 'bde8fc87', 'eef74fb8'),
(0x802F690, '9847fee7', 'd7f7ba8c'),
]: ]:
assert hx(d, va) == exp, (hex(va), hx(d, va)) assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new) set4(d, va, new)
o = off(0x8013AA8) o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8) assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
o = off(0x8013BA6) o = off(0x8013BA6)
assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2)) assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2))
d[o:o + 2] = bytes.fromhex('ff20') d[o:o + 2] = bytes.fromhex('ff20')
save(d, 'img0_b16.elf') save(d, 'img0_b16.elf')
# b14 = b13 + redirect S6 pop (0x8017afc ldmia.w sp!,{r2-r9,sl,pc}) # b14 = b13 + redirect S6 pop (0x8017afc ldmia.w sp!,{r2-r9,sl,pc})
# to S6 stublet at 0x08062140 (pc slot clobbered to 0 at runtime) # to S6 stublet at 0x08062140 (pc slot clobbered to 0 at runtime)
d = bytearray(base) d = bytearray(base)
for va, (a, b) in BLX2BL.items(): for va, (a, b) in BLX2BL.items():
set4(d, va, b) set4(d, va, b)
for va, exp, new in [ for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'), (0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'), (0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'), (0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'), (0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'), (0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'), (0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'), (0x801B76C, '280b0508', '00000708'),
(0x8006684, '01f807bd', '46c046c0'), (0x8006684, '01f807bd', '46c046c0'),
(0x8017AFC, 'bde8fc87', 'eef7afbe'),
(0x8017AFC, 'bde8fc87', 'eef74fb8'),
]: ]:
assert hx(d, va) == exp, (hex(va), hx(d, va)) assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new) set4(d, va, new)
o = off(0x8013AA8) o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8) assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b14.elf') save(d, 'img0_b14.elf')
# b13 = b12 + NOP strb.w fp,[r1,#-7]! at 0x8006684 (helper stores # b13 = b12 + NOP strb.w fp,[r1,#-7]! at 0x8006684 (helper stores
# fp into struct r1, but r1 arrives NULL from 0x8007e82 chain; # fp into struct r1, but r1 arrives NULL from 0x8007e82 chain;
# store to [0-7]=0xfffffff9 faults; struct unrecoverable here) # store to [0-7]=0xfffffff9 faults; struct unrecoverable here)
d = bytearray(base) d = bytearray(base)
for va, (a, b) in BLX2BL.items(): for va, (a, b) in BLX2BL.items():
set4(d, va, b) set4(d, va, b)
for va, exp, new in [ for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'), (0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'), (0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'), (0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'), (0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'), (0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'), (0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'), (0x801B76C, '280b0508', '00000708'),
(0x8006684, '01f807bd', '46c046c0'), (0x8006684, '01f807bd', '46c046c0'),
]: ]:
assert hx(d, va) == exp, (hex(va), hx(d, va)) assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new) set4(d, va, new)
o = off(0x8013AA8) o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8) assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
save(d, 'img0_b13.elf') save(d, 'img0_b13.elf')
# b17 = b16 with banner call NOPed (blx r3 + b.n self -> NOP NOP): # b17 = b16 with banner call NOPed (blx r3 + b.n self -> NOP NOP):
# logger is non-essential; banner stub tail gets heap-clobbered and # logger is non-essential; banner stub tail gets heap-clobbered and
# the 4B bl return lands mid-insn. Fall through to 0x802f694. # the 4B bl return lands mid-insn. Fall through to 0x802f694.
d = bytearray(base) d = bytearray(base)
for va, (a, b) in BLX2BL.items(): for va, (a, b) in BLX2BL.items():
set4(d, va, b) set4(d, va, b)
for va, exp, new in [ for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'), (0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'), (0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'), (0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'), (0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'), (0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'), (0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'), (0x801B76C, '280b0508', '00000708'),
(0x8006684, '01f807bd', '46c046c0'), (0x8006684, '01f807bd', '46c046c0'),
(0x8017AFC, 'bde8fc87', 'eef7afbe'),
(0x8017AFC, 'bde8fc87', 'eef74fb8'),
(0x802F690, '9847fee7', 'c046c046'), (0x802F690, '9847fee7', 'c046c046'),
]: ]:
assert hx(d, va) == exp, (hex(va), hx(d, va)) assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new) set4(d, va, new)
o = off(0x8013AA8) o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8) assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046') d[o:o + 4] = bytes.fromhex('c046c046')
o = off(0x8013BA6) o = off(0x8013BA6)
assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2)) assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2))
d[o:o + 2] = bytes.fromhex('ff20') d[o:o + 2] = bytes.fromhex('ff20')
save(d, 'img0_b17.elf') save(d, 'img0_b17.elf')
# b18 = b17 + S7 exit router (delay-epilogue leak fix):
# 0x8013BAC pop{r3-r7,pc} pops 0x18 with no matching push; S3-slot
# restarts (0x8013BA5) multiply it into a stack-eating cycle
# (slots->fill->zeros->null-loop->IMEM overflow). Route the epilogue
# to S7 (0x08006C10, scaffold stublet): r4<=0xff -> restart loop,
# else restore r4/r5/sl/fp and return to sbl_main (0x802f681).
# NOTE: overwrites BAE-path head (ldr r2,[pc,#0x28], only used when
# [r7]==2 — never in our flow, r7=0).
d = bytearray(base)
for va, (a, b) in BLX2BL.items():
set4(d, va, b)
for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'),
(0x8006684, '01f807bd', '46c046c0'),
(0x8017AFC, 'bde8fc87', 'eef74fb8'),
(0x802F690, '9847fee7', 'c046c046'),
(0x8013BAC, 'f8bd0a4a', 'f3f730b8'),
]:
assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new)
o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046')
o = off(0x8013BA6)
assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2))
d[o:o + 2] = bytes.fromhex('ff20')
save(d, 'img0_b18.elf')
# b19 = b17 + 0x80065d6 ldrb r0,[r1] -> movs r0,#2 (NULL table entry:
# type 2 takes exit-ish path instead of faulting on bx lr=0)
d = bytearray(base)
for va, (a, b) in BLX2BL.items():
set4(d, va, b)
for va, exp, new in [
(0x802F664, 'e4f77efc', 'c046c046'),
(0x802F672, 'e6f7bbfa', 'c046c046'),
(0x802F678, 'e9f7fef9', 'c046c046'),
(0x801B71E, '9847fee7', 'c046c046'),
(0x8008396, 'fef780f9', 'c046c046'),
(0x8016184, 'fdf790fc', 'c046c046'),
(0x801B76C, '280b0508', '00000708'),
(0x8006684, '01f807bd', '46c046c0'),
(0x8017AFC, 'bde8fc87', 'eef74fb8'),
(0x802F690, '9847fee7', 'c046c046'),
]:
assert hx(d, va) == exp, (hex(va), hx(d, va))
set4(d, va, new)
o = off(0x8013AA8)
assert d[o:o + 4].hex() == '07f026be', hx(d, 0x8013AA8)
d[o:o + 4] = bytes.fromhex('c046c046')
o = off(0x8013BA6)
assert d[o:o + 2].hex() == 'b888', (hex(0x8013BA6), hx(d, 0x8013BA6, 2))
d[o:o + 2] = bytes.fromhex('ff20')
o = off(0x80065D6)
assert d[o:o + 2].hex() == '0878', (hex(0x80065D6), d[o:o+2].hex())
d[o:o + 2] = bytes.fromhex('0220')
save(d, 'img0_b19.elf')
# b20 = b19 with 0x80065d6 movs#2 -> movs#1 (helper returns 1:
# caller cmp/beq falls through to cmp #0x25 path instead of skip)
o = off(0x80065D6)
assert d[o:o + 2].hex() == '0220'
d[o:o + 2] = bytes.fromhex('0120')
save(d, 'img0_b20.elf')
if __name__ == '__main__': if __name__ == '__main__':
main() main()
+506
View File
@@ -0,0 +1,506 @@
#!/usr/bin/env python3
"""sbl_uni.py — быстрый harness SBL1 на Unicorn (без пересборки QEMU).
Карта RAM как в saimaa; ELF-сегменты; UARTDM/TF в stdout; MMIO->0.
Использование:
./tools/sbl_uni.py fw/img/patch/img0_b17.elf --thumb 0x802f63f [--regs r0=0x220000,...]
[--fill 0x085F0000:0x10000:0x08006831] [--poke 0xADDR=0xVAL] [--limit 1000000]
Печатает: выполнено инстр., последний PC, fault-адрес, UART.
"""
import struct
import sys
from unicorn import (Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_HOOK_MEM_UNMAPPED,
UC_HOOK_MEM_WRITE, UC_HOOK_CODE)
from unicorn.arm_const import (UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2,
UC_ARM_REG_R3, UC_ARM_REG_R4, UC_ARM_REG_R5,
UC_ARM_REG_R6, UC_ARM_REG_R7, UC_ARM_REG_SP,
UC_ARM_REG_LR, UC_ARM_REG_PC,
UC_ARM_REG_CPSR)
_REGS = (UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3,
UC_ARM_REG_R4, UC_ARM_REG_R5, UC_ARM_REG_R6, UC_ARM_REG_R7)
RAMS = [
(0x00000000, 0x00001000), # zero page (QEMU-compat: [0] writes succeed)
(0x00200000, 0x00100000), # OCIMEM
(0x00400000, 0x07400000), # RPM range (heap-ish)
(0x08000000, 0x00600000), # DDRLOW
(0x08600000, 0x00010000), # IMEM
(0x80000000, 0x07C00000), # DDRHIGH_A
(0x87C00000, 0x00800000), # SMEM(+SBL BSS)
(0x078AF000, 0x00001000), # UARTDM page
(0x13000000, 0x00002000), # UEFI tables (already above, keep)
]
UART_TF = 0x078AF070
UART_SR = 0x078AF008
def load_elf(mu, path):
d = open(path, "rb").read()
assert d[:4] == b"\x7fELF"
entry = struct.unpack("<I", d[24:28])[0]
phoff = struct.unpack("<I", d[28:32])[0]
phnum = struct.unpack("<H", d[44:46])[0]
for k in range(phnum):
p = d[phoff + k * 32:phoff + (k + 1) * 32]
t, off, v, pa, fsz, msz, fl, al = struct.unpack("<IIIIIIII", p)
if t != 1:
continue
if fsz and v != 0x00220000: # QEMU parity: skip OCIMEM seg file-bytes
mu.mem_write(v, d[off:off + fsz])
for z in range(v + fsz, v + msz, 4):
mu.mem_write(z, b"\x00\x00\x00\x00")
return entry
def main():
path = sys.argv[1]
thumb = int(sys.argv[sys.argv.index("--thumb") + 1], 0)
mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
for base, size in RAMS:
mu.mem_map(base, size)
uart_out = []
unmapped = {"n": 0, "first": []}
def hook_unmapped(mu, access, addr, size, value, data):
unmapped["n"] += 1
if len(unmapped["first"]) < 5:
unmapped["first"].append((hex(addr), size, access))
return False
mu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_unmapped)
# SR reads return TX-ready
mu.mem_write(UART_SR, struct.pack("<I", 0xA0))
mu.mem_write(0x13000000, struct.pack("<II", 0x49535953, 0x54535953))
def hook_uw(mu, access, addr, size, value, data):
if addr == UART_TF:
uart_out.append(chr(value & 0xFF))
mu.hook_add(UC_HOOK_MEM_WRITE, hook_uw, begin=UART_TF, end=UART_TF)
entry = load_elf(mu, path)
for r in range(13):
mu.reg_write(UC_ARM_REG_R0 + r, 0)
if "--regs" in sys.argv:
for kv in sys.argv[sys.argv.index("--regs") + 1].split(","):
r, v = kv.split("=")
mu.reg_write(UC_ARM_REG_R0 + int(r[1:]), int(v, 0))
mu.reg_write(UC_ARM_REG_SP, 0x085FFF00)
from unicorn.arm_const import UC_ARM_REG_LR as _LR
mu.reg_write(_LR, 0x08006B8D) # QEMU parity: stale lr -> maze
mu.reg_write(UC_ARM_REG_CPSR, 0x13)
if "--fill" in sys.argv:
for spec in sys.argv[sys.argv.index("--fill") + 1].split(","):
a, sz, v = spec.split(":")
a, sz, v = int(a, 0), int(sz, 0), int(v, 0)
mu.mem_write(a, struct.pack("<I", v) * (sz // 4))
if "--poke" in sys.argv:
for spec in sys.argv[sys.argv.index("--poke") + 1].split(","):
a, v = spec.split("=")
mu.mem_write(int(a, 0), struct.pack("<I", int(v, 0)))
count = [0]
last = [0]
from collections import deque
hist = deque(maxlen=20000)
calls = [] if "--calltrace" in sys.argv else None
rets = [] if "--retrace" in sys.argv else None
qlog = [] if "--querylog" in sys.argv else None
_spmax = [0x085FFF00]
_splog = [] if "--spytrack" in sys.argv else None
elog = [] if "--entrylog" in sys.argv else None
_prev = [0]
_eranges = []
_inrng = []
if "--entrylog" in sys.argv:
for _spec in sys.argv[sys.argv.index("--entrylog") + 1].split(","):
_lo, _sz = _spec.split(":")
_lo, _sz = int(_lo, 0), int(_sz, 0)
_eranges.append((_lo, _lo + _sz))
_inrng.append(False)
from unicorn.arm_const import UC_ARM_REG_CPSR as _CPSR
def hook_code(mu, addr, size, data):
count[0] += 1
last[0] = addr
if _splog is not None:
try:
sp = mu.reg_read(UC_ARM_REG_SP)
except Exception:
sp = 0
if sp > _spmax[0] and len(_splog) < 40:
_spmax[0] = sp
_splog.append((count[0], addr, sp))
if _dirty[0]:
# deferred restore: MEM_WRITE hooks fire BEFORE the write lands,
# so in-hook restore gets overwritten by the instruction itself
_inww[0] = True
try:
for a, b in _dirty[1]:
mu.mem_write(a, stub_snap[a - STUB_LO:b - STUB_LO])
finally:
_inww[0] = False
_dirty[0] = False
del _dirty[1][:]
try:
thumb = (mu.reg_read(_CPSR) >> 5) & 1
except Exception:
thumb = 1
hist.append((addr, thumb))
if qlog is not None and addr in (0x8017EA4, 0x8017C60):
try:
vals = [mu.reg_read(_REGS[i]) for i in range(8)]
except Exception:
vals = []
qlog.append((addr, vals,
mu.reg_read(UC_ARM_REG_SP),
mu.reg_read(UC_ARM_REG_LR)))
if elog is not None:
for _i, (_elo, _ehi) in enumerate(_eranges):
inside = _elo <= addr < _ehi
if inside and not _inrng[_i]:
if len(elog) < 20000:
elog.append((count[0], _prev[0], addr))
_inrng[_i] = inside
_prev[0] = addr
if calls is not None and size in (2, 4):
try:
if size == 2:
h = struct.unpack("<H", mu.mem_read(addr & ~1, 2))[0]
is_blx = (h & 0xFF80) == 0x4780
rm = (h >> 3) & 7 if is_blx else -1
# bx Rm (jump, no link): 010001110 Rm:000
is_bx = (h & 0xFF80) == 0x4700
if is_bx:
bxrm = (h >> 3) & 15
try:
if bxrm < 8:
bxt = mu.reg_read(_REGS[bxrm])
elif bxrm == 14:
bxt = mu.reg_read(_LR)
else:
bxt = -1
except Exception:
bxt = -1
if len(calls) < 8000:
calls.append((addr, 100 + bxrm, bxt))
if rets is not None and 0x8017E00 <= addr < 0x8018000:
# pop {..,pc} (0xBD00+) or bx lr (0x4770)
if h == 0x4770 or (h & 0xFF00) == 0xBD00:
try:
rv = mu.reg_read(_REGS[0])
except Exception:
rv = -1
if len(rets) < 200:
rets.append((addr, rv))
else:
w = struct.unpack("<I", mu.mem_read(addr & ~3, 4))[0]
# ARM bx/blx Rm: cond(4) 0001001011111111111100x1 Rm(4)
is_blx = (w & 0x0FFFFFF0) in (0x012FFF10, 0x012FFF30)
rm = w & 15 if is_blx else -1
tgt = -1
if not is_blx and calls is not None:
# Thumb32 BL: h0=11110S+imm10, h1=1111x+J+imm11
h0 = w & 0xFFFF
h1 = (w >> 16) & 0xFFFF
if (h0 & 0xF800) == 0xF000 and (h1 & 0xE000) == 0xE000:
S = (h0 >> 10) & 1
J1 = (h1 >> 13) & 1
J2 = (h1 >> 11) & 1
I1 = 1 - (J1 ^ S)
I2 = 1 - (J2 ^ S)
off = (I1 << 23) | (I2 << 22) | ((h0 & 0x3FF) << 12) | ((h1 & 0x7FF) << 1)
if S:
off -= 0x1000000
if len(calls) < 8000:
calls.append((addr, 200, (addr + 4 + off) & 0xFFFFFFFF))
if rets is not None and 0x8017E00 <= addr < 0x8018000:
# pop.w {...,pc}: 0xE8BDxxxx with bit15 set
if (w & 0xFFFF8000) == 0xE8BD8000:
try:
rv = mu.reg_read(_REGS[0])
except Exception:
rv = -1
if len(rets) < 200:
rets.append((addr, rv))
if is_blx:
try:
tgt = mu.reg_read(_REGS[rm])
except Exception:
tgt = -1
if len(calls) < 4000:
calls.append((addr, rm, tgt))
except Exception:
return
mu.hook_add(UC_HOOK_CODE, hook_code)
# null-fetch redirect: branch-to-0 lands in maze (movs r0,#0; bx lr),
# immune to [0]-writes that clobber a static maze copy
from unicorn.arm_const import UC_ARM_REG_PC as _PC
_nulllog = []
def hook_null(mu, addr, size, data):
if len(_nulllog) < 10:
try:
r = [mu.reg_read(_REGS[i]) for i in range(4)]
lr = mu.reg_read(_LR)
sp = mu.reg_read(UC_ARM_REG_SP)
except Exception:
r, lr, sp = [], -1, -1
# walk back: find the branch source from hist
_nulllog.append((count[0], r, lr, sp))
mu.reg_write(_PC, STUB + 1)
mu.hook_add(UC_HOOK_CODE, hook_null, begin=0x0, end=0xFFF)
if "--watchcode" in sys.argv:
from unicorn.arm_const import UC_ARM_REG_PC as _PC2
wlog = []
def hook_ww(mu, access, addr, size, value, data):
if len(wlog) < 30:
try:
pc = mu.reg_read(_PC2)
except Exception:
pc = -1
wlog.append((pc, addr, size, value))
mu.hook_add(UC_HOOK_MEM_WRITE, hook_ww,
begin=0x8005800, end=0x8039254)
import atexit
def _dump():
print(f"code-writes: {len(wlog)}")
for pc, a, sz, v in wlog[:30]:
print(f" pc={pc:#x} [{a:#x}] sz={sz} val={v:#x}")
atexit.register(_dump)
# --- machine scaffold parity (saimaa.c values, verbatim) ---
def W32(a, v):
mu.mem_write(a, struct.pack("<I", v & 0xFFFFFFFF))
S, STUB, STUBV = 0x08006B80, 0x08006B8C, 0x08006B8D
W32(S, S + 8)
W32(S + 4, STUBV)
W32(S + 8, STUBV)
# maze blob (18B)
mu.mem_write(STUB, bytes([
0x00, 0x20, 0x4f, 0xea, 0x1e, 0x6c, 0xbc, 0xf1,
0x08, 0x0f, 0x00, 0xd1, 0x70, 0x47, 0x72, 0xb6,
0xfd, 0xe7]))
print("maze-rb:", bytes(mu.mem_read(STUB, 18)).hex())
for a in (0x087c29d18, 0x08050b28, 0x08070000):
W32(a, S)
# S6 stublet (14B @0x08006B9E)
mu.mem_write(0x08006B9E, bytes([
0xbd, 0xe8, 0xfc, 0x47, 0x43, 0xf6, 0xa5, 0x3e,
0xc0, 0xf6, 0x01, 0x0e, 0x70, 0x47]))
# banner stub (34B @0x08006BAC, prints r0 via UARTDM)
mu.mem_write(0x08006BAC, bytes([
0x10, 0xb5, 0x4f, 0xf2, 0x00, 0x03, 0xc0, 0xf2,
0x8a, 0x73, 0x21, 0x21, 0x19, 0x67, 0x10, 0xf8,
0x01, 0x1b, 0x09, 0xb1, 0x19, 0x67, 0xfa, 0xe7,
0x4f, 0xf2, 0x94, 0x6e, 0xc0, 0xf6, 0x02, 0x0e,
0x70, 0x47]))
# PBL stub (52B @0x080068C0)
mu.mem_write(0x080068C0, bytes([
0xf0, 0xb5, 0x46, 0xf6, 0x81, 0x3c, 0xc0, 0xf6,
0x00, 0x0c, 0xe0, 0x47, 0x40, 0xf2, 0x00, 0x00,
0xc0, 0xf2, 0x22, 0x00, 0x05, 0x46, 0x40, 0xf2,
0x00, 0x0a, 0xc0, 0xf6, 0x06, 0x0a, 0x41, 0xf2,
0x00, 0x0b, 0xc0, 0xf6, 0x06, 0x0b, 0x4f, 0xf2,
0x3f, 0x6e, 0xc0, 0xf6, 0x02, 0x0e, 0x70, 0x47,
0x00, 0x00, 0x00, 0x00]))
# S7 stublet (52B @0x08006C10, b18): delay-epilogue exit router.
# cmp r4,#0xff: iterate (r4<=0xff -> restart 0x8013ba5) or done
# (restore r4/r5/sl/fp, return to sbl_main 0x802f681).
mu.mem_write(0x08006C10, bytes.fromhex(
"ff2c13d940f20004c0f2220440f20005c0f2220540f2000a"
"c0f6060a41f2000bc0f6060bdff800f081f60208dff800f0"
"a53b0108"))
# stack zone fill (STUBV odd)
mu.mem_write(0x085F0000, struct.pack("<I", STUBV) * (0x10000 // 4))
# pop-site slots (sp values from traces)
for a, v in [(0x085FFF14, 0x802f65d), (0x085FFF1C, 0x802f665),
(0x085FFF44, 0x8013ba5), (0x085FFF5C, 0x802f681),
(0x085FFF74, 0x8016183)]:
W32(a, v)
W32(0x085FFF58, 0)
W32(0x085FFF34, 0)
W32(0x085FFF54, 0)
W32(0x085FFF6C, 0)
W32(0x08600944, 0) # IMEM poll magic
# minimal UEFI tables (ST/BS/RS/CO + ret0 stubs)
ST, BS, RS, CO, STUBS, LI, HEAP = (0x13000000, 0x13000100, 0x13000200,
0x13000300, 0x13000400, 0x13000500,
0x13001000)
for i in range(24):
mu.mem_write(STUBS + i * 8, bytes([0x00, 0x20, 0x70, 0x47,
0x00, 0x00, 0x00, 0x00]))
W32(BS + 24 + i * 4, STUBS + i * 8 + 1)
W32(BS, 0x56524553)
W32(ST, 0x49535953)
W32(ST + 4, 0x54535953)
W32(ST + 12, 0x00020000)
W32(ST + 16, 0x13000048)
W32(ST + 40, CO)
W32(ST + 44, CO)
W32(ST + 56, RS)
W32(ST + 60, BS)
W32(LI, 0x3130414d)
W32(HEAP - 4, HEAP)
# NOTE: no vendor string bytes (QEMU leaves zeros at 0x13000048)
# map UEFI table region (already in RAMS? add 0x13000000)
# --- end parity ---
# stub range is readonly on HW/QEMU: ignore SBL heap-clobber writes,
# keep maze/stublets intact (restore-on-write with reentrancy guard)
STUB_LO, STUB_HI = 0x080068C0, 0x08006C50 # +S7 + banner tail
stub_snap = bytes(mu.mem_read(STUB_LO, STUB_HI - STUB_LO))
_inww = [False]
_swn = [0]
_swlog = []
_dirty = [False, []]
print("stub-snap:", stub_snap[:24].hex())
print("stub-snap-maze:", stub_snap[0x2CC:0x2CC + 18].hex())
def hook_stubw(mu, access, addr, size, value, data):
if _inww[0]:
return
if addr + size <= STUB_LO or addr >= STUB_HI:
return
_swn[0] += 1
if len(_swlog) < 8:
try:
_pc = mu.reg_read(_PC)
except Exception:
_pc = -1
_swlog.append((_pc, addr, size))
# record for deferred restore (see hook_code); in-hook restore
# would be overwritten by the faulting instruction itself
a = max(addr, STUB_LO)
b = min(addr + size, STUB_HI)
if b > a and len(_dirty[1]) < 16:
_dirty[1].append((a, b))
_dirty[0] = True
#hook all DDRLOW, filter in python (catches writes starting below LO)
mu.hook_add(UC_HOOK_MEM_WRITE, hook_stubw,
begin=0x08000000, end=0x085FFFFF)
if "--watchmaze" in sys.argv:
from unicorn.arm_const import UC_ARM_REG_PC as _PC3
mzlog = []
def hook_mz(mu, access, addr, size, value, data):
if len(mzlog) < 40:
try:
pc = mu.reg_read(_PC3)
except Exception:
pc = -1
mzlog.append((pc, addr, size, value))
mu.hook_add(UC_HOOK_MEM_WRITE, hook_mz,
begin=0x08006B8C, end=0x08006B9D)
import atexit as _ax
def _dmz():
print(f"maze-writes: {len(mzlog)}")
for pc, a, sz, v in mzlog[:40]:
print(f" pc={pc:#x} [{a:#x}] sz={sz} val={v:#x}")
_ax.register(_dmz)
if "--watchlow" in sys.argv:
_spec = sys.argv[sys.argv.index("--watchlow") + 1].split(":")
_wlo, _whi = int(_spec[0], 0), int(_spec[0], 0) + int(_spec[1], 0)
from unicorn.arm_const import UC_ARM_REG_PC as _PC4
wlolog = []
def hook_wlo(mu, access, addr, size, value, data):
if len(wlolog) < 60:
try:
pc = mu.reg_read(_PC4)
except Exception:
pc = -1
wlolog.append((pc, addr, size, value))
mu.hook_add(UC_HOOK_MEM_WRITE, hook_wlo,
begin=0x08000000, end=0x085FFFFF)
import atexit as _ax2
def _dwlo():
inn = [(pc, a, sz, v) for pc, a, sz, v in wlolog
if not (a + sz <= _wlo or a >= _whi)]
print(f"low-writes-in-range: {len(inn)}")
for pc, a, sz, v in inn[:60]:
print(f" pc={pc:#x} [{a:#x}] sz={sz} val={v:#x}")
_ax2.register(_dwlo)
limit = 2000000
if "--limit" in sys.argv:
limit = int(sys.argv[sys.argv.index("--limit") + 1])
try:
mu.emu_start(thumb | 1, 0xFFFFFFFF, 0, limit)
except Exception as e:
print("EMU-EXC:", str(e)[:120])
print("insns:", count[0], "last-pc:", hex(last[0]))
print("unmapped:", unmapped["n"], unmapped["first"][:4])
print("regs:",
" ".join(f"r{i}={mu.reg_read(_REGS[i]):#x}" for i in range(8)),
f"sp={mu.reg_read(UC_ARM_REG_SP):#x}",
f"lr={mu.reg_read(UC_ARM_REG_LR):#x}",
f"pc={mu.reg_read(UC_ARM_REG_PC):#x}")
try:
from capstone import Cs, CS_ARCH_ARM, CS_MODE_THUMB, CS_MODE_ARM
md_t = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
md_a = Cs(CS_ARCH_ARM, CS_MODE_ARM)
real = [(pc, th) for pc, th in hist
if not (0 <= (pc & ~1) < 0x1000)][-48:]
for pc, thumb in real:
try:
base = pc & ~1
code = mu.mem_read(base, 8 if thumb else 12)
md = md_t if thumb else md_a
for ins in md.disasm(code, base):
print(f" {base:#x}{'t' if thumb else 'a'}: {ins.mnemonic} {ins.op_str}")
break
except Exception:
print(f" {pc:#x}: <unreadable>")
except ImportError:
print("hist:", [hex(pc) for pc, _ in list(hist)[-12:]])
if elog is not None:
print(f"entries: {len(elog)}")
for n, prev, cur in elog[-30:]:
print(f" insn#{n} {prev:#x} -> {cur:#x}")
if _splog is not None:
print(f"sp-highs: {len(_splog)}")
for n, pc, sp in _splog:
print(f" insn#{n} {pc:#x} sp={sp:#x}")
if _nulllog:
print(f"null-entries: {len(_nulllog)}")
for n, r, lr, sp in _nulllog:
print(f" insn#{n} r0-r3={[hex(x) for x in r]} lr={lr:#x} sp={sp:#x}")
if qlog is not None:
print(f"querylog: {len(qlog)}")
for pc, r, sp, lr in qlog[:20]:
print(f" {pc:#x} r0-r7={[hex(x) for x in r]} sp={sp:#x} lr={lr:#x}")
if rets is not None:
print(f"rets-in-range: {len(rets)}")
for pc, rv in rets[-20:]:
print(f" ret {pc:#x} r0={rv:#x}" if rv >= 0 else f" ret {pc:#x} r0=?")
if calls is not None:
print(f"blx-calls: {len(calls)}")
bls = [(pc, tgt) for pc, rm, tgt in calls if rm == 200]
print(f"bl-immed: {len(bls)}")
for pc, tgt in bls[-30:]:
print(f" {pc:#x}: bl #{tgt:#x}")
for pc, rm, tgt in calls[-12:]:
if rm == 200:
continue
tag = f"bx r{rm - 100}" if rm >= 100 else f"blx r{rm}"
print(f" {pc:#x}: {tag} -> {tgt:#x}" if tgt >= 0 else f" {pc:#x}: {tag} -> ?")
if "--dumpmem" in sys.argv:
for spec in sys.argv[sys.argv.index("--dumpmem") + 1].split(","):
a, sz = spec.split(":")
a, sz = int(a, 0), int(sz, 0)
print(f"mem[{a:#x}+{sz:#x}]:")
print(bytes(mu.mem_read(a, sz)).hex())
print("UART:", "".join(uart_out)[:160])
print(f"stubguard-hits: {_swn[0]}")
for _pc, _a, _s in _swlog:
print(f" guarded pc={_pc:#x} [{_a:#x}] sz={_s}")
if __name__ == "__main__":
main()