From ac4f53b8a163ec93a7182e8f5c00510cc8a44bf7 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 20:17:30 +0000 Subject: [PATCH] re: interpreter command table recovered; withdraw my 'constructors' reading sub_822FE040 is a fully unrolled registration: 1023 slots filled with a default, then 57 explicit writes, of which 48 are real handlers and nine are a shared accept-and-discard stub. Return convention is nonzero = consumed, 0 = retry, which is how the interpreter waits for a named unit to exist. Opcode 995 is the ONLY handler touching the phase mirror [*(0x828F35F8)+236] -- the sole read and sole write in the table -- independently confirming why polling that mirror saw nothing during phase 1. And no handler spawns or despawns a unit: 256 is the strongest deploy candidate but is unconfirmed because the message ids are write-only in this image. WITHDRAWN, verified wrong: I had recorded the writes to '+20' in sub_8226E7D8 / sub_8226E930 as block initialisations by container constructors. At 0x8226E86C-0x8226E8E0 they do li r3,28 / bl 0x8230C160 then lis r10,0xAB03 / ori r7,r10,0xE4BA / stw r7,4(r3): they build an INTERPRETER COMMAND RECORD for opcode 996 and push it, i.e. AddSelector and RemoveSelector, with a 32-entry cap. The stw to 20(r3) is the command record's +20, a different object. Wrong twice: not constructors, and not that container. Also flags that sub_8230C398 -- gated on *(0x82899CE0) == 16 at both call sites -- looks like Stage 16's script compiled in C++, which 'debug defaults' does not survive given the .ssb loader explicitly refuses mission 16. --- docs/re/structures/isl-builtins.md | 50 ++++++++++++++++++++++++++---- tools/re-capture/trigger_watch.sh | 9 ++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/docs/re/structures/isl-builtins.md b/docs/re/structures/isl-builtins.md index 21d520f..f07c6f5 100644 --- a/docs/re/structures/isl-builtins.md +++ b/docs/re/structures/isl-builtins.md @@ -388,12 +388,16 @@ Three approaches, none of which produced the appender: * **`sub_8226E160`**, flagged earlier as "enqueue a pending trigger", takes a `double` plus several pointers, rejects `arg == -1`, and has **exactly one caller** (`0x8226A044`). It is a specific operation, not the general append. -* **Writes to the count at `+20`** inside the container code - (`0x8226DF00–0x8226F200`) are only **four**, and all four are part of a - *block initialisation* — `stw` to `0, 8, 12, 16, 20, 24` in consecutive - instructions, in `sub_8226E7D8` and `sub_8226E930`. Those are **constructors** - (callers `0x8226E560` and `0x822608A0`, the latter inside `ScriptMission`'s - own constructor), not increments. +* ~~**Writes to the count at `+20`** … are part of a block initialisation … + Those are **constructors**~~ — **WRONG, withdrawn.** Verified at + `0x8226E86C`–`0x8226E8E0`: those functions do `li r3,28 ; bl 0x8230C160` + (allocate 28 bytes), then `lis r10,0xAB03 ; ori r7,r10,0xE4BA ; + stw r7,4(r3)`. They are building an **interpreter command record** stamped + `0xAB03E4BA` = opcode **996**, and pushing it into the interpreter queue — + `sub_8226E7D8` = **AddSelector**, `sub_8226E930` = **RemoveSelector**, with a + 32-entry cap (`cmpwi r8, 32`). The `stw … 20(r3)` I read as "the container's + count" is the *command record's* `+20`, a different object entirely. The + analysis was wrong twice over: not constructors, and not that container. * So the increment that takes the count 0 → 1 → 2 — which is **measured, live** — does not appear as a plain `stw rN, 20(rM)` anywhere in the container's own code. It is either inlined into a caller, uses a different addressing form @@ -478,3 +482,37 @@ count. The guest was executing inside `sub_8226EAB8` at the moment of the write. **Next:** rather than guessing among 16 callers, set the watchpoint again and read the **guest LR** out of the context (`%rsi`) at the hit — the same technique that named `sub_8226EAB8` will name its caller. + +## ✅ The interpreter command table, recovered in full + +`sub_822FE040` is a **fully unrolled** registration sequence — no loop, no +`.rdata` copy. It fills **1023** slots (not 1024: `32 + 8·1023 = 8216`, and +`this+8216` is a `std::map`) with a default, then writes **57** explicit slots. +Of those, **48 are real handlers**; nine point at a shared `li r3,1 ; blr` +accept-and-discard stub. + +* default `0x82674028` = `li r3,0 ; blr` → the 966 unpopulated slots always + return 0, so an unknown command is retried 10× and dropped. +* Return convention is **nonzero = consumed**, 0 = retry — which is how the + interpreter *waits*: several handlers return 0 until a named unit exists. +* ⚠️ The dispatcher masks the opcode to 10 bits, so **opcode 1023 would fetch the + map's first word as a handler** — a latent OOB nothing constructs. + +🔑 **Opcode 995 is the only handler that touches the phase mirror +`[*(0x828F35F8)+236]`** — the sole read *and* the sole write in the entire table. +That independently confirms why polling that mirror saw nothing during phase 1. + +🔑 **No handler spawns or despawns a unit.** 518/519 destroy *order* objects; +1014 broadcasts to every mission unit. Opcode **256** is the strongest deploy +candidate (two name→definition lookups, a 52-byte request, message +`0xFE0018EF`) but is **unconfirmed** — the message ids are write-only in this +image, so the consumer cannot be reached statically. + +🟡 **`sub_8230C398` may be Stage 16's script, compiled in C++.** It posts +256/513/514/784/803/896/998/1011 with hard-coded literals (`Route_TCN001_p1F`, +`TCN001`, `SUBOBJ_001`, …) and is **gated on `*(0x82899CE0) == 16`** at both call +sites. `mission-phase-advance.md` calls those literals "debug defaults" — that +does not survive an `== 16` gate, especially as the `.ssb` loader explicitly +refuses mission 16 (`if (n == 16 || n > 32) return`) and S16 is already the +corpus outlier with no unit predicates. **Strongly indicated, not proven**: no +writer for `0x82899CE0` was found. diff --git a/tools/re-capture/trigger_watch.sh b/tools/re-capture/trigger_watch.sh index 9d9d082..ed000c8 100755 --- a/tools/re-capture/trigger_watch.sh +++ b/tools/re-capture/trigger_watch.sh @@ -70,6 +70,15 @@ kill -INT "$pid"; sleepfor 3 { echo 'echo === WHO WROTE IT ===\n'; echo 'x/3i $pc' echo 'info registers rsi rdi' echo 'echo === GUEST CONTEXT ===\n' + # PPCContext layout (ppc_context.h): cr0..cr7 = 0x00..0x1F, then r[32] at 0x20, + # ctr at 0x120, lr at 0x128, msr at 0x130. The base is pinned independently: + # the faulting instruction read 0x110(%rsi), which is r[30] under this layout, + # and the count write is `stw r11, 8(r30)` -- so 0x110 IS r30. + echo 'echo --- r28..r31, ctr, lr, msr\n' + echo 'x/8gx $rsi+0x100' + echo 'echo --- guest LR\n' + echo 'p/x *(unsigned long*)($rsi+0x128)' + echo 'p/x *(unsigned long*)($rsi+0x120)' echo 'x/128wx $rsi' echo 'echo === END ===\n'; echo 'delete'; echo 'continue'; } >> "$CMD" sleepfor 10