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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user