diff --git a/config/regions.tsv b/config/regions.tsv index 19ad12d..0799abc 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -331,6 +331,7 @@ 0x80090C8C 0x80090CAC src/func_80090C8C.c 0x80090CAC 0x80090D00 src/func_80090CAC.c 0x8009107C 0x800910B0 src/func_8009107C.c +0x800910BC 0x800911D4 src/func_800910BC.c 0x800912D4 0x800912FC src/func_800912D4.c 0x800912FC 0x8009132C src/func_800912FC.c 0x8009132C 0x80091370 src/func_8009132C.c diff --git a/docs/MATCHING_COOKBOOK.md b/docs/MATCHING_COOKBOOK.md index 0d72ed3..f1d92c5 100644 --- a/docs/MATCHING_COOKBOOK.md +++ b/docs/MATCHING_COOKBOOK.md @@ -1138,8 +1138,16 @@ magic-number expansion of `(x << 12) / 27648`, where `0x4BDA12F7 = ceil(2^45 / 2 `mflo` for a genuine 64-bit multiply — probed on open `gcc-2.7.2-psx` and the real Sony `CC1PSX` 4.0 and 4.6. So the tell is checkable in one glance. -**Recover the divisor from the magic, never from the constant's face value:** -`D = ceil(2^(32+s) / M)`. Worker D burned four spellings writing the multiply before +**Recover the divisor from the magic AND the `sra` shift together — never from the magic alone** +(worker D's amendment, after it cost a spelling on `0x800910BC`): + + D = 2^(32+s) / M where s is the shift of the `sra` that follows `mfhi` + +The magic is **ambiguous on its own**: `0x2AAAAAAB` is `/6` at `s=0`, `/12` at `s=1`, `/24` +at `s=2`. Worker D read it as `/6` and was wrong — the `sra` after the `mfhi` is `1`, so the +divisor is 12. **Corollary: the same magic appearing twice in one function is not a +contradiction** — on that row `0x66666667` serves both `/10` at `s=2` and `/5` at `s=1`, and +cc1 materialises it **once** into a callee-saved register and reuses it for both. Worker D burned four spellings writing the multiply before solving for `D` and getting 27648 — which is also the second argument of all six `func_80010654` calls in the same function. Probe `int f(int x) { return (x << 12) / 27648; }` reproduces the triple exactly. @@ -1309,3 +1317,17 @@ negations then land immediately after their own loads. Same property as worker A's `buf[1]` finding in `0x80027CA0` (`buf[1] = a0[1]-a1[1];` then `buf[1] = 0;`), now confirmed on a much larger body. **Not a scheduler quirk — a second statement.** + +### 82. The named-local rule is PER-SITE within one function (worker D) + +The third distinct instance this session of the named-locals family deciding a row, and the first +where the rule applies **per-site inside a single function**. + +Worker D's `0x800910BC`: naming the second division's result (`r = func_800F6F60() % 10; if (r >= 6) ...`) +costs **exactly 2 words** — the original keeps the remainder in **v0** because it is consumed directly +by the `slti`, while the named local forces **a0**. But the **first** division's result *may* be named, +because it is used twice (`s1 = r + 6`), and there it lands in a0 in both compilations. + +**Rule: name a result only where the original REUSES it. Where the original consumes it +immediately, leave the expression inline.** Do not apply the decision once per function — apply it +once per value. diff --git a/src/func_80069580.c b/src/func_80069580.c new file mode 100644 index 0000000..a1beaa2 --- /dev/null +++ b/src/func_80069580.c @@ -0,0 +1,54 @@ +/* + * func_80069580 — 88 bytes at 0x80069580..0x800695D8 + * + * Byte-identical reconstruction of a framed teardown helper: it detaches an + * inner object through one callee, then — only when a second field is set — + * logs the owner through a second callee with a fixed format argument. + * + * The observed instructions are: + * addiu sp,sp,-24 + * sw s0,16(sp) + * move s0,a0 the argument is needed after the first call + * sw ra,20(sp) + * lw v0,8(s0) \ func_8002497C(*(int *)(*(int *)(a0 + 8) + 12)); + * nop | + * lw a0,12(v0) | + * jal 0x8002497C | + * nop / + * lw v0,12(s0) \ if (*(int *)(a0 + 12)) + * nop | + * beqz v0,0x800695C4 | + * move a1,s0 | (delay slot) the second argument + * lui a0,0x8007 | func_8006D1C4(&D_80068A64, a0, 0); + * addiu a0,a0,-30108 | + * jal 0x8006D1C4 | + * move a2,zero / (delay slot) + * lw ra,20(sp) + * lw s0,16(sp) + * addiu sp,sp,24 + * jr ra + * nop + * + * The first callee receives a **doubly indirect** word: a pointer loaded from + * `a0 + 8` is dereferenced again at +12, and only that value is passed. The + * second field is re-read from `a0` (not from the register) after the call, + * because the call may have rewritten the object — the reload is load-bearing. + * `0x80068A64` is reached with the symbol `la` form (`lui` + `addiu`), so it is + * an address-named symbol. + * + * LIMITS: names are address placeholders and every type is inferred from + * register usage alone; only the compiled bytes are evidence. The argument is + * modelled as `int *` because the body does word loads at +8 and +12 through it; + * the nested object's layout is not observable beyond the +12 field. The three + * callees' signatures are hypotheses from the registers each call site sets. + */ + +extern char D_80068A64; +extern void func_8002497C(int); +extern void func_8006D1C4(void *, int *, int); + +void func_80069580(int *a0) { + func_8002497C(*(int *)(*(int *)((char *)a0 + 8) + 12)); + if (*(int *)((char *)a0 + 12)) + func_8006D1C4(&D_80068A64, a0, 0); +} diff --git a/src/func_8007E7FC.c b/src/func_8007E7FC.c new file mode 100644 index 0000000..cdece0f --- /dev/null +++ b/src/func_8007E7FC.c @@ -0,0 +1,54 @@ +/* + * func_8007E7FC — 96 bytes at 0x8007E7FC..0x8007E85C + * + * Byte-identical reconstruction of a framed retry helper: it asks one callee to + * process an object with a fixed selector pair, and when that reports a negative + * result it retries the same call with a zeroed selector pair. + * + * The observed instructions are: + * addiu sp,sp,-32 + * sw s0,16(sp) \ s0 = a0; s1 = a1; + * move s0,a0 | + * sw s1,20(sp) | + * move s1,a1 / + * sw ra,24(sp) + * lw v0,28(s1) \ if (func_8007C664(a0, + * li a2,2 | *(unsigned char *)(*(int *)(a1 + 28) + 18), + * lbu a1,18(v0) | 2, 2) < 0) + * jal 0x8007C664 | + * li a3,2 / (delay slot) + * bgez v0,0x8007E844 \ func_8007C664(a0, + * move a0,s0 | *(unsigned char *)(*(int *)(a1 + 28) + 18), + * lw v0,28(s1) | 0, 0); + * move a2,zero | + * lbu a1,18(v0) | + * jal 0x8007C664 | + * move a3,zero / (delay slot) + * lw ra,24(sp) + * lw s1,20(sp) + * lw s0,16(sp) + * addiu sp,sp,32 + * jr ra + * nop + * + * The object is **re-read** for the retry (`lw v0,28(s1)` again, not the value + * kept in a register): the first call may rewrite it, so the second call site + * recomputes the same address expression — the "re-reads it → cc1 re-reads it" + * direction of cookbook finding 45. Both call sites pass the same `a0`, which is + * why only the callee-saved copy is used to restore a0 in the second call's + * branch delay slot. + * + * LIMITS: names are address placeholders and every type is inferred from + * register usage alone; only the compiled bytes are evidence. The second + * parameter is modelled as `char *` with raw offsets (+28 then +18) because only + * those two displacements are observable; the nested objects' real layouts are + * unknown. The first callee's result is only tested with `bgez`, so its width + * beyond the sign is unproven. + */ + +extern int func_8007C664(int, int, int, int); + +void func_8007E7FC(int a0, char *a1) { + if (func_8007C664(a0, *(unsigned char *)(*(int *)(a1 + 28) + 18), 2, 2) < 0) + func_8007C664(a0, *(unsigned char *)(*(int *)(a1 + 28) + 18), 0, 0); +}