mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-27 22:45:39 -04:00
5e78dccf07
- ROOT CAUSE PINNED (the session-2 half-pin was INVERTED; both probes were vacuous, R35): cc1 emits .align 3 before EVERY jump table; maspsx passes it VERBATIM (the :435 'drop' is an inventory-only pass); as bakes the pad SECTION-RELATIVE; link placement was never guilty (SUBALIGN(2) + ALIGN(.,4) place 4-mod-8 carve starts tight). Merging originally-separate TUs fires an intra-TU align where the original packed tight -> +4 at rodata 0xCC -> image-wide %lo shift. Honest probes persisted: .run/probe_jtbl/ (verdict.md + objdumps). - NEW tools/jtbl_rodata_pads.py: post-maspsx filter replaces each rodata .align 3 with the ORIGINAL's exact pad bytes per a JTBL_PADS spec; fail-loud on table-count drift / non-align-3 / non-jtbl rodata content. Byte-proven: verbatim 0xE4 pad-at-0xCC -> filtered 0xE0 tight (= the merged carve span). - jtbl_carve.py: spec-aware same-subseg merge (gap 0 or 4-with-zero-payload-word; else NON-CONTIGUOUS -> isolate), interval-arithmetic pad specs (committed values CARRIED, never re-derived), JTBL_PADS target-var emission into overlays.mk + revert() restore + stale-.o invalidation; the false 'maspsx drops .align' docstring corrected (H5). - Makefile: $(if $(JTBL_PADS),| jtbl_rodata_pads.py ...) stage in build/src/%.o + file-scope empty default (env-shield). jtbl_family_bank.stub_file: duplicate-stub fail-loud (the earlier 'ladder failure' was a wrong-TU splice into a stale _a.c stub, byte-witnessed). - R22 clean-fleet WITH the fix wired: 140/140 byte-identical, tools-health green (dedup 1846/0, C1 234205/234205), ZERO new banks -- fleet-neutral by construction. - cookbook §8e (the jtbl alignment law) + §8a/§8a-pad corrections; decision-log R31 entry; SETUP.md tool row; .gitignore allowlist for the probe verdict artifacts.
105 lines
5.1 KiB
Python
105 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Phase-29 §8e: reproduce the ORIGINAL's jump-table padding in a multi-table .rodata carve.
|
|
|
|
The mechanism (byte-proven, .run/probe_jtbl/verdict.md):
|
|
- cc1 (Sony gcc 2.7.2) emits `.rdata` + `.align 3` + label before EVERY switch jump table.
|
|
- maspsx passes `.align` through VERBATIM (the maspsx/__init__.py:435 `continue` is in an
|
|
inventory-only pass; the output path re-emits `.align` unchanged).
|
|
- GNU as bakes the align pad into the section SECTION-RELATIVE — the linker can never remove it
|
|
(SUBALIGN(2) caps only the section's *external* placement alignment).
|
|
- The ORIGINAL image packs originally-separate TUs' tables TIGHT (the PSX linker 4-aligned
|
|
section placement), while intra-TU consecutive tables carry REAL zero-word pads where the
|
|
previous table ends ≡4 mod 8.
|
|
|
|
So when a decomp TU holds MULTIPLE matched jr-functions (originally separate TUs merged), cc1's
|
|
`.align 3` fires at boundaries where the original packed tight -> a +4 interior pad -> every
|
|
downstream data symbol shifts -> %lo relocs break image-wide. And conversely a table's alignment
|
|
pad must appear exactly where the original HAS one. `.align` cannot express this (it is relative
|
|
to the section start, whose vram parity varies per overlay sibling) — only explicit pad bytes can.
|
|
|
|
This filter sits between maspsx and as (Makefile `build/src/%.o` recipe, armed per-object by a
|
|
`JTBL_PADS` target-specific make var written by tools/jtbl_carve.py). It REPLACES each rodata
|
|
`.align` with the exact pad the original has at that table boundary:
|
|
|
|
--pads 0,4,0 -> table 1: no pad (specs[0] is always 0 — the table starts the section),
|
|
table 2: one `.word 0` pad, table 3: no pad.
|
|
|
|
Pads are derived by jtbl_carve from the carve intervals (pad[K] = start[K] - end[K-1], values in
|
|
{0,4}), so each overlay sibling gets its own spec from its own addresses. Unfiltered objects
|
|
(no JTBL_PADS var — every single-table carve) keep today's byte-identical pipeline.
|
|
|
|
Fail-loud guards (R32) — each converts a silent downstream SHA1 mismatch into an immediate,
|
|
attributable build error:
|
|
- spec count != rodata `.align` count (table-count drift: a sibling's switch compiled
|
|
to a different number of tables)
|
|
- a rodata `.align` other than `.align 3` (unknown emission shape)
|
|
- rodata content other than $L labels / `.word $L...` / blank (strings, floats — the carve
|
|
model does not cover them)
|
|
- pads[0] != 0, or any pad not in {0, 4}
|
|
"""
|
|
import argparse
|
|
import re
|
|
import sys
|
|
|
|
LABEL_RE = re.compile(r"^\$L\d+:$")
|
|
WORD_RE = re.compile(r"^\.word\s+\$L\d+$")
|
|
|
|
|
|
def run(pads, lines, out):
|
|
in_rodata = False
|
|
consumed = 0
|
|
for line in lines:
|
|
s = line.strip()
|
|
if s == ".section .rodata":
|
|
in_rodata = True
|
|
out.write(line)
|
|
continue
|
|
if in_rodata and (s == ".text" or s.startswith(".section")):
|
|
in_rodata = False
|
|
out.write(line)
|
|
continue
|
|
if in_rodata:
|
|
if s.startswith(".align"):
|
|
parts = s.split()
|
|
if len(parts) != 2 or parts[1] != "3":
|
|
sys.exit(f"jtbl_rodata_pads: unexpected rodata align {s!r} "
|
|
f"(cc1 emits `.align 3` per jump table — see verdict.md)")
|
|
if consumed >= len(pads):
|
|
sys.exit(f"jtbl_rodata_pads: more rodata .align directives than pad specs "
|
|
f"({len(pads)}) — table-count drift vs the carve")
|
|
if pads[consumed] == 4:
|
|
out.write(".word 0 # jtbl_rodata_pads: original inter-table .align 3 pad\n")
|
|
consumed += 1
|
|
continue
|
|
if s == "" or LABEL_RE.match(s) or WORD_RE.match(s):
|
|
out.write(line)
|
|
continue
|
|
sys.exit(f"jtbl_rodata_pads: unexpected rodata content {s!r} — "
|
|
f"the carve model covers only jump tables ($L labels + .word entries)")
|
|
out.write(line)
|
|
if consumed != len(pads):
|
|
sys.exit(f"jtbl_rodata_pads: consumed {consumed} rodata .align(s) but {len(pads)} pad "
|
|
f"spec(s) given — table-count drift vs the carve")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--pads", required=True,
|
|
help="comma list of pad BYTES before each rodata jump table, in emission "
|
|
"order; pads[0] must be 0")
|
|
a = ap.parse_args()
|
|
try:
|
|
pads = [int(x) for x in a.pads.split(",")]
|
|
except ValueError:
|
|
sys.exit(f"jtbl_rodata_pads: malformed --pads {a.pads!r}")
|
|
if not pads or pads[0] != 0:
|
|
sys.exit(f"jtbl_rodata_pads: pads[0] must be 0 (the first table starts the section): {pads}")
|
|
if any(p not in (0, 4) for p in pads):
|
|
sys.exit(f"jtbl_rodata_pads: every pad must be 0 or 4: {pads}")
|
|
run(pads, sys.stdin, sys.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|