Files
BFM-decomp/cookbook/C0281.md
T

6.0 KiB

§260 — THE §154-A LEADING-ISLAND SPLIT: ONE CONFIG LINE, AND THE ISLAND PEELS FROM THE END (P31 S59, byte-proven)

An md_* module binds .rodata at offset 0 to the same subseg as its code, so the object's rodata order is the C file's include chain: the INCLUDE_RODATA blobs, then every INCLUDE_ASM'd function's still-migrated jump table, in source order. That island reproduces byte-exactly while the functions are stubs, and the moment one is matched its table leaves the chain and cc1 re-emits it at the end of the object's .rodata — the +8-and-everything-shifts failure P30 S48 measured.

THE WHOLE FIX IS ONE INSERTED CONFIG LINE PLUS AN ISOLATION. On md_SC03_076 / func_801F218C:

      - [0x0, .rodata, md_SC03_076]                      # leave the island piece ALONE
      - [0x268, .rodata, md_SC03_076_jr_801F218C]        # the split: the function's own table
      - [0x27c, c, md_SC03_076]
      - [0x2d24, c, md_SC03_076_jr_801F218C]             # jr_isolate_all.py --only

Do not create a _pre piece (a dotted .rodata with no sibling .c points splat's ld at a never-built implied C file and unbinds the island from its module). Do not touch ld_interleave — the generated script is already rodata-first in yaml order. Use jr_isolate_all.py --only, not jr_isolate.py (its backend sys.exits on a top-level extern block).

THE CONTROL THAT SEPARATES "IT WORKED" FROM "IT DID NOTHING." A green SHA proves nothing on its own here, because a split that silently failed to happen is also green. Pair it with the object-level size:

whole-binary sha1 md_SC03_076.o .rodata md_SC03_076_jr_801F218C.o .rodata
before 9a165e36… 0x27c (the whole island) —
after 9a165e36… 0x268 0x14 (the 5-entry table)

THE ISLAND IS A STACK. Census of md_SC03_076 (file offsets): D_801EF468 0x000, D_801EF540 0x0D8, then func_801EFBB4 0x144 · func_801F0210 0x1B4 · func_801F0734 0x1EC · func_801F0A9C 0x214 · func_801F0F28 0x23C · func_801F218C 0x268 → 0x27C, the island end. Only the end-adjacent table carves cheaply; each isolation makes the next one end-adjacent, so a module peels from the end, one function at a time. Picking a middle table first is what makes the job look like cascading isolation.

TWO TOOL BLINDNESSES THIS EXPOSED, both md_*-only and both now fixed. (1) jtbl_carve.parse_config took the FIRST data/.rodata piece in the file rather than the trailing run after the last c — identical on 171 configs, and on the 42 md_* it pointed at the island, so apply()'s splice DELETED the c line and wrote the yaml to disk before erroring for unrelated reasons. (2) jr_isolate_all.jr_inventory asserts every .rodata piece resolves to exactly one banked owner (R32) — true where jtbl_carve created every piece, false for the island, which has no owner; it aborted with UNOWNED 0x801ef468 and md_* could not be isolated at all. The structural discriminator, verified over all 213 configs: a .rodata piece at offset 0 whose subseg is the binary's own alias exists in exactly the 42 md_* and in none of the others.

§260-A — STAGE 2 IS PROVEN, AND THE WHOLE jtbl PIPELINE IS AUTOMATED AT THE GATE (P31 S59, same day)

Three byte-proofs, one per structural class, each carve→draft→bank through tools/jtbl_lane.py → gate_stage.run_gate → harvest_verify._jtbl_prep_one with the live campaign running (per-binary flock + exclusive shared lock + the campaign's draw lock held across gate+commit — gates never overlap):

  • tail (ov_SC03_014/func_8017DCC0, 45 ins, commit b801b499e): the §8b same-subseg ADJACENT MERGE + §8e single-table-predecessor pad recovery, untouched machinery — jr_8017AE2C.o .rodata 0x14→0x28 TIGHT, JTBL_PADS := 0,0 tables=+0x0,+0x14 self-derived, sha d84b01a2… green.
  • covered (md_SC03_076/func_801F218C, 83 ins, commit bad793c73): §260 STAGE 2 — the matched C emits the island table itself; sha 9a165e36… IDENTICAL to stage 1, jr .rodata 0x14 now COMPILER-EMITTED, md .rodata 0x268. The first md_ jr function ever banked.* jtbl_carve now reports this state as a no-op success instead of the historical refusal.
  • island-end (md_SC03_135/func_801E5358, 83 ins, commit f74ad7ac8): the FULL split done BY THE GATE on a virgin module — isolate → insert - [0x268, .rodata, <ov>_jr_<ADDR>] → re-extract → covered no-op → bank; sha b901fda5…, md.o 0x27c→0x268 + jr.o 0x14. ~1.1 s wall (an md module is 34 KB; R40-checked against artifact mtimes before believing it).

The joins that made it automatic (each one line to find): jtbl_carve.island_probe — the READ-ONLY structural classifier (tail / covered / island-end / island-blocked / island-pads / main-manual) that build_wave_atlas (--levers jtbl-carve only) and jtbl_lane --census route on; jtbl_carve --island-split — the one-line §260 insert, refusing non-end-adjacent tables; harvest_verify._ISLAND_WALLS — the prep branch that runs isolate (body spliced, §61b) → split → extract → re-carve on the §154-A refusal. Census over all 245 jtbl members (R32): tail 173 / island-end 7 / covered 1 = 181 members, 26,445 ins reachable unattended; main-manual 47 (parked), island-blocked 10 (stack-ordered, convert as peels land), island-pads 6 (needs §8e pads wired for config/modules.mk — jtbl_carve's mk writers are overlays.mk-only), no-jtbl 1 (atlas mislabel). ORDER LAW for the island branch: isolate FIRST, insert SECOND — jr_inventory's 1:1 owner assertion runs inside the isolation, and the fresh line has no owner until the body is real C. Drafter nuance that cost the only iteration: a callee reached via an eagerly-filled branch delay slot (a0=s0 serving the ELSE path) reads like an argument — func_801EF6E4 takes NONE (extern void f(void)), and the one-mismatch signature is a redundant move a0,s0 in the jal's own slot. Full design + failure semantics: docs/sunset/tool-designs/jtbl-automation-s59.md.