From 495963cfa32b7f0deb409dcbd010acdc512c8a64 Mon Sep 17 00:00:00 2001 From: seedds Date: Sun, 18 Oct 2026 13:32:32 +0800 Subject: [PATCH] patch_nso: retry blocks into exclusive windows, adr-relative jump tables (Tears of the Kingdom) Tears of the Kingdom 1.4.3's main stopped the rewriter at three new shapes: - An atomic update's retry path moved out of line: the failed stxr branches to ldxr; cmp; b.eq , which re-enters the window at its store (main+0x18598 -> 0x181fc, 8 sites). retry_block / site_retry_window give that block a patch copy of its own: the retry block and the window's tail from the store on, under one prologue, so no memory access falls between the exclusive load and the store. - A window no longer spans a second exclusive load. A retry block's ldxr at main+0x5a4674 used to open a window that ran on into the next, unrelated ldxr ... stxr loop, and branches to that loop's head landed mid-window. - Jump tables whose entries are relative to an adr, not to the table (main+0x393bb0: adr x18; ldrsw x1,[x17,x0,lsl #2]; add x18,x18,x1; br x18), with the bounds check on a copied register (mov w16,w15) and, once, a table base built far from the dispatch: every adrp + add pair building that register in the function is tried, and a table is accepted only if exactly one makes every target an instruction of the function. jump_adr_offsets is tried only where jump_offsets finds nothing. main now rewrites (134 windows, 8 retry windows, 814 tpidr_el0 reads as in the census). Every other extracted module of every title (31: Mario 1.0 and 1.2.1, Engage, Metroid, Sentinels, 3D World, Breath of the Wild, Link's Awakening, Odyssey, Hades) gives byte-identical output; only build_src.sha256, the rewriter's own hash, differs. --- dm/tools/patch_nso.py | 122 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/dm/tools/patch_nso.py b/dm/tools/patch_nso.py index 5c807d9..28d3836 100644 --- a/dm/tools/patch_nso.py +++ b/dm/tools/patch_nso.py @@ -506,6 +506,9 @@ def find_windows(insl): j = i while j < len(insl) and insl[j].mnemonic not in EXCL_ST: j += 1 if j >= len(insl) or j - i >= 16: continue + # Another exclusive load before the store: this one has no store of its own (a retry block, see retry_block); spanning both would merge two sequences + # (Tears of the Kingdom main+0x5a4674: ldxr w18; cmp; b.eq ; ...; 0x5a469c: ldxr w13; and; stxr; cbnz, a loop other code branches to) + if any(x.mnemonic in EXCL_LD for x in insl[i + 1:j]): continue if j + 1 < len(insl) and insl[j + 1].mnemonic in ('cbz', 'cbnz'): j += 1 seg = insl[i:j + 1] if any(has18(x) for x in seg): out.append(seg) @@ -544,6 +547,57 @@ def site_window(seg): emit(STRX(S, T)); emit(LDP(S, T, 31, -16)); emit(B(here(), t)); emit(NOP) return start, lo, hi +def retry_block(insl, k, wseg): + """The branch insl[k] lands in the middle of window wseg. Accepted only when it ends an out-of-line retry block that re-enters the window at its store-exclusive: + an exclusive load, then instructions that do not branch, then this branch. Tears of the Kingdom main+0x18598: ldxr w17,[x10]; cmp w15,w17; b.eq 0x181fc, + where 0x181fc is the stxr w17,w18,[x10] of the window 0x181ec..0x18200, whose failed stxr branches back to 0x18598 (same shape at 0x185e8 → 0x18244). + Returns (the retry block, the window from that store on), or None""" + t = insl[k].operands[-1].imm + tail = [y for y in wseg if y.address >= t] + if not tail or tail[0].mnemonic not in EXCL_ST: return None + for p in range(k - 1, max(-1, k - 8), -1): + q = insl[p] + if q.mnemonic in EXCL_LD: return insl[p:k + 1], tail + if q.mnemonic in ('b', 'bl', 'br', 'blr', 'ret', 'cbz', 'cbnz', 'tbz', 'tbnz') or q.mnemonic.startswith('b.') or q.mnemonic in EXCL_ST: return None + return None + +def site_retry_window(parts): + """A retry block (see retry_block) gets a patch copy of its own: it cannot jump into the window's copy, because the x18 stand-in would have to be loaded + between its exclusive load and the store. parts = [(retry block, its fall-through), (window from the store on, the window's fall-through)] are copied one + after the other under one prologue, each followed by its fall-through stub; branches to any copied instruction (between the parts, and the failed store's + branch back to the retry block's head) stay inside the copy, every other target leaves through an exit stub as in site_window""" + used = set() + for seg, _ in parts: + for x in seg: used |= regs_in(x) + S, T = pick(used) + start = here() + emit(STP(S, T, 31, -16)); emit(MRS_TPIDRRO(T)); emit_slot_addr(T, S, 'K18'); emit(LDRX(S, T)) + pos, at = {}, here() + for seg, _ in parts: + for x in seg: pos[x.address] = at; at += 4 + at += 16 + exits, ex_targets = at, [] + for seg, ft in parts: + for x in seg: + w = struct.unpack('> 2 + if m in ('tbz', 'tbnz'): w = (w & 0xFFF8001F) | ((off & 0x3FFF) << 5) + else: w = (w & 0xFF00001F) | ((off & 0x7FFFF) << 5) + dd = dis(w, pc); assert dd.operands[-1].imm == newt and dd.mnemonic == m + else: + assert m not in ('adr', 'adrp', 'b', 'bl', 'br', 'blr', 'ret') and not (m == 'ldr' and '[' not in x.op_str), hex(x.address) + emit(w) + emit(STRX(S, T)); emit(LDP(S, T, 31, -16)); emit(B(here(), ft)); emit(NOP) + for t in ex_targets: + emit(STRX(S, T)); emit(LDP(S, T, 31, -16)); emit(B(here(), t)); emit(NOP) + return start + # ---- Function boundaries (eh_frame_hdr) ---- m0 = struct.unpack_from('= 0 else (a, a + 4) -done = set(); windows = []; jumps = {}; tabjumps = {} +done = set(); windows = []; jumps = {}; tabjumps = {}; retries = [] def jump_offsets(insl, j): """The br x18 is preceded by the offset-table form (Engage main+0x12eec94): cmp w18,#N; b.hi; ldrsw x18,[xB,x18,lsl #2]; add x18,x18,xB. The index may be another register, and the bounds check may be on a register copied into it: Link's Awakening's GLSLC (subsdk0+0x1be6e8) has @@ -636,6 +690,52 @@ def jump_offsets(insl, j): if page is None: return None tb = page + lo return [tb + struct.unpack_from('