From 692a8045f0c703ba5f442f6344f5ec88f1e9959c Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Fri, 28 Aug 2026 00:59:34 +0000 Subject: [PATCH] re: the 60 duplicated functions are static-object constructors Each of the 60 builds a 40-element array of 24-byte string objects: addis 0x820B + addi fetches the literal, addi r30,r11,12944 fixes the array (0x828E3290 for the first), bl sub_8217FA08(dest, literal, -1) assigns, the rest of the object is zeroed, addi r3,r30,24 steps on. Why they have no callers: each appears exactly twice in the image - once in the .pdata unwind table, where EVERY function appears as (address, prolog/ length word) and reading it as a registry is a trap - and once as a slot of a null-terminated 182-entry function-pointer array at 0x82870018-0x828702EC, 170 of whose targets have zero direct xrefs. That is the static-initialiser list. Zero callers here means "static ctor", not "dead code". The teardown side confirms it: 39-43 functions sit between consecutive clones (median 40), the dominant size in 0x8281xxxx-0x8284xxxx is 11 instructions (2280 of them, about 60 x 40), and three consecutive ones call sub_823F3D68 on 0x828E3290, +24, +48 - one destructor thunk per element, same stride, same global, all zero-xref. rot_n001 and rou_e202 ARE on the disc: a raw byte sweep finds rot_n001 26 times in 7 archives (DefTables included) and rou_e202 6 in 6, exactly one per GP_MAIN_GAME_ pack. Controls: rou_e201 151/8, rou_e901 917/8, mob_n500 46/8. Narrows the earlier claim to "not GameResourceID values". Not settled: what reads the arrays. An exact-address search returns 2347 functions outside the 60 - too broad to be a consumer list; the globals sit in a dense data region and that route has no power. All fifteen artefacts byte-identical. --- docs/re/BACKLOG.md | 43 +++++++++++++++++ docs/re/structures/player-tuning-tables.md | 54 ++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index e998eae..f7be23e 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -5617,3 +5617,46 @@ byte-identical on a second run. Written up in **Both of the solver's named false-positive modes are now closed — one real (`r0`), one refuted (the 64K flag).** What remains unmeasured is the 146 rows the positive test is silent on. + +--- + +## ✅🟡 2026-08-28 — the 60 duplicated functions are static-object constructors + +Item (a), continued from the duplication finding. **What they are is settled; +what reads them is not.** + +✅ **Each of the 60 is a static-object constructor building a 40-element array of +24-byte string objects.** `addis r11, r0, 0x820B` + `addi r4, r11, -9584` fetches +the literal, `addi r30, r11, 12944` fixes the array (`0x828E3290` for the first), +`bl sub_8217FA08(dest, literal, -1)` assigns, `stw` zeroes `+4…+20`, and +`addi r3, r30, 24` steps to the next element. + +✅ **Why they have no callers.** Each clone appears exactly **twice** in the image: +once in the **`.pdata` unwind table** — where every function appears as +`(address, prolog/length word)`; reading that as a registry is a trap I nearly +fell into — and once as a slot of a **null-terminated 182-entry function-pointer +array at `0x82870018`–`0x828702EC`**, of whose targets **170 have zero direct +xrefs**. That is the static-initialiser list, dispatched indirectly and run before +`main`. **Zero callers here means "static ctor", not "dead code".** + +✅ **The teardown side confirms the shape.** Between consecutive clones sit +**39–43 functions (median 40)**, and the dominant size in the whole +`0x8281xxxx`–`0x8284xxxx` region is **11 instructions, 2 280 of them** ≈ 60 × 40. +Three consecutive ones call `sub_823F3D68` on `0x828E3290`, `+24`, `+48` — one +destructor thunk per element, same stride, same global, all zero-xref. So the unit +is **one ctor + 40 one-line dtor thunks, emitted 60 times**. + +✅ **`rot_n001` and `rou_e202` are on the disc after all.** A raw byte sweep over +every decompressed pak entry: **`rot_n001` 26 occurrences in 7 archives** +(`DefTables` included), **`rou_e202` 6 in 6** — exactly one per +`GP_MAIN_GAME_` pack and none in `DefTables`. Controls in the same sweep: +`rou_e201` 151/8, `rou_e901` 917/8, `mob_n500` 46/8. Neither is image-only; +`rou_e202` is just the roster's rarest member. Withdraws the "two names the image +carries that the disc never uses" reading — they are not `GameResourceID` values, +which is a narrower statement than I made. + +🟡 **Not settled: what reads the arrays.** An exact-address search finds **2 347** +functions outside the 60 that materialise one of the 60 global addresses — far too +broad to be a consumer list. The globals sit in a dense data region, so that route +has no power; the consumer needs a different one. Also unsettled: what selects 40 +of the disc's 172 `rou_*` ids, and which disc field holds `rot_n001`/`rou_e202`. diff --git a/docs/re/structures/player-tuning-tables.md b/docs/re/structures/player-tuning-tables.md index 092672b..bffd3bf 100644 --- a/docs/re/structures/player-tuning-tables.md +++ b/docs/re/structures/player-tuning-tables.md @@ -435,3 +435,57 @@ Two names the image carries that this disc field never uses. 🟡 Not settled: what the 60 globals are read by, and what selects 40 of the 172 `rou_*` ids. + +## ✅ What the 60 duplicated functions are (2026-08-28) + +Following the duplication finding above: each of the 60 is a **static-object +constructor**, and the object it builds is a **40-element array of 24-byte string +objects**. + +``` +8280FE20 addis r11, r0, 0x820B ; the string literal +8280FE28 addi r4, r11, -9584 +8280FE34 addi r30, r11, 12944 ; r30 = 0x828E3290, the array +8280FE3C bl 0x8217FA08 ; assign(dest, literal, -1) +8280FE40 li r11, 0 … stw r11,4..20(r30) ; zero the rest of the object +8280FE64 addi r3, r30, 24 ; next element, stride 24 +``` + +**Why they have no callers.** Each clone appears exactly **twice** in the whole +image: once in the `.pdata` unwind table — where every function appears, as +`(address, prolog/length word)`, and reading that as a registry is a trap — and +once as a slot of a **null-terminated 182-entry function-pointer array at +`0x82870018`–`0x828702EC`**. **170 of those 182 targets have zero direct xrefs**, +so the array is dispatched indirectly: it is the static-initialiser list, run +before `main`. Zero callers here means "static ctor", not "dead code". + +**The teardown side confirms the shape.** Between two consecutive clones sit +**39–43 other functions (median 40)**, and the dominant size in the whole +`0x8281xxxx`–`0x8284xxxx` region is **11 instructions, 2 280 of them** (60 × ~40). +Reading three consecutive ones: + +``` +sub_828105BC: r3 = 0x828E3290 -> bl sub_823F3D68 +sub_828105E4: r3 = 0x828E3290 + 24 -> bl sub_823F3D68 +sub_82810610: r3 = 0x828E3290 + 48 -> bl sub_823F3D68 +``` + +One thunk per element, same 24-byte stride, same global, all with zero xrefs — +the per-object destructors registered at exit. So the pattern is **one ctor +writing 40 names + 40 one-line dtor thunks, emitted 60 times**: a template or +macro-generated static array, not sixty tables. + +🟡 What this still does not say: **which code reads the arrays**. An exact-address +search finds 2 347 functions outside the 60 that materialise one of the 60 global +addresses — far too broad to be a consumer list, and the region is a dense global +data area, so that search has no power here. The consumer needs a different route. + +### ✅ `rot_n001` and `rou_e202` are on the disc after all + +Both are absent from the 480 `GameResourceID` values, but a raw byte search across +every decompressed pak entry finds **`rot_n001` 26 times in 7 archives** +(including `DefTables.pak`) and **`rou_e202` 6 times in 6** — and those six are +exactly one per `GP_MAIN_GAME_` pack, with none in `DefTables`. Controls on +the same sweep: `rou_e201` 151 / 8 archives, `rou_e901` 917 / 8, `mob_n500` 46 / 8. +So neither is an image-only name; **`rou_e202` is simply the rarest member of the +roster**, carried once per language pack. 🟡 Which field holds them is not settled.