feat(phase-29 §8e): jtbl 8-align pad-spec filter — the 4-giant unblock tooling (fleet-neutral)

- 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.
This commit is contained in:
Drew T
2026-07-18 02:03:48 -06:00
parent 1dbf8310ef
commit 5e78dccf07
12 changed files with 490 additions and 33 deletions
+5
View File
@@ -36,6 +36,11 @@
!/.run/giants/*.c
!/.run/giants/*.md
!/.run/giants/*.sh
!/.run/probe_jtbl/
/.run/probe_jtbl/*
!/.run/probe_jtbl/verdict.md
!/.run/probe_jtbl/rodata_verbatim.objdump
!/.run/probe_jtbl/rodata_filtered.objdump
!/.run/giants/fable_cd4/
/.run/giants/fable_cd4/*
!/.run/giants/fable_cd4/*.c
+20
View File
@@ -0,0 +1,20 @@
.run/probe_jtbl/tu_filtered.o: file format elf32-tradlittlemips
Contents of section .rodata:
0000 50000000 50000000 50000000 50000000 P...P...P...P...
0010 50000000 50000000 50000000 50000000 P...P...P...P...
0020 50000000 50000000 50000000 50000000 P...P...P...P...
0030 50000000 50000000 50000000 50000000 P...P...P...P...
0040 50000000 50000000 50000000 50000000 P...P...P...P...
0050 50000000 50000000 50000000 50000000 P...P...P...P...
0060 50000000 50000000 50000000 50000000 P...P...P...P...
0070 50000000 50000000 50000000 50000000 P...P...P...P...
0080 50000000 50000000 50000000 50000000 P...P...P...P...
0090 50000000 50000000 50000000 50000000 P...P...P...P...
00a0 50000000 50000000 50000000 50000000 P...P...P...P...
00b0 50000000 50000000 50000000 50000000 P...P...P...P...
00c0 48000000 48000000 48000000 60670000 H...H...H...`g..
00d0 60670000 54690000 6c6a0000 146c0000 `g..Ti..lj...l..
[ 9] .rodata PROGBITS 00000000 00f6f0 0000e0 00 A 0 0 4
[10] .rel.rodata REL 00000000 017abc 0001c0 08 I 12 9 4
+22
View File
@@ -0,0 +1,22 @@
.run/probe_jtbl/tu_verbatim.o: file format elf32-tradlittlemips
Contents of section .rodata:
0000 50000000 50000000 50000000 50000000 P...P...P...P...
0010 50000000 50000000 50000000 50000000 P...P...P...P...
0020 50000000 50000000 50000000 50000000 P...P...P...P...
0030 50000000 50000000 50000000 50000000 P...P...P...P...
0040 50000000 50000000 50000000 50000000 P...P...P...P...
0050 50000000 50000000 50000000 50000000 P...P...P...P...
0060 50000000 50000000 50000000 50000000 P...P...P...P...
0070 50000000 50000000 50000000 50000000 P...P...P...P...
0080 50000000 50000000 50000000 50000000 P...P...P...P...
0090 50000000 50000000 50000000 50000000 P...P...P...P...
00a0 50000000 50000000 50000000 50000000 P...P...P...P...
00b0 50000000 50000000 50000000 50000000 P...P...P...P...
00c0 48000000 48000000 48000000 00000000 H...H...H.......
00d0 60670000 60670000 54690000 6c6a0000 `g..`g..Ti..lj..
00e0 146c0000 .l..
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 9] .rodata PROGBITS 00000000 00f6f0 0000e4 00 A 0 0 8
[10] .rel.rodata REL 00000000 017ac0 0001c0 08 I 12 9 4
+33
View File
@@ -0,0 +1,33 @@
# jtbl 8-align probe — the HONEST stage-walk (2026-07-18, replaces the vacuous probe_jtbl_{cc1,maspsx}.s)
**Target:** func_80131340 (424 ins) draft spliced over its INCLUDE_ASM stub in the committed
src/ov_SC01_077/ov_SC01_077_jr_8012ACE0.c (probe copy: tu_spliced.c). Real pipeline, real flags.
## Verdict — the session-2 checkpoint's half-pin ("cc1 AND maspsx emit .align 2") is INVERTED
| stage | observable | result |
|---|---|---|
| cc1 (Sony gcc 2.7.2) | directives before EACH jump table | `.rdata` + **`.align 3`** + label (cc1.s L46-48 → $L15/51 words = jtbl_801D8078; L10578-80 → $L810/5 words = jtbl_801D8144). Exactly one `.align 3` per table, exactly 2 tables in the TU. |
| maspsx (pinned 874855c) | `.align` handling | **passed through VERBATIM** (maspsx.s L40-42, L9312-14; `.rdata`→`.section .rodata` only). The `continue` at maspsx/__init__.py:435-437 is inventory-only (`preprocess_lines`); the output path catch-all (L872-873) re-emits `.align`. |
| GNU as 2.42 | .rodata content | **size 0xE4, sh_addralign=8, ZERO PAD WORD AT OFFSET 0xCC** (rodata_verbatim.objdump: `00c0 48000000 48000000 48000000 00000000` then table 2 at 0xD0). The +4 interior pad, baked into the object — the linker can never remove it. |
| as control | `.section .rodata`+`.word` alone → Al=**4** (ctrl.o); +`.align 3` → Al=**8** (ctrl2.o) | the Al=8 is cc1's directive, NOT an as default. |
## Why this breaks the image
Original layout (asm/ov_SC01_077/data/tail2.data.s): jtbl_801D8078's 51 entries end 0x801D8144 exactly
where jtbl_801D8144 begins — originally-SEPARATE TUs, packed TIGHT by the original linker. Merging both
into one decomp TU makes cc1's intra-TU `.align 3` fire at 0xCC (%8==4) where the original had a tight TU
boundary → every downstream data symbol +4 → %lo relocs break image-wide (.run/build_g2.log SHA1 FAIL).
Intra-TU pads ARE real elsewhere (jtbl_801D8158/8170/8188/81A0: 5 entries + one 0x00000000 each, every
non-first table landing %8==0) — so the align must be REPLACED by the original's exact pad bytes
(pad ∈ {0,4} from carve-interval arithmetic), not blanket-dropped. Isolation is not general: as computes
`.align` SECTION-RELATIVE, so a 4-mod-8-start multi-table object would mis-pad internally.
**Fix:** tools/jtbl_rodata_pads.py post-maspsx filter, per-object JTBL_PADS spec from jtbl_carve.
For this TU the spec is `0,0` → expected filtered .rodata = 0xE0, table 2 tight at 0xCC (= the merged
carve span 0xaff20..0xB0000).
## CONFIRMED (same session): the filter's byte proof
`maspsx.s | jtbl_rodata_pads.py --pads 0,0 | as` → `.rodata` size **0xE0**, Al=4, table 2 TIGHT at
0xCC (rodata_filtered.objdump) = the merged carve span 0xaff20..0xB0000 exactly. Guards verified
fail-loud (1-spec / 3-spec / pads[0]=4 all rc=1 with attributable messages). The pad=4 direction
verified synthetically (`.word 0` emitted in place of the align).
+12 -1
View File
@@ -436,6 +436,12 @@ ASPSX_VERSION := 2.56
# maspsx leaves a bare divu and div/rem functions never match. Only affects div/rem,
# so the all-INCLUDE_ASM build and div-free functions are unchanged.
MASPSX_FLAGS := --expand-div
# Phase-29 §8e: per-object jump-table pad spec (tools/jtbl_rodata_pads.py). Set ONLY as a
# target-specific var by tools/jtbl_carve.py in config/overlays.mk for multi-table .rodata
# carve spans; the file-scope empty default shields the recipe from an inherited environment
# variable accidentally arming the filter fleet-wide (a plain `JTBL_PADS=... make` would
# otherwise become a global make var). Unset => the compile pipeline is byte-unchanged.
JTBL_PADS :=
# Object set must match the splat linker script's references. After the Phase-6 asm->c
# flip the text subseg is src/800.c -> build/src/800.o; the per-function
@@ -539,10 +545,15 @@ build/assets/%.o: assets/%.bin
# fingerprint ladder PINS the triple (then this block + ASPSX_VERSION are updated, G8).
CPPFLAGS := -lang-c -Iinclude -undef -Wall -fno-builtin -Dmips -D__GNUC__=2 -D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C
CC1FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
# The optional jtbl_rodata_pads stage (Phase-29 §8e) is inserted only when the object has a
# JTBL_PADS target-specific var (written by tools/jtbl_carve.py for multi-table .rodata carve
# spans): it replaces cc1's per-table `.align 3` with the ORIGINAL's exact pad bytes, so a merged
# span reproduces the original packing regardless of section-start parity. Unset => stage absent,
# pipeline byte-identical to pre-§8e.
build/src/%.o: src/%.c
@mkdir -p $(dir $@)
@echo " CC $@"
@set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) | $(AS) $(ASFLAGS) -o $@
@set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) $(if $(JTBL_PADS),| $(VENV_PY) tools/jtbl_rodata_pads.py --pads $(JTBL_PADS)) | $(AS) $(ASFLAGS) -o $@
# Per-module optimization override (SETUP §5.5 — per-module compiler mixing). The boot/
# main/game-mode-dispatch module (src/boot.c, vram 0x80010000-0x800123F0) was compiled at
+1
View File
@@ -676,6 +676,7 @@ Every script under `tools/` (plus the two report make-targets), grouped by purpo
| | `tools/make_snd_used.py` | **(Phase 8)** Build the combined libspu+libsnd curated dir (alias dedup by byte-match, scattered-`.bss` exclusions; §9.6). |
| | `tools/make_apicard_used.py` | **(Phase 8)** Build the combined libapi+libcard curated dir (§9.6). |
| | `tools/ld_interleave.py` | Interleave linker inputs to match original section ordering. |
| | `tools/jtbl_rodata_pads.py` | **(Phase 29, cookbook §8e)** Post-maspsx filter for multi-table `.rodata` carve spans: REPLACES each cc1 `.align 3` (which is section-relative and would mis-pad a merged/4-mod-8 span) with the ORIGINAL's exact pad bytes per the object's `JTBL_PADS` spec (written by `jtbl_carve` into `config/overlays.mk`; armed via `$(if $(JTBL_PADS),…)` in the Makefile `build/src/%.o` recipe). Fail-loud on table-count drift / non-`.align 3` / non-jtbl rodata content. Unset var ⇒ pipeline byte-identical. |
| | `tools/split_src_region.py` | Split a `src/` region file at object boundaries. |
| | `tools/rollout_whale_o0.py` | **(Phase 24 W9)** Roll out the -O0 whale `func_80144B9C` ×134: per single-file overlay, line-split `<ov>.c` at the whale, carve the yaml code subseg into before/`_o0b`(-O0)/`_after`, write a thin `<ov>_o0b.c` that `#include`s the shared `src/shared/func_80144B9C.h`. Idempotent; the `WHALE_O0B_OBJS` Makefile wildcard -O0-compiles all `_o0b.o` (cookbook §38). |
| **Reports** | `tools/progress.py` | Per-binary decomp progress (`make report`); counts dedup-shared fns as REAL via the registry (Phase 11). **`--fleet`** (Phase 15) aggregates all 136 binaries → `docs/progress.fleet.md` (deterministic, source-derived). **`--weighted`** (Phase 25) prints the two BYTE/instruction-weighted metrics from `.run/sig.*.jsonl` (executable code only): **instr-weighted** (fleet per-overlay, the decomp.dev-display number) + **distinct-code** (dedup, each unique fn once, the distinct-RE number); both also fold into `--fleet` alongside the ×134-inflated function-count %. Needs `make sig-overlays` first; degrades gracefully without sigs. |
+39
View File
@@ -1353,3 +1353,42 @@ are self-contained") is false whenever `engine_core.h` already declares the memb
signature. `--fix-def-sig` should likely be default-on for the h_seq path. And the meta-lesson, hammered
three times in one task: **a masked/intermediate MATCH is a candidate, never a diagnosis — reproduce the
real build and read the real error before naming the cause (R35).**
### 2026-07-18 — P29 jtbl 8-align wall: the half-pin was INVERTED (vacuous probes), the fix is a pad-spec filter (§8e)
**Context.** The 4 jtbl giants (`func_80131340`/`80159C84`/`8013C414`/`8013F350`, all match_one MATCH,
~2.6M agent-tokens of preserved drafts) were blocked on ONE tooling gap: banking `func_80131340` into the
shared `_jr_8012ACE0` TU produced a +4 pad at rodata 0xCC → image-wide %lo shift → SHA1 fail. The session-2
checkpoint recorded a half-pin — "cc1 AND maspsx both emit the jtbl `.align 2`; the +4 is a downstream
`as`/`ld_interleave` artifact" — and told the next session to start from there.
**What the evidence actually said (R35, again).** Both preserved probes were VACUOUS: an empty `j $31`
function with NO jump table — the `.align 2` they "showed" was the function-entry `.text` align. The honest
stage-walk (real draft spliced into the real TU, `.run/probe_jtbl/`) inverted every clause: cc1 emits
**`.align 3` before every table**; **maspsx passes it through verbatim** (the famous `maspsx.py:435` "drops
`.align`" is an inventory-only pass — the §8a-pad cookbook claim was false too); `as` bakes the pad
section-relative; and the LINK side was never guilty (`SUBALIGN(2)` + `ALIGN(.,4)` place even 4-mod-8 carve
starts tight — the banked `0xb07dc` carve proves it). Two Explore subagents produced OPPOSITE readings of
maspsx (one read the inventory pass as the output path); the tie was broken by reading the code path myself
plus one byte observable — the clean object's `.rodata` sh_addralign=8, which only a surviving `.align 3`
explains (R34: make oracles argue; R14: settle on bytes).
**The design fork and why the filter won.** The obvious fixes all fail a generality test: blanket align-demote
breaks the main EXE's island (its intra-TU pads are load-bearing); pure isolation fails multi-table functions
whose first table sits at vram ≡4 mod 8 (`.align` is section-relative, so the section-start parity flips every
internal pad); sed/as/ld have no per-occurrence mechanism. The winning shape: **replace each rodata `.align 3`
with the ORIGINAL's exact pad bytes** — derived per span by interval arithmetic from the carve config
(`pad[K] = start[K] − end[K−1]` ∈ {0,4}), emitted as a per-object `JTBL_PADS` make var, applied by a ~50-line
post-maspsx filter with fail-loud drift guards. Parity-independent, per-sibling self-adapting (each overlay's
own addresses), and structurally fleet-neutral (every pre-existing carve is single-table → no var → pipeline
byte-identical). A red-team subagent pre-verified the transform empirically (verbatim 0xE4/pad-at-0xCC vs
filtered 0xE0/tight) and surfaced 7 hardening items, including the LATENT bug that produced the original
failure (tight abutment silently merged into a bytes-impossible span) and a byte-witnessed wrong-TU splice
(`stub_file` first-match returned a stale duplicate stub — the "conflicting types" cascade was never the
draft's fault).
**Hindsight better-path.** The checkpoint's half-pin cost nothing this time because R35 forced re-derivation —
but only because the vacuous probes were LOOKED AT. The transferable rule: **a probe whose output contains no
instance of the probed thing pins nothing** — check that first, before trusting any recorded verdict. And when
a wall involves a multi-stage pipeline, walk it stage-by-stage with one byte observable per stage before
designing anything; the whole design fell out of five observables in under an hour.
+52 -1
View File
@@ -353,7 +353,8 @@ a 25-ins single-jtbl jr-function in ov_SC01_077):**
orthogonal — reconcile first, then the carved jtbl lands byte-exact.
- **Alignment:** gcc emits the jtbl `.rdata .align 3` (8-byte). If the original jtbl address is 8-aligned
(`jtbl_801D8078`, 0x…078) there is no pad and it lands exact. A **4-aligned** original address (`jtbl_801D8AFC`)
would force a 4-byte align pad → handle then (not hit by the PoC target).
would force a 4-byte align pad → **HANDLED (Phase 29): the §8e `JTBL_PADS` pad-spec filter** (hit for real by
`jtbl_801D8144` in the func_80131340 bank).
- **rtu_match is NOT a whole-binary gate for jr-functions** — it masks relocs AND excludes the §8 jtbl rodata, so
it MATCHes a body whose switch is subtly wrong (e.g. func_80159C84's 2nd jtbl was 5 words vs the real 6 — a
false-MATCH). Always confirm jr-function cracks with the whole-binary gate (which now works, via this carve).
@@ -364,6 +365,11 @@ a 25-ins single-jtbl jr-function in ov_SC01_077):**
### §8a-pad — a trailing `.word 0x00000000` under a jtbl dlabel is `.align` PAD, not an entry (Phase 26 session 6, byte-proven)
> **⚠ CORRECTED by §8e (Phase 29):** the "maspsx drops all `.align`" rationale below is **FALSE** (that
> `continue` is in an inventory-only pass; the output path passes `.align` verbatim). The TRIM itself remains
> correct — the pad belongs to the NEXT table's `.align 3`, absent when that owner isn't compiled in the same
> object. Multi-table spans now reproduce interior pads via the §8e `JTBL_PADS` spec filter.
**This retroactively explains the §8a `func_80159C84` "5 words vs the real 6" false-MATCH.**
The raw `dlabel jtbl_XXXXXXXX` in `asm/<ov>/data/*.data.s` can span one word MORE than the switch has
@@ -507,6 +513,51 @@ the base for the later recovery stages. **First sibling byte-identical; 562-ins
finds only make's `Error 33`. Grep for the diagnostic text (`conflicting types`, `undeclared`, `parse error`,
`redefinition`) instead, and remember `warning: conflicting types for built-in function 'memcpy'` is benign.
## §8e The jtbl ALIGNMENT LAW + the pad-spec filter — multi-table .rodata spans (Phase 29, byte-proven; `.run/probe_jtbl/verdict.md`)
**Two prior cookbook claims are CORRECTED here** (both were instrument errors, R35):
- §8a-pad's "**maspsx drops all `.align`**" is **FALSE**. The `continue` at `maspsx/__init__.py:435` is in
`preprocess_lines` — an *inventory-only* pass (sbss/bss/sdata dicts) whose skips produce no output; the real
output path (`process_line`, L872-873 catch-all) re-emits `.align` **verbatim**. The §8a-pad *trim* is still
right, for a different reason: the trailing pad word belongs to the NEXT table's `.align 3`, which is only
emitted when that next owner is compiled in the same object.
- The Phase-29 session-2 "half-pin" ("cc1 AND maspsx emit the jtbl `.align 2`") was **inverted**, measured off
VACUOUS probes (an empty `j $31` fn — no jtbl in them at all; the only `.align 2` was the function-entry
`.text` align). Lesson: **a probe whose output contains no instance of the thing being probed pins nothing.**
**The law (each link byte-verified in `.run/probe_jtbl/`):**
1. cc1 (Sony gcc 2.7.2) emits `.rdata` + **`.align 3`** + label before **every** switch jump table
(probe: 2 tables in one TU → 2× `.align 3`; the clean single-table object's `.rodata` sh_addralign=8).
2. maspsx passes `.align` through; `as` bakes the pad into the section **SECTION-RELATIVE** — the linker can
never remove an intra-object pad. (`as` control: bare `.word` section → Al=4; with `.align 3` → Al=8.)
3. Link placement is always TIGHT: `SUBALIGN(2)` (splat `subalign: 2`, fleet-wide) + `ld_interleave`'s
`. = ALIGN(., 4)` override input-section alignment — a 4-mod-8 carve start places exactly (the banked
`0xb07dc` carve is the byte proof). So **only intra-object pads can diverge from the original.**
4. ORIGINAL layout semantics: originally-separate TUs pack **TIGHT** (PSX linker, 4-aligned placement:
jtbl_801D8078's 51 entries end 0x801D8144 exactly where the next TU's table begins, %8==4); **intra-TU**
consecutive tables carry a REAL zero-word pad wherever the previous table ends ≡4 mod 8
(tail2.data.s: jtbl_801D8158/8170/8188/81A0 = 5 entries + one `.word 0` each).
5. ⇒ Merging originally-separate TUs into one decomp TU makes cc1's intra-TU `.align 3` fire where the
original had a tight TU boundary: a non-first table at original vram ≡4 mod 8 gains a **+4 interior pad**
→ every downstream data symbol shifts → `%lo` relocs break image-wide (func_80131340: pad at rodata 0xCC,
SHA1 fail from a clean build). Conversely a genuine intra-TU pad must be REPRODUCED.
6. **Isolation does NOT fix this in general**: `.align` is section-relative, so an isolated object whose
first table starts at vram ≡4 mod 8 flips the parity of every INTERNAL align — a multi-table function
with a 4-mod-8 first table would mis-pad inside its own object. Only explicit pad control is general.
**The mechanism (`tools/jtbl_rodata_pads.py` + `jtbl_carve` + Makefile `JTBL_PADS`):** for a multi-table
span, `jtbl_carve` derives each boundary's pad by **interval arithmetic** (`pad[K] = start[K] − end[K−1]`
∈ {0,4}; a 4-gap must be a verifiably-zero payload word, else NON-CONTIGUOUS→isolate) and writes a
per-object `build/src/<ov>/<sub>.o: JTBL_PADS := 0,4,…` target var into `config/overlays.mk`; the Makefile
pipes that object through the filter, which **REPLACES each rodata `.align` with the spec'd pad bytes**
(`.word 0` or nothing). Per-sibling self-adapting (each overlay's own addresses), fail-loud on drift
(spec count ≠ align count; non-`.align 3`; non-jtbl rodata content). Committed spec values are CARRIED,
never re-derived (a committed span's interior boundaries are unrecoverable from its interval).
Single-table carves get no var — their pipeline is byte-identical to pre-§8e (their lone `.align 3` at
section offset 0 pads nothing and SUBALIGN neutralizes the sh_addralign).
- **Object-layer proof before image-layer claims:** verbatim vs filtered objdump — 0xE4/pad-at-0xCC vs
0xE0/tight — settled the mechanism before any carve landed. Cheap, decisive, reusable probe shape.
## §9 Link real PsyQ library objects byte-exact (Phase 7 — GO proven)
~350 of BFM's functions are unmodified PsyQ 4.0 SDK code. They are **byte-identical to the real PsyQ library
objects**, so link them directly instead of hand-decompiling — and each library `.o` brings its own correct
+34
View File
@@ -144,6 +144,40 @@ conditional) · main-EXE/B9 + GLM/B6 + resident's 14 walls (P30) · behemoths B7
## Per-task log
*(appended after each task; the crash-recovery trail — becomes `phase-ends/logs/Phase29.md` at close, R19)*
- **2026-07-18 — jtbl 8-align fix OPENED (Fable5 Max, plan approved).** Plan mirror:
`~/.claude/plans/fable5-max-set-jtbl-ancient-marble.md`. **⚠️ CORRECTION of the session-2 checkpoint's
half-pin (R35/R14 — do NOT re-trust it): the claim "cc1 AND maspsx both emit the jtbl `.align 2`" is
INVERTED, and both preserved probes (`.run/probe_jtbl_{cc1,maspsx}.s`) are VACUOUS (empty `j $31` fn, no
jtbl).** True chain (byte-grounded this session): **(1)** Sony cc1 emits `.align 3` before EACH jump table
in `.rdata` (clean `jr_8012ACE0.o` has `.rodata` sh_addralign=8 w/ a single 51-entry table); **(2)** maspsx
passes `.align` through VERBATIM (`maspsx/__init__.py:872-873` output catch-all; the L435-437 `continue` is
an inventory-only pass — `jtbl_carve.py:132`'s "maspsx drops .align" docstring is FALSE); **(3)** `as` bakes
the pad SECTION-RELATIVE (intra-object, linker can't remove); **(4)** placement is always tight
(`SUBALIGN(2)` fleet-wide + ld_interleave `ALIGN(.,4)` — the 0xb07dc 4-mod-8 carve proves it); **(5)**
original semantics: separate TUs pack TIGHT (078→144 abutment), intra-TU tables 8-align w/ real zero-word
pads (tail2.data.s 8158/8170/8188/81A0). **THE BUG:** merging originally-separate TUs into one decomp TU
fires an intra-TU `.align 3` where the original had a tight TU boundary → +4 at rodata 0xCC → image-wide
%lo shift (build_g2.log SHA1 FAIL). **THE FIX (red-team-hardened, empirically pre-verified):** pad-spec
filter `tools/jtbl_rodata_pads.py` (post-maspsx, per-object `JTBL_PADS` make var, fail-loud guards) +
`jtbl_carve` interval-arithmetic specs + merge relaxation (gap 4 must be verifiably zero) + revert/stale-.o
coverage + `jtbl_family_bank` stub_file uniqueness assert (yesterday's ladder failure spliced into a stale
`_a.c` duplicate stub — byte-witnessed in bank_func_80131340.log). Isolation is NOT general (as aligns
section-relative; a 4-mod-8-start multi-table object mis-pads internally) — kept as NON-CONTIGUOUS fallback.
- **✅ 2026-07-18 — jtbl §8e fix BUILT + fleet-neutral (Steps 1-3 of the approved plan).** Honest probes
persisted (`.run/probe_jtbl/verdict.md` + objdumps; the vacuous pair deleted): cc1 `.align 3` per table
(2 tables → 2 aligns, byte-listed), maspsx verbatim passthrough in situ, **the +4 pad materialized at object
rodata 0xCC** (0xE4 verbatim), as-controls (bare rodata → Al=4; +`.align 3` → Al=8). Built: NEW
`tools/jtbl_rodata_pads.py` (spec-driven align→pad-bytes replacement; guards byte-tested rc=1 ×3; pad=4
direction synthetically proven), `jtbl_carve.py` (spec-aware merge: gap∈{0,4-zero-checked-in-payload},
interval-arithmetic pads CARRIED never re-derived, `JTBL_PADS` target-var emission into overlays.mk +
revert() restore + stale-.o invalidation, honest docstring), Makefile `$(if $(JTBL_PADS),…)` stage +
env-shield default, `jtbl_family_bank.stub_file` duplicate-stub fail-loud. **Object-layer byte proof:**
filtered `.rodata` = 0xE0, table 2 TIGHT at 0xCC = the merged carve span exactly. Dry-run build_carve →
span `0xaff20..0xb0000` spec `[0,0]` as designed. **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.**
Flywheel captured in-session (R30): cookbook **§8e** + §8a/§8a-pad corrections, decision-log entry,
SETUP.md row. NEXT: Step 4 — bank func_80131340 ×1.
- **2026-07-16 — Phase opened.** Session Start Protocol complete (all 28 PhaseEnds + roadmap +
calibration + decision-log read). 3 Explore surveys grounded the plan (tooling state, frontier
data, idiom bank). Plan approved at Max/plan-mode. Beginning Task 1.
+157 -27
View File
@@ -21,6 +21,18 @@ code object contributes at most ONE contiguous .rodata run, so two matched jr-fu
code subseg (non-adjacent jtbls in the island) are UNSATISFIABLE -> this tool fails loud, and the
caller must first isolate one into its own code subseg (the whale `_o0b` precedent).
MULTI-TABLE spans & the 8-align pad spec (Phase-29 §8e; .run/probe_jtbl/verdict.md): cc1 emits
`.align 3` before EVERY jump table and maspsx passes it through, so a merged same-subseg span whose
non-first table sits at an original vram ≡4 mod 8 would gain a +4 interior pad the original does
not have (originally-separate TUs pack TIGHT) — and conversely a real intra-TU pad word must be
reproduced where the original HAS one. When a span holds >1 table, this tool derives the per-table
pad spec by interval arithmetic (pad[K] = start[K] - end[K-1] ∈ {0,4}; a 4-gap must be a zero word
in the payload) and writes a per-object `JTBL_PADS` target var into config/overlays.mk; the
Makefile then pipes that object through tools/jtbl_rodata_pads.py, which REPLACES each rodata
`.align` with the spec'd pad bytes. Committed spec values are carried, never re-derived (a
committed span's interior boundaries are not recoverable from its interval). Single-table carves
get NO var and keep today's byte-identical pipeline.
Usage: jtbl_carve.py <ov> --func func_XXXX [--func ...] # add these matched jr-fns to the carve set
jtbl_carve.py <ov> --revert # restore the config from git (drop carves)
Idempotent: re-running with the same (accumulated) funcs reproduces the same config.
@@ -51,6 +63,46 @@ def overlay_vram_base(ov):
return int(m.group(1), 16)
def payload_path(ov):
"""The overlay's decompressed payload, derived from the config's target_path (R33)."""
m = re.search(r"target_path:\s*(\S+)", open(cfg_path(ov)).read())
if not m:
sys.exit(f"jtbl_carve: no target_path in {cfg_path(ov)}")
p = os.path.join(REPO, m.group(1))
if not os.path.exists(p):
sys.exit(f"jtbl_carve: payload {p} missing — run `make extract` first (R32: refusing to "
f"skip the gap-word check)")
return p
def payload_word(ov, off):
"""The little-endian u32 at file offset `off` in the overlay's payload."""
with open(payload_path(ov), "rb") as f:
f.seek(off)
b = f.read(4)
if len(b) != 4:
sys.exit(f"jtbl_carve: short read at payload offset 0x{off:x}")
return int.from_bytes(b, "little")
# Per-object pad-spec lines in config/overlays.mk (Phase-29 §8e — consumed by the Makefile's
# jtbl_rodata_pads.py stage). One line per multi-table span:
# build/src/<ov>/<sub>.o: JTBL_PADS := 0,4 # Phase-29 §8e jtbl pad spec (jtbl_carve.py)
PADS_COMMENT = " # Phase-29 §8e jtbl pad spec (jtbl_carve.py)"
def pads_line_re(ov):
return rf"^build/src/{re.escape(ov)}/(\w+)\.o: JTBL_PADS := ([\d,]+).*$"
def current_pads_specs(ov, txt=None):
"""{subseg: [pad,...]} from the CURRENT overlays.mk (carries committed + in-flight values)."""
if txt is None:
txt = open(os.path.join(REPO, "config/overlays.mk")).read()
return {m.group(1): [int(x) for x in m.group(2).split(",")]
for m in re.finditer(pads_line_re(ov), txt, re.M)}
def code_pieces(ov):
"""[(vram, subseg)] for every `- [off, c, name]` code piece in the config, ascending."""
base = overlay_vram_base(ov)
@@ -129,12 +181,16 @@ def jtbl_range(ov, jtbl_hex, labels, region_end_vram):
not a jump target, and the function's `sltiu <n>` range check names the true entry count
(byte-confirmed: `func_8015AE2C` → `sltiu 0x7` = 7 entries, yet the raw dlabel spans 8 words).
This matters because **maspsx drops `.align`**, so a C-emitted jump table can never reproduce the
pad. Carving to the next dlabel would reserve 8 words while the compiled object supplies only 7 —
under-filling the `.rodata` piece by 4 bytes and shifting every later symbol (the same +4 image
corruption class as §41d). Trimming leaves the pad where it belongs: in the raw post-carve data
piece. This also retroactively explains the §8a `func_80159C84` "5 words vs the real 6"
false-MATCH."""
Why trim (CORRECTED Phase-29 §8e — the old rationale "maspsx drops `.align`" was FALSE; maspsx
passes it through, .run/probe_jtbl/verdict.md): the pad belongs to the NEXT table's `.align 3`,
which is emitted only if that next table's owner is compiled in the SAME object. When the next
owner is unmatched (or another TU), the carved object ends at the last real entry, so carving
to the next dlabel would reserve the pad word the object does not supply — under-filling the
`.rodata` piece by 4 bytes and shifting every later symbol (the same +4 image corruption class
as §41d). Trimming leaves the pad where it belongs: in the raw post-carve data piece — and when
the next owner IS matched into the same span later, the merge re-attributes the pad to that
table's JTBL_PADS spec (pad[K]=4) and jtbl_rodata_pads.py emits it. This also retroactively
explains the §8a `func_80159C84` "5 words vs the real 6" false-MATCH."""
start = int(jtbl_hex, 16)
if start not in labels:
sys.exit(f"jtbl_carve: jtbl_{jtbl_hex} not found in the raw data asm "
@@ -208,14 +264,19 @@ def parse_config(ov):
def build_carve(ov, funcs):
"""Return (region_lines, order_arg): the regenerated data-region `- [...]` piece lines and the
`ld_interleave --order` object list, for the accumulated carve set (existing + the new funcs)."""
"""Return (region_lines, order_arg, pads_map): the regenerated data-region `- [...]` piece
lines, the `ld_interleave --order` object list, and {subseg: [pad,...]} for every carve span,
for the accumulated carve set (existing + the new funcs)."""
base = overlay_vram_base(ov)
(_, indent, _, _, tail_start, region_end, trailing_present, existing) = parse_config(ov)
region_end_vram = base + region_end
# carves: (start_off, end_off, subseg). Existing ones come from the config (already migrated).
carves = list(existing)
# carves: (start_off, end_off, subseg, spec). Existing ones come from the config (already
# migrated); their pad spec is CARRIED from overlays.mk (a committed span's interior boundaries
# are not recoverable from its interval — never re-derive), default [0] = single-table (the
# fleet-wide state before Phase-29 §8e; a multi-table span always writes its spec line).
specs = current_pads_specs(ov)
carves = [(s, e, sub, specs.get(sub, [0])) for (s, e, sub) in existing]
have = {c[0] for c in carves}
# A new jtbl's end is bounded by the next RAW data dlabel OR the next EXISTING carve start
# (an already-carved adjacent jtbl is gone from the data asm, so the raw dlabels alone would
@@ -230,28 +291,42 @@ def build_carve(ov, funcs):
s_off, e_off = s_vram - base, e_vram - base
if s_off in have:
continue # idempotent: already carved
carves.append((s_off, e_off, sub))
carves.append((s_off, e_off, sub, [0]))
have.add(s_off)
carves.sort()
carves.sort(key=lambda c: (c[0], c[1]))
# A code object emits its jtbls CONTIGUOUS in .rodata (gcc source order). So two carves in the
# SAME subseg are byte-correct only if ADJACENT in the island (no unmatched jtbl between) -> merge
# them into one spanning .rodata piece. NON-adjacent same-subseg is unsatisfiable (a single object
# can't leave a gap for the raw jtbl between) -> isolate one fn into its own subseg (jr_isolate.py).
# SAME subseg are byte-correct only if ADJACENT in the island — where "adjacent" is abutting
# (gap 0) OR separated by exactly one original `.align 3` pad word (gap 4, verifiably zero in
# the payload; Phase-29 §8e) -> merge them into one spanning .rodata piece, recording the
# boundary pad in the span's spec (jtbl_rodata_pads.py reproduces it at compile time).
# Any other same-subseg gap is unsatisfiable (a single object can't leave a hole for the raw
# jtbl between) -> isolate one fn into its own subseg (jr_isolate_all.py).
merged = []
for s_off, e_off, sub in carves:
if merged and merged[-1][2] == sub and merged[-1][1] == s_off:
merged[-1] = (merged[-1][0], e_off, sub) # extend the contiguous same-subseg run
else:
merged.append((s_off, e_off, sub))
for s_off, e_off, sub, spec in carves:
if merged and merged[-1][2] == sub:
gap = s_off - merged[-1][1]
if gap in (0, 4):
if gap == 4:
w = payload_word(ov, merged[-1][1])
if w != 0:
sys.exit(
f"jtbl_carve: the 4-byte gap at 0x{merged[-1][1]:x} between same-subseg "
f"carves is 0x{w:08x}, not a zero .align pad word — treating as "
f"NON-CONTIGUOUS. Isolate one matched jr-function into its own code "
f"subseg first (tools/jr_isolate_all.py), then re-carve.")
ps, _, _, pspec = merged[-1]
merged[-1] = (ps, e_off, sub, pspec + [gap] + spec[1:])
continue
merged.append((s_off, e_off, sub, spec))
seen_subsegs = {}
for s_off, _, sub in merged:
for s_off, _, sub, _ in merged:
if sub in seen_subsegs:
sys.exit(
f"jtbl_carve: subseg '{sub}' would host NON-CONTIGUOUS .rodata carves "
f"(0x{seen_subsegs[sub]:x} and 0x{s_off:x}) — a single object can't leave a gap for the "
f"unmatched jtbl between them. Isolate one matched jr-function into its own code subseg "
f"first (tools/jr_isolate.py, the whale `_o0b` precedent), then re-carve.")
f"first (tools/jr_isolate_all.py, the whale `_o0b` precedent), then re-carve.")
seen_subsegs[sub] = s_off
carves = merged
@@ -264,7 +339,7 @@ def build_carve(ov, funcs):
nonlocal n_data
n_data += 1
return "tail" if n_data == 1 else f"tail{n_data}"
for s_off, e_off, sub in carves:
for s_off, e_off, sub, _spec in carves:
if cursor < s_off:
nm = data_name()
pieces.append((cursor, "data", nm))
@@ -283,16 +358,20 @@ def build_carve(ov, funcs):
for off, kind, name in pieces:
comment = " # Phase-26 §8 jtbl-rodata carve (jtbl_carve.py)" if kind == ".rodata" else ""
region_lines.append(f"{indent}- [{hex(off)}, {kind}, {name}]{comment}")
return region_lines, "--order " + ",".join(order)
pads_map = {sub: spec for (_s, _e, sub, spec) in carves}
return region_lines, "--order " + ",".join(order), pads_map
def apply(ov, funcs):
region_lines, order_arg = build_carve(ov, funcs)
region_lines, order_arg, pads_map = build_carve(ov, funcs)
lines, indent, lo, hi, *_ = parse_config(ov)
new_lines = lines[:lo] + region_lines + lines[hi:]
open(cfg_path(ov), "w").write("\n".join(new_lines) + "\n")
set_overlays_var(ov, order_arg)
print(f"jtbl_carve {ov}: carve set = {len(region_lines)} pieces; JTBL_INTERLEAVE = {order_arg}")
set_pads_vars(ov, pads_map)
multi = {s: p for s, p in pads_map.items() if len(p) > 1}
print(f"jtbl_carve {ov}: carve set = {len(region_lines)} pieces; JTBL_INTERLEAVE = {order_arg}"
+ (f"; JTBL_PADS = {multi}" if multi else ""))
def set_overlays_var(ov, args):
@@ -310,6 +389,38 @@ def set_overlays_var(ov, args):
open(mk, "w").write(txt)
def set_pads_vars(ov, pads_map):
"""Write this overlay's per-object JTBL_PADS lines (Phase-29 §8e), preserving carried values.
Only multi-table spans (len(spec) > 1) get a line; single-table spans get none (their pipeline
stays byte-identical to pre-§8e). All of this overlay's current pads lines are replaced by the
regenerated block as one unit (values were CARRIED into pads_map by current_pads_specs, so this
is a rewrite of the same state plus the new boundary — not a re-derivation). Any object whose
spec appears, changes, or disappears gets its stale build/src/<ov>/<sub>.o deleted: the spec is
no make-prerequisite, and a padless stale object would fail the SHA gate mystifyingly."""
mk = os.path.join(REPO, "config/overlays.mk")
txt = open(mk).read()
before = current_pads_specs(ov, txt)
after = {sub: spec for sub, spec in pads_map.items() if len(spec) > 1}
# drop all current lines for this overlay, then insert the regenerated block
txt = re.sub(pads_line_re(ov) + r"\n", "", txt, flags=re.M)
if after:
block = "\n".join(
f"build/src/{ov}/{sub}.o: JTBL_PADS := {','.join(map(str, spec))}{PADS_COMMENT}"
for sub, spec in sorted(after.items()))
m = re.search(rf"^{re.escape(ov)}_JTBL_INTERLEAVE.*$", txt, re.M)
if not m:
sys.exit(f"jtbl_carve: no {ov}_JTBL_INTERLEAVE line to anchor JTBL_PADS on")
txt = txt[:m.end()] + "\n" + block + txt[m.end():]
open(mk, "w").write(txt)
for sub in set(before) | set(after):
if before.get(sub) != after.get(sub):
obj = os.path.join(REPO, f"build/src/{ov}/{sub}.o")
if os.path.exists(obj):
os.remove(obj)
print(f"jtbl_carve: JTBL_PADS changed for {sub} — removed stale {obj}")
def revert(ov):
"""Restore this overlay's carve state to the COMMITTED one.
@@ -335,9 +446,28 @@ def revert(ov):
txt = txt.replace(anchor, anchor + "\n" + m.group(0), 1)
else: # no committed carve -> drop ours
txt = re.sub(rf"^{re.escape(ov)}_JTBL_INTERLEAVE.*\n", "", txt, flags=re.M)
# JTBL_PADS lines (Phase-29 §8e): restore this overlay's per-object pad specs to the committed
# set with the same surgical splice (a failed sibling bank must not leave its spec behind, and
# a blunt checkout would wipe OTHER siblings' in-flight lines — overlays.mk is shared).
now_pads = current_pads_specs(ov, txt)
committed_pads = current_pads_specs(ov, committed)
txt = re.sub(pads_line_re(ov) + r"\n", "", txt, flags=re.M)
committed_lines = [l for l in committed.splitlines()
if re.match(pads_line_re(ov), l)]
if committed_lines:
m2 = re.search(rf"^{re.escape(ov)}_JTBL_INTERLEAVE.*$", txt, re.M)
if not m2:
sys.exit(f"jtbl_carve: no {ov}_JTBL_INTERLEAVE line to anchor committed JTBL_PADS on")
txt = txt[:m2.end()] + "\n" + "\n".join(committed_lines) + txt[m2.end():]
open(mk, "w").write(txt)
for sub in set(now_pads) | set(committed_pads):
if now_pads.get(sub) != committed_pads.get(sub):
obj = os.path.join(REPO, f"build/src/{ov}/{sub}.o")
if os.path.exists(obj):
os.remove(obj)
print(f"jtbl_carve {ov}: reverted config + JTBL_INTERLEAVE restored to committed"
f"{'' if m else ' (none)'}")
f"{'' if m else ' (none)'}"
+ (f" + {len(committed_lines)} JTBL_PADS line(s) restored" if committed_lines else ""))
def main():
+11 -4
View File
@@ -38,10 +38,17 @@ def sh(cmd):
def stub_file(ov, func):
for cf in sorted(glob.glob(f"src/{ov}/{ov}*.c")):
if re.search(rf'INCLUDE_ASM\("[^"]*",\s*{func}\);', open(cf).read()):
return cf
return None
"""The ONE .c holding this func's INCLUDE_ASM stub. Fail-loud on duplicates (Phase-29 §8e):
the first-sorted-glob behavior once returned a STALE duplicate stub in a different TU
(func_80131340 spliced into ov_SC01_077_a.c instead of ..._jr_8012ACE0.c), producing a
misattributed `conflicting types` cascade — a wrong-TU splice, not a draft defect."""
hits = [cf for cf in sorted(glob.glob(f"src/{ov}/{ov}*.c"))
if re.search(rf'INCLUDE_ASM\("[^"]*",\s*{func}\);', open(cf).read())]
if len(hits) > 1:
sys.exit(f"jtbl_family_bank: {func} has {len(hits)} INCLUDE_ASM stubs in {ov}: {hits} — "
f"duplicate/stale stub; fix the source before banking (a first-match splice "
f"would hit the wrong TU's decl environment)")
return hits[0] if hits else None
def region_files(ov):
+104
View File
@@ -0,0 +1,104 @@
#!/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()