3.9 KiB
§188 — 🔴 THE jr $ra + addiu $sp TAIL IS AN ASSEMBLER ARTIFACT, NOT A FRAME SHAPE
(P31 S53 — found twice independently, from 800c3 and from 800c2; corrects §177 row 2, answers §182)
§182 asked what else forces those 800c3 frames. The answer is: nothing does — it is not gcc.
LAW 1 — cc1 cannot produce it, so no C lever reaches it. In mips.c, function_epilogue sets
noreorder = (epilogue_delay != 0) (:5081). Only that noreorder branch emits j $31 before the
stack restore (:5204, addu at :5209/:5214); the reorder branch emits addu $sp first and j $31
second (:5225-5238) — the A-form. epilogue_delay is non-empty only when
mips_epilogue_delay_slots() returns 1 (frame empty, or mask == RA_MASK && fmask == 0, :5378), and
on that same branch load_only_r31 is the identical predicate (:5174), so exactly one lw $31 is
emitted. Therefore: a tail with jr $ra + addiu $sp and two or more restores above it is
unreachable from cc1 for any C body whatsoever. §177's row 2 ("frame saves $s regs → keep a value
live across a call") is inverted for this shape: saving an $s register is what makes it impossible.
LAW 2 — two conditions, both necessary, measured as a 2×2. The B-form comes from GNU as filling
the return delay slot, and it needs an empty slot to fill:
- maspsx appends
nop # DEBUG: branch/jumpinto any emptyj $31slot, and force-emits.set\tnoreorderafter every.ent(tools/maspsx/maspsx/__init__.py:856-859), while its.set\tbranch (:844-848) updatesis_reorderbut never re-emits — so gcc's own.set reorderis swallowed. A TAB-formed.set\tnoreorder+ SPACE-formed.set reorderpair emitted just before the epilogue suppresses the padding (maspsx consumes the TAB form, passes the SPACE form toas). as -O2performs the swap;as -O1— which this project pins (Makefile:563,tools/match_one.py:62) — only pads withnop.
Measured on func_8005E3AC from ONE md5-identical .asm: marker + -O1 = 54 ins / 2 diffs · no marker
-O1= 54 / 2 · no marker +-O2= 54 / 2 (inert — noreorder wins) · marker +-O2= 53 / 0. So "as -O2flips it" is only half the cause; the marker is load-bearing, and it is byte-inert at-O1.
THE DIAGNOSTIC — tools/oracle_reorder.py (promoted out of gitignored scratch for exactly this
reason). Re-assemble the same draft bypassing maspsx with as -O2 and compare. Measured 2×2 on
func_80061FA8 (target 103 ins): maspsx+-O1 57 diffs/106 ins · maspsx+-O2 57/106 (inert) ·
bypass+-O1 97/107 · bypass+-O2 0 diffs/103 ins = MATCH.
If the bypass+-O2 cell is 0, the draft's C is already correct: file IMMOVABLE and stop grinding.
And do not scope this to epilogues — func_80061FA8's 57-diff cascade is unfilled beqz/j/jal slots
body-wide, including an la macro's %lo half.
DO NOT "FIX" IT GLOBALLY. as -O2 is byte-inert build-wide today, but maspsx's forced noreorder
does not cover INCLUDE_ASM'd hand asm — that content never passes through maspsx — so flipping the
flag changes the risk surface for every remaining stub. Treat -O2 as a diagnostic, not a build change.
LAW 3 — and 6 of the affected functions are SDK objects, found with the STRICT oracle. Running
psyq_identify.py (the field-masked oracle, §187) over the band first identifies: libpad
pdent3.o@0x8005D244, pdent4.o@0x8005D33C, pdent5.o@0x8005D410 (PadInfoComb),
pdmain1.o@0x8005D588, pdmaiini.o@0x8005D8B4, and libapi first.o@0x80061FA8. Those are
decomp work only by mistake — route them to psyq_integrate.py. The remaining ~24 in the band are
game-authored C whose bodies can be byte-exact today and are blocked only by Law 2.
(Unlike §187's refuted libgs claim, this identification comes FROM the strict oracle rather than being
checked by it — which is the whole difference.)