From 4588107cd7618e4c79e166b7d34783aa64a434a3 Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Thu, 24 Sep 2026 19:14:00 -0400 Subject: [PATCH] phase12: merge 26 (674 bodies) + the epilogue token is a per-row property invisible in a length check --- config/regions.tsv | 2 ++ phase-ends/logs/Phase12.md | 50 ++++++++++++++++++++++++++++ src/func_8001278C.c | 68 ++++++++++++++++++++++++++++++++++++++ src/func_80012834.c | 48 +++++++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 src/func_8001278C.c create mode 100644 src/func_80012834.c diff --git a/config/regions.tsv b/config/regions.tsv index 44390da..9fe4c44 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -12,8 +12,10 @@ # Matched so far: 0x80010810 0x8001084C src/func_80010810.c 0x80012780 0x8001278C src/func_80012780.c +0x8001278C 0x800127F0 src/func_8001278C.c maspsx=epilogue 0x800127F0 0x8001281C src/func_800127F0.c 0x8001281C 0x80012834 src/func_8001281C.c +0x80012834 0x8001289C src/func_80012834.c 0x8001289C 0x800128E4 src/func_8001289C.c 0x800128E4 0x80012918 src/func_800128E4.c 0x80012918 0x80012960 src/func_80012918.c diff --git a/phase-ends/logs/Phase12.md b/phase-ends/logs/Phase12.md index 8733cea..80ecfb0 100644 --- a/phase-ends/logs/Phase12.md +++ b/phase-ends/logs/Phase12.md @@ -1299,3 +1299,53 @@ Not rewriting history for a number, but recording it, because **a wrong body cou exactly the kind of figure a later session trusts without re-deriving**, and the phase's whole projection rests on this number being the one the gate produced. The authority is `config/regions.tsv` counted at the merge, and that said 681 regions / 672 bodies. + +### The `maspsx=epilogue` TOKEN is a PER-ROW property and it is INVISIBLE in a length check (worker D) + +D's `0x8001278C` (100 B) **requires** `--fill-epilogue`, and `0x80012834` (104 B) — **ten rows away in +the same band** — epilogues PLAIN and must **not** get the flag. This makes the token the second entry +on the list of things that may not be carried across rows, and it is worse than the first: + +* the indexed-vs-pointer **loop form** at least shows up as a length difference; +* the **epilogue token** does not. Both shapes have the same instruction COUNT — + the original's `lw ra,16(sp) / move v0,zero / jr ra / addiu sp,sp,24` against cc1's + `lw ra,16(sp) / addiu sp,sp,24 / jr ra / nop` — so a missing token presents as **differing bytes at + the correct length**, which is precisely the presentation every worker on this roster has learned to + read as an *allocator or scheduler floor*. **A worker who trusts the shape will mis-classify a row + that a one-token claim would close.** + +**D's procedure, which is the right one and is now the roster's:** *read the original's last three +instructions BEFORE choosing the flag, and carry the token only when the `addiu sp,sp,N` sits inside the +`jr ra` slot.* **15 registered regions carry `maspsx=epilogue` today**, each individually verified +byte-exact, so none is at risk — but the habit is. + +### A measurable invariant for extents, and it answers D's delay-slot worry + +D reported losing a reading because its own `--end` **excluded the `jr ra` delay slot**, and said the +failure "looked exactly like a one-instruction-short row" — which is the same symptom as my guessed- +extent error earlier the same hour. + +I checked all 681 registered regions against the invariant and it holds **uniformly**: + +> **a region's extent ends exactly 8 bytes past its LAST `jr $31`** — 681 of 681, zero exceptions. + +That is worth having as a procedure because it is a **self-check a worker can run on an extent it +computed itself**: find the last `jr $31` in the extent, confirm `end - that_address == 8`, and if it is +not, the extent is wrong rather than the candidate. It is also the right rule for the multi-exit rows, +where "the last `jr $31`" and "the only `jr $31`" are different addresses. + +### Worker D's third instance of "the counter survives unless the source has no counter" + +`0x8001278C`'s loop variable is the **byte offset**, not an index: the original walks +`v0 = 180; do { store; v0 -= 20; } while (v0 >= 0)` with the address formed +`lui at,0x8012 / addu at,at,v0 / sw zero,12912(at)`. The natural indexed spelling +(`for (i = 9; i >= 0; i--) D_80123270[i].f0 = 0;`) makes cc1 keep **both** the counter and the derived +IV (`li v1,9`, `addiu v1,v1,-1`, `bgez v1`, and the delay slot becomes `addiu v0,v0,-20` instead of a +`nop`) — four instructions more. Spelling it as an explicit byte offset leaves cc1 with ONE variable, +which is the original. **Third instance this phase: the counter survives unless the source has no +counter to survive.** + +Also from that row, D's **second** instance of the symbol-arithmetic procedure paying off: D first +mis-added 2300 and asked for a `0x801221E4` the binary never touches; `gp+12` is `0x80121944` and +`gp+2300` is **`0x80122234`**, and both were already registered. D's own rule now: *read the +displacement and compute the symbol, do not infer it from an adjacent symbol's name.* diff --git a/src/func_8001278C.c b/src/func_8001278C.c new file mode 100644 index 0000000..878e880 --- /dev/null +++ b/src/func_8001278C.c @@ -0,0 +1,68 @@ +/* + * func_8001278C — 100 bytes at 0x8001278C..0x800127F0 + * + * Calls four helpers, zeroes the halfword at gp+12 (0x80121944), clears the first + * word of each of ten 20-byte records at 0x80123270 (walking a BYTE OFFSET down from + * 180), zeroes a SECOND halfword at gp+2300 (0x80122234), and returns 0. + * + * THREE levers, two of them reusable: + * + * 1. **`--fill-epilogue` (region token `maspsx=epilogue`).** The original's tail is + * `lw ra,16(sp) / move v0,zero / jr ra / addiu sp,sp,24` -- the frame release + * INSIDE the jump's delay slot. Without the flag cc1 emits + * `lw ra,16(sp) / addiu sp,sp,24 / jr ra / nop`, which is the same instruction + * COUNT and a different shape, so it shows up as differing bytes rather than a + * length mismatch. Any framed row whose `addiu sp,sp,N` sits in the `jr ra` delay + * slot needs this flag; the claim must carry the token. + * 2. **THE LOOP VARIABLE IS THE BYTE OFFSET, NOT AN INDEX.** The original iterates + * `v0 = 180; do { store; v0 -= 20; } while (v0 >= 0)` with the address formed as + * `lui at,0x8012 / addu at,at,v0 / sw zero,12912(at)`. Writing the natural + * indexed form -- `for (i = 9; i >= 0; i--) D_80123270[i].f0 = 0;` -- makes cc1 + * KEEP BOTH the counter and the derived induction variable (`li v1,9`, + * `addiu v1,v1,-1`, `bgez v1`, and the delay slot filled with `addiu v0,v0,-20` + * instead of a `nop`), 4 instructions more. Spelling the walk as an explicit byte + * offset (`*(int *)((char *)D_80123270 + i)`, 180 down to 0 by 20) leaves cc1 with + * a single variable, which is what the original has. + * 3. **THE TWO HALFWORDS ARE DIFFERENT GLOBALS.** Decode the displacement rather than + * reusing the first symbol: gp+12 is 0x80121944 and gp+2300 (0x8FC) is 0x80122234. + * (Both are already registered WITH gp markers, so no symbol request was needed; + * my own first attempt mis-added 2300 and asked for a 0x801221E4 that the binary + * does not touch -- the assembler's displacement is the authority.) + * + * LIMITS: the ten-record/20-byte stride is inferred from the walk (`li v0,180`, + * `addiu v0,v0,-20`), not from a recovered struct; only the first word of each record + * is written, so the record size is a lower bound. The two halfwords are two + * separate symbols and nothing here relates them. The four callees are named for + * their addresses and their contracts are unestablished; two of them are called + * twice in a row and are NOT merged, which is only evidence that they are calls. + */ +typedef struct { + int f0; + char pad[16]; +} Rec_80123270; + +extern Rec_80123270 D_80123270[10]; +extern short D_80121944; +extern short D_80122234; + +void func_80026304(void); +void func_80016174(void); +void func_80012F24(void); +void func_80021BA8(void); + +int func_8001278C(void) +{ + int i; + + func_80026304(); + func_80016174(); + D_80121944 = 0; + for (i = 180; i >= 0; i -= 20) { + *(int *)((char *)D_80123270 + i) = 0; + } + func_80012F24(); + func_80021BA8(); + func_80021BA8(); + D_80122234 = 0; + return 0; +} diff --git a/src/func_80012834.c b/src/func_80012834.c new file mode 100644 index 0000000..0f763d9 --- /dev/null +++ b/src/func_80012834.c @@ -0,0 +1,48 @@ +/* + * func_80012834 — 104 bytes at 0x80012834..0x8001289C + * + * If `*q` is non-zero, calls a helper with `p[37]` and `*q`; if the helper's low + * byte is non-zero that byte is returned. Otherwise stores a second helper's result + * back through `q` and returns 0. Matched on the FIRST spelling. + * + * WHAT THE BYTES PIN DOWN: the value of `*q` is loaded ONCE at the top, before the + * branch, and stays live in `a1` across it (the first call passes it without + * reloading), while the first call's FIRST argument is the VALUE at `p+148` + * (`lw a0,148(s1)`) and the second call's first argument is the ADDRESS `p+148` + * (`addiu a0,s1,148`) -- a value/address pair on the same field, which is the shape + * that makes the two calls a get/set pair rather than a repeated call. The second + * call's second argument is the POINTER `q` itself (`move a1,s0`), not `*q`. The + * `0xff` mask is computed once and reused for both the test and the return value + * (`andi v0,v0,0xff` then `bnez v0`), so `v & 0xff` must appear in BOTH the condition + * and the returned expression -- writing it once and returning `v` changes the + * emitted code. + * + * CONTRAST WORTH KEEPING: this function's epilogue is the PLAIN form + * (`addiu sp,sp,32` BEFORE `jr ra`), so it must NOT be built with `--fill-epilogue`. + * 0x8001278C, ten rows away, needs that flag. **The epilogue mode is a per-row + * property, like the loop form -- check which shape the original has before choosing + * the flag, and carry the token only when the frame release is inside the delay + * slot.** + * + * LIMITS: `p` is treated as an `int *` because the field is reached as `p[37]` + * (offset 148) and by `&p[37]`; the field's real type, the meaning of the two + * helpers, the role of `q` (an out-parameter that receives the second helper's + * result, and whose incoming value gates the first call), and what a non-zero low + * byte signifies are NOT recovered. The callees are named for their addresses. The + * frame is 32 bytes with `s0`/`s1`/`ra`, which is what two saved registers plus a + * call costs; nothing here identifies what `s0`/`s1` stand for beyond `q` and `p`. + */ +extern int func_800263E8(int a, int b); +extern int func_80026460(int *a, int *b); + +int func_80012834(int *p, int *q) +{ + if (*q != 0) { + int v = func_800263E8(p[37], *q); + + if (v & 0xff) + return v & 0xff; + } + *q = func_80026460(&p[37], q); + return 0; +}