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.
This commit is contained in:
Sylpheed RE agent
2026-08-25 20:17:30 +00:00
parent 13ec75465b
commit ac4f53b8a1
2 changed files with 53 additions and 6 deletions

View File

@@ -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
(`0x8226DF000x8226F200`) 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.

View File

@@ -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