From 8ece49c130c8041ea8851bacdde9b336ec2f8a34 Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Wed, 23 Sep 2026 21:24:15 -0400 Subject: [PATCH] phase6: register the leaf/gp batch and record the framed-function blocker --- config/regions.tsv | 6 ++- config/symbols.tsv | 2 + docs/MATCHING_COOKBOOK.md | 24 ++++++++++ docs/PHASE6_FRAMED_BLOCKER.md | 84 +++++++++++++++++++++++++++++++++++ phase-ends/CURRENT_PHASE.md | 28 +++++++++++- src/func_80017AE8.c | 31 +++++++++++++ src/func_80026264.c | 25 +++++++++++ src/func_800262E0.c | 28 ++++++++++++ 8 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 docs/PHASE6_FRAMED_BLOCKER.md create mode 100644 src/func_80017AE8.c create mode 100644 src/func_80026264.c create mode 100644 src/func_800262E0.c diff --git a/config/regions.tsv b/config/regions.tsv index 9ba9729..6b0eea3 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -10,7 +10,11 @@ # the build is the all-payload data baseline and contains no C. # # Matched so far: +0x80012780 0x8001278C src/func_80012780.c 0x80017AD4 0x80017AE8 src/func_80017AD4.c +0x80017AE8 0x80017AF8 src/func_80017AE8.c +0x80026264 0x80026274 src/func_80026264.c +0x800262E0 0x800262EC src/func_800262E0.c +0x800262EC 0x800262F8 src/func_800262E0.c 0x8002D2A0 0x8002D2BC src/func_8002D2A0.c 0x8002D2BC 0x8002D2D4 src/func_8002D2BC.c -0x80012780 0x8001278C src/func_80012780.c diff --git a/config/symbols.tsv b/config/symbols.tsv index 4cc6fd1..fd33449 100644 --- a/config/symbols.tsv +++ b/config/symbols.tsv @@ -16,3 +16,5 @@ D_8012E2C8 0x8012E2C8 D_8010F354 0x8010F354 _gp 0x80121938 D_80121974 0x80121974 gp +D_80121B18 0x80121B18 gp +D_80121B14 0x80121B14 gp diff --git a/docs/MATCHING_COOKBOOK.md b/docs/MATCHING_COOKBOOK.md index 36f8bc0..96a3d8b 100644 --- a/docs/MATCHING_COOKBOOK.md +++ b/docs/MATCHING_COOKBOOK.md @@ -13,6 +13,11 @@ project; each claim was observed here first. | Assembler | `ASPSX` 2.81 (Sony) | SDK banner | | Working invocation | `cc1 -quiet -O2 -G0 -mno-split-addresses`, then GNU `as -march=r3000 -G0` | Byte-identical ranges; `make gate` | +> **Under review (Phase 6).** This identification does not explain the whole executable. Framed +> functions use a reorder-mode epilogue that `egcs-2.91.66` + maspsx does not produce, and two +> verified functions require opposite assembler scheduling. See finding 11 and +> [PHASE6_FRAMED_BLOCKER.md](PHASE6_FRAMED_BLOCKER.md). + ## Findings ### 1. `-mno-split-addresses` is mandatory @@ -128,8 +133,27 @@ over 1,286 distinct addresses. See [PHASE6_SMALL_DATA.md](PHASE6_SMALL_DATA.md). *Limit:* the numeric `-G` threshold is **not recoverable** from the code (object sizes are unknown); `-mgpopt`/`-mno-gpopt` produce identical `cc1` output for these functions. +### 11. Framed epilogues use reorder-mode scheduling — unresolved + +- **322** framed functions end `lw ra,off(sp)` / `addiu sp,sp,N` / `jr ra` / `nop` (shape A). +- `egcs-2.91.66` + `-mno-split-addresses` + maspsx produces `lw ra,off(sp)` / `nop` / `jr ra` / + `addiu sp,sp,N` (shape B). The **real** PsyQ 4.5 `CC1PSX` and the **real** `ASPSX` 2.81 also + produce shape B. +- The `gcc-2.6.0-psx` / `2.6.3-psx` / `2.7.2-psx` / `2.7.2-cdk` builds emit the epilogue in reorder + mode and reproduce shape A byte-for-byte (test case `0x80024C14`). +- Separately, `q_sym_store` (`0x800F3160`) and `func_8002D2BC` (`0x8002D2BC`) require **opposite** + assembler scheduling, so no single assembler/maspsx configuration reproduces both. + +*Basis:* the epilogue census (322 vs 13), the `0x80024C14` compiler matrix, and real-`ASPSX` +assembly of the cc1 output. +*Limit:* **open** — the compiler identity and the assembler model need revision before framed +functions can be matched. See [PHASE6_FRAMED_BLOCKER.md](PHASE6_FRAMED_BLOCKER.md). + ## Open questions +- **Framed-function epilogue and assembler scheduling are unresolved** (finding 11): the original + compiler is a 2.6/2.7-family build, or egcs-2.91.66 with an unidentified flag; and + `q_sym_store`/`func_8002D2BC` conflict under every tested assembler configuration. - The exact `-G` small-data threshold is not recoverable from the code; the per-symbol `gp` form is reconstructed from the original's accesses instead (finding 10). - Whether `-mgpopt` was passed is not observable: it does not change `cc1` output for the cases diff --git a/docs/PHASE6_FRAMED_BLOCKER.md b/docs/PHASE6_FRAMED_BLOCKER.md new file mode 100644 index 0000000..e33edf3 --- /dev/null +++ b/docs/PHASE6_FRAMED_BLOCKER.md @@ -0,0 +1,84 @@ +# Phase 6 — Framed-Function and Assembler-Scheduling Blocker + +**Scope:** P6-T6 (the matching batch). **Status: OPEN — recorded, not resolved.** This blocks the +"call with a frame" shape and any framed function, and it puts the Phase 5 compiler identification +back in question. It does **not** affect the 8 already-registered matches. + +## 1. The framed epilogue + +A census of `jr ra` sites shows two epilogue shapes for functions with a stack frame: + +| Shape | Instructions | Count | +|---|---|---| +| **A** (game code) | `lw ra,off(sp)` / `addiu sp,sp,N` / `jr ra` / `nop` | **322** | +| **B** (CRT/library region `0x800FBxxx`–`0x80100xxx`) | `lw ra,off(sp)` / `nop` / `jr ra` / `addiu sp,sp,N` | 13 | + +The original uses shape **A** almost everywhere. Our pipeline produces shape **B**: + +- The open `gcc-2.91.66-psx` and the **real** PsyQ 4.5 `CC1PSX.EXE` produce the *same* cc1 output, + which puts the stack restore in the jump delay slot under `.set noreorder`. +- The **real** `ASPSX.EXE` 2.81, run on that cc1 output, also produces shape **B** (verified: the + object's `.text` is `lw ra,16(sp)` / `nop` / `jr ra` / `addiu sp,sp,24`). +- The `gcc-2.6.0-psx`, `gcc-2.6.3-psx`, `gcc-2.7.2-psx` and `gcc-2.7.2-cdk` builds emit the epilogue + in **reorder** mode (no `.set noreorder`). With maspsx + GNU `as` they produce shape **A** and + match the original byte-for-byte. + +Test case (`0x80024C14..0x80024C34`, a trivial `void f(void){ g(); }`): + +| Compiler | Result | +|---|---| +| `gcc-2.91.66-psx` (`-mno-split-addresses`) | `DIFF` at `0x80024C28` | +| real `CC1PSX.EXE` 4.5 | same as 2.91.66 | +| `gcc-2.6.0-psx`, `gcc-2.6.3-psx`, `gcc-2.7.2-psx`, `gcc-2.7.2-cdk` | **`MATCH`** | + +**Implication:** the original compiler is in the **2.6/2.7 family**, or egcs-2.91.66 emits shape A +under an unidentified flag (none found: `-O0/-O1/-O2/-O3/-Os`, `-fno-delayed-branch`, +`-fno-schedule-insns(2)`, `-mcpu=r3000`, `-mips1`, `-fno-omit-frame-pointer`, and others were tried). + +## 2. The conflicting assembler evidence + +Two functions that are both byte-verified force **opposite** assembler scheduling: + +| Function | Original bytes | Requires | +|---|---|---| +| `q_sym_store` `0x800F3160` (12 B) | `lui at,0x8014` / `jr ra` / `sw a0,off(at)` | `$at` macro form **and** the store in the jump delay slot (GNU `as` reorder behaviour) | +| `func_8002D2BC` `0x8002D2BC` (24 B) | `lui v0` / `addiu v0` / `lui at` / `sw v0,off(at)` / `jr ra` / `nop` | ASPSX `addiu` for `la` **and** the store before the jump (noreorder behaviour) | + +Measured results for the same cc1 output: + +| Pipeline | `q_sym_store` | `func_8002D2BC` | +|---|---|---| +| `as` reorder, assemble-time `--defsym`, no maspsx | **MATCH** (12 B) | `LENGTH-MISMATCH` (20 B) | +| maspsx + link-time symbols (current, P6-T3) | `LENGTH-MISMATCH` (16 B) | **MATCH** (24 B) | +| real `ASPSX.EXE` 2.81 | `LENGTH-MISMATCH` (16 B) | **MATCH** (24 B) | + +No single assembler, flag set, or maspsx configuration reproduces both. Either the original compiler +emitted per-function `.set` scheduling that we have not reproduced, or the toolchain/assembler model +is wrong. + +**Regression:** P6-T3's maspsx stage (needed for `func_8002D2BC`'s `la`) changes `q_sym_store` from +`MATCH` to `LENGTH-MISMATCH`. `q_sym_store` was a Phase 5 oracle but was never registered, so the +tracked gate is unaffected. + +## 3. Consequences + +- **No framed function can be matched** until the epilogue scheduling is resolved; the "call with a + frame" shape required by P6-T6 is blocked. +- The `0x8005DEF8` re-attempt is **pre-empted**: it is a framed function, so its epilogue would + mismatch regardless of the constant-multiply reconstruction. +- The Phase 5 compiler identification (`egcs-2.91.66`, PsyQ 4.5) is **not sufficient** to explain the + executable. The loader's `PsyQ Version = 4.5.0` may describe the runtime library, not the compiler. + +## 4. What is unaffected + +- The 8 registered regions still pass `sf3_match range` and `make gate` + (`c_regions=8`, 0 differing bytes, SHA-1 `e173426c157384ebf1b6caf8c6fea18a85a14af9`). +- The four toolchain-gap closures (P6-T2..T4) remain byte-proven for the functions they were tested + on. + +## 5. Questions for the developer + +1. Re-identify the compiler within the 2.6/2.7 family (2.6.0, 2.6.3, 2.7.2-psx, 2.7.2-cdk) — more + discriminating functions are needed. +2. Decide the assembler model (ASPSX vs GNU `as` reorder vs a per-function hybrid). +3. Decide how to handle maspsx: it is required for `la` but breaks the `sw`-delay-slot case. diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md index cf20d80..7bb1866 100644 --- a/phase-ends/CURRENT_PHASE.md +++ b/phase-ends/CURRENT_PHASE.md @@ -12,7 +12,7 @@ - [x] **P6-T4 — `-G` small-data threshold from byte evidence** (complete) - [x] **Rules check** — re-read `AGENTS.md` mandatory behavior after P6-T4 and stated the required continuation notice. - [x] **P6-T5 — Evidence-graded function-boundary inventory** (complete) -- [ ] P6-T6 — First matching batch, with duplicate sharing +- [~] **P6-T6 — First matching batch, with duplicate sharing** — **partial**: 8 regions registered and duplicate sharing demonstrated; the "call with a frame" shape is blocked by `docs/PHASE6_FRAMED_BLOCKER.md` - [ ] P6-T7 — Cookbook, conventions, verification record, and phase gate ## P6-T1 — Baseline revalidation (2026-09-23) @@ -193,3 +193,29 @@ code in this single linked image. **Limit:** no candidate is promoted to a match on the strength of the inventory alone; every match still needs `sf3_match range` and `make gate`. + +## P6-T6 — Matching batch (partial, 2026-09-23) + +**Delivered:** + +- Registered three more matches and one duplicate row, taking the registry to **8 regions**: + `func_80017AE8` (three struct stores), `func_80026264` (gp getter), and `func_800262E0` / + `func_800262EC` (an identical 12-byte gp setter body registered twice against one source). +- **Duplicate sharing demonstrated on a real shared body:** the 12-byte body occurs exactly twice in + the payload (`0x800262E0`, `0x800262EC`); both rows compile from `src/func_800262E0.c`. +- `make gate`: `c_regions=8`, 0 differing bytes, SHA-1 `e173426c…`. + +**Blocked:** the "call with a frame" shape, and the `0x8005DEF8` re-attempt (itself a framed +function). The blocker is recorded in `docs/PHASE6_FRAMED_BLOCKER.md`: + +- 322 framed functions end `lw ra,off(sp)` / `addiu sp,sp,N` / `jr ra` / `nop` (shape A); our + `egcs-2.91.66` + maspsx pipeline produces `lw ra` / `nop` / `jr ra` / `addiu sp,sp,N` (shape B). +- The **real** PsyQ 4.5 `CC1PSX` and the **real** `ASPSX` 2.81 also produce shape B, so the original + compiler is not egcs-2.91.66 for this class; the `gcc-2.6.0/2.6.3/2.7.2-psx/2.7.2-cdk` builds + produce shape A and match the test case byte-for-byte. +- `q_sym_store` (`0x800F3160`) and `func_8002D2BC` (`0x8002D2BC`) require **opposite** assembler + scheduling, so no single assembler/maspsx configuration reproduces both. P6-T3's maspsx fixes + `la` but regresses `q_sym_store`. + +**Not claimed:** no framed function is matched; the Phase 5 compiler identification is recorded as +insufficient pending developer input. diff --git a/src/func_80017AE8.c b/src/func_80017AE8.c new file mode 100644 index 0000000..b56764d --- /dev/null +++ b/src/func_80017AE8.c @@ -0,0 +1,31 @@ +/* + * func_80017AE8 — 16 bytes at 0x80017AE8..0x80017AF8 + * + * Byte-identical reconstruction of a leaf that stores three arguments into + * consecutive struct fields, with the last store scheduled into the `jr ra` + * delay slot. + * + * The observed instructions are: + * sw a1,12(a0) + * sw a2,16(a0) + * jr ra + * sw a3,20(a0) (delay slot) + * + * LIMITS: the function name, the struct name and every field name are + * hypotheses reconstructed from the disassembly. Only the compiled bytes are + * evidence. Fields before offset 0x0C and after 0x14 are not touched here and + * are not modelled. + */ + +typedef struct { + char pad[12]; /* offsets 0x00..0x0B — not touched */ + int f3; /* offset 0x0C */ + int f4; /* offset 0x10 */ + int f5; /* offset 0x14 */ +} func_80017AE8_args; + +void func_80017AE8(func_80017AE8_args *p, int a, int b, int c) { + p->f3 = a; + p->f4 = b; + p->f5 = c; +} diff --git a/src/func_80026264.c b/src/func_80026264.c new file mode 100644 index 0000000..4aca587 --- /dev/null +++ b/src/func_80026264.c @@ -0,0 +1,25 @@ +/* + * func_80026264 — 16 bytes at 0x80026264..0x80026274 + * + * Byte-identical reconstruction of a leaf getter that returns the difference of + * two gp-relative globals. + * + * The observed instructions are: + * lw v1,480(gp) v1 = D_80121B18 + * lw v0,476(gp) v0 = D_80121B14 + * jr ra + * subu v0,v1,v0 (delay slot) + * + * `gp` is 0x80121938, so the two globals are 0x80121B18 and 0x80121B14; both + * are marked `gp` in `config/symbols.tsv` and accessed with `%gp_rel`. + * + * LIMITS: the function name and both global names/types are hypotheses + * reconstructed from the disassembly. Only the compiled bytes are evidence. + */ + +extern int D_80121B18; +extern int D_80121B14; + +int func_80026264(void) { + return D_80121B18 - D_80121B14; +} diff --git a/src/func_800262E0.c b/src/func_800262E0.c new file mode 100644 index 0000000..e28d987 --- /dev/null +++ b/src/func_800262E0.c @@ -0,0 +1,28 @@ +/* + * func_800262E0 / func_800262EC — 12 bytes each + * + * Byte-identical reconstruction of a leaf setter. The identical 12-byte body + * occurs at both 0x800262E0..0x800262EC and 0x800262EC..0x800262F8, so it is + * matched once and registered twice against this source (the documented + * N-rows-to-one-source duplicate-sharing mechanism). + * + * The observed instructions are: + * sw a0,480(gp) D_80121B18 = x + * jr ra + * nop + * + * `gp` is 0x80121938, so the target is 0x80121B18; it is marked `gp` in + * `config/symbols.tsv` and accessed with `%gp_rel`. + * + * LIMITS: the function name and the global's name/type are hypotheses + * reconstructed from the disassembly. Only the compiled bytes are evidence. + * The two addresses are treated as two function entries because each ends in + * its own `jr ra` and each is a distinct code location; this is an evidence + * judgement, not a proven original symbol table. + */ + +extern int D_80121B18; + +void func_800262E0(int x) { + D_80121B18 = x; +}