4.1 KiB
§34 — The func_80138ED0 giant crack: gcc-2.7.2's 3-qty sort bug + the zero-byte asm allocation toolkit + the giv-init fence (Phase 24 T5; Opus→close=21, Fable5→MATCH ×134)
The 2nd region-a giant (159 ins, bit-unpack/tilemap; 2 giant-local data bases, callees func_8013914C/func_800599B8). Opus applying §32 reached close=21 (all semantics/control-flow/constants exact); Fable5 reading the vanilla gcc-2.7.2 source cracked the pure regalloc/schedule residual — every class C-reachable, no permuter. Each lever byte-verified via match_one + gdb-on-cc1. The banking then took the standard pipeline (cast_call_sites reconciled func_8013914C (u8*,u16*)→canonical (s32,s32)+call-site cast; reconcile_decls a NO-OP — the 2 data bases are giant-local, no fleet conflict) → ×1 → dedup_propagate --recover → ×134 byte-identical (pins/asm body propagates fine; func_800599B8's lone (s32,s32) decl is in ov_SC01_077.c, a different TU from the _a.c bank, so no per-member conflict).
THE HEADLINE — gcc-2.7.2's 3-qty local-alloc SORT BUG (local-alloc.c:1441-1463/:1494-1516). For a block with ≤3 local register quantities, the unrolled comparison switch compares fixed qty numbers (qty_compare(0,1),(1,2),(0,1)) but exchanges order slots — when pri(q1) is highest the third compare re-fires and undoes the first swap, so 3-qty blocks allocate in qty-CREATION order, not density order (≥4 qtys go through qsort, correct). Symptom: a low-density local grabs a reg it shouldn't. Fix: a zero-instruction DECOY qty (asm("":"=r"(decoy):"r"(x)); asm("" :: "r"(decoy));) bumps the block to 4 qtys → the qsort path → correct density-order first-fit (find_reg, global.c:904; regs_used_so_far pre-seeded with the call-used regs, global.c:352-355). A real compiler bug, now a reusable lever.
THE ZERO-BYTE ASM TOOLKIT (allocation/schedule dials that emit NOTHING — the byte-gate certifies the induced codegen):
- input-only dummy
asm("" :: "r"(v))— floats tov's def; a ref-count / density dial (raise a pseudo's priority / extend a live range one way). - multi-input dummy
asm("" :: "r"(a),"r"(b))— anchors at the LATEST def; a lifetime-extender / joint-release (releases a,b together in sched1's backward pass →rank_for_schedule's class rulesched.c:2385, cost-1 dep beats cost-2 load, orders the emit). - def+use pair
asm("":"=r"(d):"r"(s)); asm("" :: "r"(d))— mints a decoy qty (the 3-qty-bug fix). - giv-init fence
asm("":"=r"(ba):"0"(ba)); dst = ba;— forcesemit_iv_add_mult's giv-init MOVE (loop.c:5556 if (reg != result) emit_move_insn) =addu dst,ba,$zero; the general fix for the "gcc coalesced the giv init, dropping one instruction → full count mismatch" class on any giant with a counter-derived pointer. (Found by the Opus pass; kept.)
Statement-position / type levers (no asm): a leading pb = param_3; rides sched.c:3191-3215's "don't delay getting parameters" pin so combine folds the parm-save into it (prologue order); an explicit u32 pv = uVar1; before p = base; replaces loop.c's move_movables hoist-at-loop_start (loop.c:1652/1810) with source order; a HImode % 3 (a u16 var) defeats gcc's x%3==0 → beq (x/3)*3,x fold.
gdb-on-cc1 (the method that settled it): the shipped tools/bin/gcc-2.7.2-psx/cc1 is i386-static WITH symbols — breakpoint find_free_reg/post_mark_life (.run/t7/fable/gdbtrace.gdb) to dump the real qty order + register grants when hand-modeling stalls. Cite the vanilla tools/reference/gcc-2.7.2/ tree (now complete — global.c/local-alloc.c/reload1.c/toplev.c/function.c/flow.c/… from the FSF tarball; sched.c/loop.c/mips.c verified byte-identical to vanilla). Pass order (why upstream fixes reach the prologue): sched1 → local_alloc → global_alloc/reload → prologue threading (toplev.c:3103) → sched2 (:3117) → jump2 → dbr; prologue saves are sched2-scheduled, everything upstream tunes sched2's LUID tie-breaks.
Gotcha: parallel match_one runs on the SAME function share .run/match/<fn> — pass a unique --work dir or the scores are garbage.