From a1c41771e7b93b9d84cd22de47263e6cff7aaac2 Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Thu, 24 Sep 2026 09:46:48 -0400 Subject: [PATCH] =?UTF-8?q?phase11:=20merge=2019=20+=20cookbook=2092-94=20?= =?UTF-8?q?=E2=80=94=20535=20bodies=20/=20544=20regions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker B's four first-attempt claims (0x800B255C, 0x8004857C, 0x80030858, 0x800909D8). Cookbook 94 is the strategic one: worker B's failures cluster into exactly TWO mechanical classes -- reorg slot-fill choice and rare-epilogue fill -- and neither is a shape problem. Both are the post-pass family, which two workers have now independently arrived at and stopped on. That is the strongest argument yet for writing the post-pass rather than grinding these rows with source spellings. --- config/regions.tsv | 3 + docs/MATCHING_COOKBOOK.md | 26 ++++++++ src/func_80016F18.c | 69 +++++++++++++++++++++ src/func_80036B14.c | 122 ++++++++++++++++++++++++++++++++++++++ src/func_80036DA4.c | 106 +++++++++++++++++++++++++++++++++ 5 files changed, 326 insertions(+) create mode 100644 src/func_80016F18.c create mode 100644 src/func_80036B14.c create mode 100644 src/func_80036DA4.c diff --git a/config/regions.tsv b/config/regions.tsv index 50ec66b..8c50325 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -38,6 +38,7 @@ 0x8001644C 0x800164EC src/func_8001644C.c 0x80016DCC 0x80016E24 src/func_80016DCC.c 0x80016E50 0x80016E68 src/func_80016E50.c +0x80016F18 0x80016F80 src/func_80016F18.c 0x800170C0 0x80017108 src/func_800170C0.c 0x800171D8 0x80017200 src/func_800171D8.c 0x8001761C 0x80017660 src/func_8001761C.c @@ -135,6 +136,7 @@ 0x800301FC 0x8003022C src/func_800301FC.c 0x80030284 0x800302DC src/func_80030284.c 0x80030358 0x80030390 src/func_80030358.c +0x80030858 0x800308C4 src/func_80030858.c 0x800308C4 0x800309CC src/func_800308C4.c 0x800319F0 0x80031A48 src/func_800319F0.c 0x80031BBC 0x80031CC0 src/func_80031BBC.c @@ -329,6 +331,7 @@ 0x800900A0 0x800900CC src/func_800900A0.c 0x80090894 0x800908E4 src/func_80090894.c 0x800908E4 0x80090990 src/func_800908E4.c +0x800909D8 0x80090A44 src/func_800909D8.c 0x80090A44 0x80090A70 src/func_80090A44.c 0x80090B64 0x80090B7C src/func_80090B64.c 0x80090B7C 0x80090BB4 src/func_80090B7C.c diff --git a/docs/MATCHING_COOKBOOK.md b/docs/MATCHING_COOKBOOK.md index 1704a98..ae3dbc2 100644 --- a/docs/MATCHING_COOKBOOK.md +++ b/docs/MATCHING_COOKBOOK.md @@ -1440,3 +1440,29 @@ the registry plus the one row. That is the correct technique and worth copying. `0x80044D78` makes ten repeated calls to `func_800460AC` with successive addresses, and **entries 7 and 8 are swapped** (0x80116C50 before 0x80116C10). That is a genuine quirk in the original source order, not a transcription error — worth stating because it looks like one. + +### 92. The narrowing IS the type evidence (worker B) + +`0x800B255C`: both parameters are narrowed with `sll`/`sra`, so they are `short` rather than `int` — +**an `int` parameter is passed through untouched.** The narrowing instruction is the type evidence, +so a parameter's declared type can be read off the prologue without guessing. + +### 93. Store widths are the only evidence for a record's layout (worker B) + +`0x800909D8`: six consecutive symbol stores through `$at` with three different widths +(`sw`/`sh`/`sb`). Where a record's fields are written one after another, **the widths of the +stores are the layout** — there is no other evidence available for an unregistered struct. + +### 94. The two mechanical failure classes, and why they are the post-pass family (worker B) + +Worker B's batch clustered into exactly two classes, and **neither is a shape problem**: + +1. **Reorg slot-fill choice** (`0x800B1F94`, `0x800F2F08`) — the control flow and every instruction + match; one store/load sits in a different delay slot. In `0x800F2F08` the original sinks the last + store (`sw v1,28(a2)`) into the following `jal` slot where cc1 leaves the slot empty. +2. **Rare-epilogue fill** (`0x800FFBEC`, finding 84) — residual is the last two instructions' order, + and `maspsx=off` is worse. + +**Both are the same post-pass family** (finding 40), which is why grinding them with source spellings +is the wrong expenditure. This is the strongest argument yet for writing the post-pass: two workers +independently arrived at the same two classes and both stopped for the same reason. diff --git a/src/func_80016F18.c b/src/func_80016F18.c new file mode 100644 index 0000000..15da04c --- /dev/null +++ b/src/func_80016F18.c @@ -0,0 +1,69 @@ +/* + * func_80016F18 — 104 bytes at 0x80016F18..0x80016F80 + * + * Byte-identical reconstruction of a leaf accessor: given an object, it walks + * two levels of indirection to reach an inner pointer, and — only when both that + * pointer and the object's first word are non-zero — publishes the inner + * pointer's first word and its successor through the two out-parameters, + * otherwise zeroing both. + * + * The observed instructions are: + * beqz a0,0x80016F78 if (a0 == 0) return; + * move v1,zero (delay slot) p = 0; + * lw v0,16(a0) \ q = *(int **)(a0 + 16); + * nop | + * lw a3,32(v0) | r = *(int **)(q + 32); + * li v0,-1 | + * beq a3,v0,0x80016F3C | if (r != (int *)-1) + * nop | + * lw v1,132(a3) / p = *(int **)(r + 132); + * nop + * beqz v1,0x80016F58 \ if (p == 0 || *(int *)a0 == 0) { + * nop | + * lw v0,0(a0) | + * nop | + * bnez v0,0x80016F64 / + * nop + * sw zero,0(a1) \ *a1 = 0; + * j 0x80016F78 | + * sw zero,0(a2) / } (delay slot) *a2 = 0; + * lw v0,0(v1) \ else { + * nop | + * sw v0,0(a1) | *a1 = *p; + * addiu v0,v1,4 | + * sw v0,0(a2) / *a2 = (int)(p + 1); + * jr ra + * nop + * + * The guard is written with the **zeroing arm first** (`p == 0 || ...`), which + * is what lays the zeroing block out inline with a `j` over it and leaves the + * publishing arm last. The natural `if (p && *a0)` spelling puts the zeroing arm + * at the end and costs 19 differing bytes. Both conditions are one `||` chain + * and the two comparisons use the *same* loaded register order as the original. + * + * LIMITS: names are address placeholders and every type is inferred from + * register usage alone; only the compiled bytes are evidence. The object is + * modelled with raw offsets (+16, then +32, then +132) because only those + * displacements are observable; the sentinel `-1` is compared against a pointer + * because that is what the bytes compare. `*a2` receives `p + 1` as an int, so + * whether the original stored a pointer or a small integer there is unproven. + */ + +void func_80016F18(char *a0, int *a1, int *a2) { + int *p = 0; + + if (a0) { + int *q = *(int **)(a0 + 16); + int *r = *(int **)((char *)q + 32); + + if (r != (int *)-1) + p = *(int **)((char *)r + 132); + if (p == 0 || *(int *)a0 == 0) { + *a1 = 0; + *a2 = 0; + } else { + *a1 = *p; + *a2 = (int)(p + 1); + } + } +} diff --git a/src/func_80036B14.c b/src/func_80036B14.c new file mode 100644 index 0000000..91f34a5 --- /dev/null +++ b/src/func_80036B14.c @@ -0,0 +1,122 @@ +/* + * func_80036B14 — 656 bytes at 0x80036B14..0x80036DA4 + * + * Hypothesis, not a claim about meaning: the sibling of func_80036DA4. It primes the same + * two 4-int vectors and byte flag, and between the first and second update calls it scales + * three of the four components by a caller-supplied vector, dividing by 4096. + * + * Shape (s0 = a0, s1 = a1, s2 = a2, s3 = a3, s5 = the 5th argument, s4 = the 6th): + * 27BDFFB0 addiu sp,sp,-80 + * AFB50044 sw s5,68(sp) + * 8FB50060 lw s5,96(sp) ; 5th argument (a pointer, read from the stack) + * 93B40064 lbu s4,100(sp) ; 6th argument (unsigned char) + * ... ; s0..s3 = the four register arguments + * 1220006C beqz s1,0x80036CCC ; if (a1 == 0) skip BOTH the first and second blocks + * 8E020000 lw v0,0(s1) ; *(V *)(a0+176) = *a1 (4 loads then 4 stores) + * ... AE0500BC sw a1,188(s0) + * 8EA20000 lw v0,0(s5) ; *(V *)(a0+196) = *a4 + * ... AE0500D0 sw a1,208(s0) + * 8E0500A4 lw a1,164(s0) ; p = *(int **)(a0 + 164) + * 26070168 addiu a3,s0,168 + * A20400D4 sb s4,212(s0) + * 24A00240 addiu a0,a1,576 ; 576/4 = 144 + * 8CA20148 lw a2,328(a1) ; p[82] + * 0C00BEF0 jal 0x8002FBC0 + * 24A10270 addiu a1,a1,624 ; (delay) 624/4 = 156 + * 8E220000 lw v0,0(s1) ; t = *a1 (sp+16..sp+28) + * ... AFA1001C sw a1,28(sp) + * 1240002A beqz s2,0x80036C70 ; if (a2 == 0) skip the scaling + * 8FA10010 lw v1,16(sp) ; -- NESTED inside the a1 block + * 1840000F blez v1,0x80036C08 ; if (t.v[0] > 0) + * 8E420000 lw v0,0(s2) + * 00620018 mult v1,v0 + * 00001012 mflo v0 + * 04410002 bgez v0,0x80036C00 + * 24400FFF addiu v0,v0,4095 + * 00000740 sra v0,v0,0xc ; ... t.v[0] * a2[0] / 4096 + * AFA20010 sw v0,16(sp) + * ... ; the same for v[1] (a2[1]) and v[2] (a2[2]) + * 8FA20010 lw v0,16(sp) ; 0x80036C70: copy t back and call again + * ... AE0500BC sw a1,188(s0) + * ... AE0500D0 sw a1,208(s0) + * 8E0500A4 lw a1,164(s0) + * A20400D4 sb s4,212(s0) + * 24A0060C addiu a0,a1,1548 ; 1548/4 = 387 + * 8CA20514 lw a2,1300(a1) ; p[325] + * 0C00BEF0 jal 0x8002FBC0 + * 24A1063C addiu a1,a1,1596 ; (delay) 1596/4 = 399 + * 1260002B beqz s3,0x80036D7C ; if (a3 == 0) done + * ... ; u = D_8010C124; t.v[0..2] = *a3; copy; call 1224/1272/976 + * 8FBF0048 lw ra,72(sp) ; (END) + * ... + * + * **THE `if (a2 != 0)` BLOCK IS NESTED INSIDE THE `if (a1 != 0)` BLOCK, AND THE ONLY + * DIAGNOSTIC IS TWO BRANCH TARGETS.** Writing the two guards as siblings gives the CORRECT + * LENGTH (656) and exactly **2 differing bytes**, both of them branch displacements: + * `beqz s1` then jumps to the `if (a2)` test instead of past the whole second block, and + * `beqz s2` jumps to the wrong place. Because `t` is only initialised inside the `if (a1)` + * block, the nested form is also the only one that is correct — but the *tell* is worth + * remembering: **a correct-length candidate whose residual is a handful of branch words + * means the block nesting is wrong, not the code inside the blocks.** + * + * Also byte-required: the vector copies are struct assignments (4 loads then 4 stores, same + * lever as 0x8005584C / 0x80036DA4); `t.v[0] = t.v[1] = t.v[2] = *a3;` reads the same + * address three times (no CSE) leaving the 4th word as stack garbage; `struct V u = + * D_8010C124;` is an initialiser and is hoisted above the `t` stores; the 5th and 6th + * arguments arrive on the stack as a word and a byte respectively. The scaling is a + * DIVISION by 4096 (`/ 4096`), not `>> 12` — cc1 emits the `addiu v0,v0,4095` sign bias for + * the division and would not for a shift. + * + * LIMITS: the function name, the callee, the struct layouts and every offset are hypotheses + * read from the instruction shape; only the bytes are evidence. D_8010C124 and func_8002FBC0 + * are not registered and are referenced by their address-named spellings. `t.v[3]` is + * deliberately never written, because the original copies four words from a three-word + * initialisation. + */ + +struct V { int v[4]; }; + +extern struct V D_8010C124; +void func_8002FBC0(int *, int *, int, int *); + +void func_80036B14(char *a0, struct V *a1, int *a2, int *a3, struct V *a4, unsigned char a5) +{ + struct V t; + struct V u; + int *p; + + if (a1 != 0) { + *(struct V *)(a0 + 176) = *a1; + *(struct V *)(a0 + 196) = *a4; + *(unsigned char *)(a0 + 212) = a5; + p = *(int **)(a0 + 164); + func_8002FBC0(p + 144, p + 156, p[82], (int *)(a0 + 168)); + t = *a1; + + if (a2 != 0) { + if (t.v[0] > 0) + t.v[0] = t.v[0] * a2[0] / 4096; + if (t.v[1] > 0) + t.v[1] = t.v[1] * a2[1] / 4096; + if (t.v[2] > 0) + t.v[2] = t.v[2] * a2[2] / 4096; + } + *(struct V *)(a0 + 176) = t; + *(struct V *)(a0 + 196) = *a4; + *(unsigned char *)(a0 + 212) = a5; + p = *(int **)(a0 + 164); + func_8002FBC0(p + 387, p + 399, p[325], (int *)(a0 + 168)); + } + + if (a3 != 0) { + u = D_8010C124; + t.v[0] = *a3; + t.v[1] = *a3; + t.v[2] = *a3; + *(struct V *)(a0 + 176) = t; + *(struct V *)(a0 + 196) = u; + *(unsigned char *)(a0 + 212) = a5; + p = *(int **)(a0 + 164); + func_8002FBC0(p + 306, p + 318, p[244], (int *)(a0 + 168)); + } +} diff --git a/src/func_80036DA4.c b/src/func_80036DA4.c new file mode 100644 index 0000000..47658fc --- /dev/null +++ b/src/func_80036DA4.c @@ -0,0 +1,106 @@ +/* + * func_80036DA4 — 460 bytes at 0x80036DA4..0x80036F70 + * + * Hypothesis, not a claim about meaning: primes a sub-object's two 4-int vectors and a + * byte flag, then runs one of three update calls. The first two blocks are byte-identical + * apart from three constants, and the third differs only in how its vectors are built — + * which is why it matched on the FIRST spelling. + * + * Shape (s1 = a0, s2 = a1, s3 = a2, s5 = a3, s4 = the 5th argument, read as + * `lbu s4,96(sp)` because it arrives on the stack): + * 27BDFFB0 addiu sp,sp,-80 + * AFB40040 sw s4,64(sp) + * 93B40060 lbu s4,96(sp) ; the 5th argument + * ... ; s1..s5 = the four register arguments + * 1240003C beqz s2,0x80036E98 ; if (a1 == 0) skip the first two blocks + * 8E420000 lw v0,0(s2) ; *(V *)(a0+176) = *a1 -- a 4-word block move + * 8E430004 lw v1,4(s2) + * 8E440008 lw a0,8(s2) + * 8E45000C lw a1,12(s2) + * AE2200B0 sw v0,176(s1) + * ... AE2500BC sw a1,188(s1) + * 8E650000 lw v0,0(s5) ; *(V *)(a0+196) = *a3 + * ... AE2500D0 sw a1,208(s1) + * 26300168 addiu s0,s1,168 ; s0 = a0 + 168 + * 8E2500A4 lw a1,164(s1) ; p = *(int **)(a0 + 164) + * 02003821 move a3,s0 + * A3B400D4 sb s4,212(s1) ; *(char *)(a0 + 212) = a4 + * 24A00230 addiu a0,a1,560 + * 8CA20148 lw a2,328(a1) ; p[82] + * 0C00BEF0 jal 0x8002FBC0 ; func_8002FBC0(p+140, p+152, p[82], a0+168) + * 24A10260 addiu a1,a1,608 ; (delay) + * ... ; the SAME block again with 1532 / 1580 / 1300 + * 1260004A beqz s3,0x80036F48 ; if (a2 == 0) skip the third block + * 3C118011 lui a1,0x8011 ; u = D_8010C124 + * 2631C124 addiu a1,a1,-16092 + * ... ; 4 loads then 4 stores to sp+32..sp+44 + * 8E630000 lw v0,0(s3) ; t.v[0] = t.v[1] = t.v[2] = *a2 + * AF A20010 sw v0,16(sp) ; -- THE SAME ADDRESS LOADED THREE TIMES + * ... (three times into sp+16, sp+20, sp+24) + * ... ; t (4 words, the 4th UNINITIALISED) -> a0+176 + * ... ; u -> a0+196, sb a4,212(a0), call with 1208/1256/976 + * 8FBF0048 lw ra,72(sp) ; (END) + * ... + * + * FOUR BYTE-REQUIRED SHAPES: + * + * 1. **The vector copies are STRUCT ASSIGNMENTS** (`*(struct V *)(a0 + 176) = *a1;`), not + * four element stores — the same lever proven at 0x8005584C. This gives the original's + * 4-loads-then-4-stores block move. + * 2. **`t.v[0] = t.v[1] = t.v[2] = *a2;` reads the SAME address three times.** cc1 does not + * CSE the three `lw v0,0(s3)` loads, so the source really does read `*a2` three times. + * The struct's fourth word is never written and is copied as stack garbage — the + * original copies 4 words from a 3-word initialisation, which is only reproducible by + * copying a whole struct. + * 3. **`struct V u = D_8010C124;`** — an initialiser, so it is hoisted above the `t` + * stores exactly as the original emits it (sp+32..44 filled first). + * 4. **The 5th argument is `unsigned char`** and arrives on the stack (`lbu s4,96(sp)`, + * i.e. the caller's sp+16), then is stored with `sb`. + * + * The three calls are `func_8002FBC0(p + A, p + A + 12, p[A - 58], a0 + 168)` with + * A = 140, 383, 302 — i.e. a pointer field 58 words before two vectors 12 words apart, + * which is a struct layout, not three unrelated constants. + * + * LIMITS: the function name, the callee, the struct layouts (V, the object at a0+164, the + * target of p) and every offset are hypotheses read from the instruction shape; only the + * bytes are evidence. D_8010C124 is not registered and is referenced by its address-named + * spelling. func_8002FBC0 is likewise referenced by address. + */ + +struct V { int v[4]; }; + +extern struct V D_8010C124; +void func_8002FBC0(int *, int *, int, int *); + +void func_80036DA4(char *a0, struct V *a1, int *a2, struct V *a3, unsigned char a4) +{ + int *p; + + if (a1 != 0) { + *(struct V *)(a0 + 176) = *a1; + *(struct V *)(a0 + 196) = *a3; + *(unsigned char *)(a0 + 212) = a4; + p = *(int **)(a0 + 164); + func_8002FBC0(p + 140, p + 152, p[82], (int *)(a0 + 168)); + + *(struct V *)(a0 + 176) = *a1; + *(struct V *)(a0 + 196) = *a3; + *(unsigned char *)(a0 + 212) = a4; + p = *(int **)(a0 + 164); + func_8002FBC0(p + 383, p + 395, p[325], (int *)(a0 + 168)); + } + + if (a2 != 0) { + struct V t; + struct V u = D_8010C124; + + t.v[0] = *a2; + t.v[1] = *a2; + t.v[2] = *a2; + *(struct V *)(a0 + 176) = t; + *(struct V *)(a0 + 196) = u; + *(unsigned char *)(a0 + 212) = a4; + p = *(int **)(a0 + 164); + func_8002FBC0(p + 302, p + 314, p[244], (int *)(a0 + 168)); + } +}