Files
BFM-decomp/cookbook/C0210.md
T

3.9 KiB
Raw Blame History

§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/jump into any empty j $31 slot, and force-emits .set\tnoreorder after every .ent (tools/maspsx/maspsx/__init__.py:856-859), while its .set\t branch (:844-848) updates is_reorder but never re-emits — so gcc's own .set reorder is swallowed. A TAB-formed .set\tnoreorder + SPACE-formed .set reorder pair emitted just before the epilogue suppresses the padding (maspsx consumes the TAB form, passes the SPACE form to as).
  • as -O2 performs the swap; as -O1 — which this project pins (Makefile:563, tools/match_one.py:62) — only pads with nop.

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 -O2 flips 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.)