re: two trigger appenders -- built-in 19's node carries the handler routine

Resolves the puzzle the previous commit left open: payload+28 is a 1-12 index, yet
the drain hands it to a spawner that wants a code offset.  The cause is that built-in
25 is not the only thing that appends a trigger node.

Searching for functions that write a double at +16 AND a word at +28 of the same
register finds exactly two: sub_8226A0D8 (built-in 25, vtable slot 28) which fills the
payload inline, and sub_8226E160, a free-list recycling variant whose sole caller
sub_82269ED0 is vtable slot 68 = BUILT-IN 19.

Built-in 19 passes local[4] -> node+0 (the unit), local[12] -> +4, local[24] -> +16 as
a double, and local[32] -> +28.  That last one is the field the drain spawns from, and
it is a genuine code offset:

  over all 79 call sites disc-wide
    land on the instruction stream    79 / 79 = 100.0%
    control, random 4-aligned offsets           27.8%
    values 12164 .. 111080, 73 distinct

Large, distinct, and every one resolves against a control that resolves barely a
quarter of the time.  So built-in 19 registers a trigger whose handler is a routine,
and the drain's `base + payload+28` spawn is that handler.  The two builders simply
put different things in the same slot.

Observed alongside: built-in 25 writes +24 = 1 and built-in 19 writes +24 = 0, which
is kind-tag shaped -- but the drain was not shown branching on it, so that is recorded
as an observation and not a reading.

All artefacts regenerate byte-identical; documentation only.

Still open: whether +24 selects between the node kinds, and this STILL does not
explain the unreached code -- 0 of the 79 handler offsets are unreached run-starts.
This commit is contained in:
Sylpheed RE agent
2026-08-27 07:38:24 +00:00
parent 9ee32f7940
commit 6402a34d9b
2 changed files with 65 additions and 3 deletions

View File

@@ -85,13 +85,59 @@ argument. `sub_8226D740` does operate on the trigger container; what
`sub_8226E458` does *to* it is still unread, so only the argument is corrected
here, not the conclusion.
## ✅ RESOLVED: there are TWO appenders, and only one carries a routine
The puzzle above — `payload+28` holding a 112 index while the drain hands it to
a spawner that wants a code offset — had a simple cause: **built-in 25 is not the
only thing that appends a trigger node.**
Searching for functions that write a **double at +16** and a **word at +28** of
the same register finds exactly two:
| builder | how |
|---|---|
| `sub_8226A0D8`**built-in 25**, vtable slot 28 | fills the payload inline |
| `sub_8226E160` | a free-list recycling variant, called from `sub_82269ED0` |
`sub_82269ED0` is **vtable slot 68 = built-in 19**, and it calls the builder as
```
8226A034 addi r3, r28, 272 ; the trigger container
8226A040 lwz r4, 4(r30) ; local[4] -> node+0 (the unit)
8226A03C lwz r5, 12(r30) ; local[12] -> node+4
8226A038 lfd f1, 24(r30) ; local[24] -> node+16 (double)
8226A030 lwz r8, 32(r30) ; local[32] -> node+28
```
### ✅ Built-in 19's `local[32]` IS a code offset
| over all 79 call sites disc-wide | |
|---|---|
| land on the instruction stream | **79 / 79 = 100.0 %** |
| control, random 4-aligned offsets | 27.8 % |
| value range | 12 164 … 111 080, **73 distinct** |
Large, distinct, and every one resolves — against a control that resolves barely
a quarter of the time. So **built-in 19 registers a trigger whose handler is a
routine**, and the drain's `base + payload+28` spawn is exactly that handler.
Built-in 25's `payload+28` really is a small index; the two builders simply put
different things in the same slot. Observed alongside it: built-in 25 writes
**`+24 = 1`** and built-in 19 writes **`+24 = 0`**, which is the shape of a kind
tag — 🟡 but the drain was not shown branching on it, so that is an observation,
not a reading.
## 🟡 Not settled
* **What `local[12]` indexes.** Values 112, and `SYM1_SLOTS` lists slot 12 for
built-in 25 (symbol table 1 — routes, messages, objectives), which is
consistent with a small index but was not verified here.
* **What the drain actually spawns.** `[stack+112]` is filled from `payload+28`,
yet the spawner takes a code offset — so some step in that chain is not what I
read. Left open rather than guessed.
* ~~What the drain actually spawns.~~ ✅ Resolved above: built-in 19's nodes carry
a real routine offset at `+28`.
* **Whether `+24` selects between the two node kinds** — 1 from built-in 25, 0
from built-in 19 — is unshown; the drain reads it into a stack slot that the
spawn path does not visibly use.
* **This still does not explain the unreached code.** All 79 of built-in 19's
handler offsets are already reachable — **0 are unreached run-starts**.
* **`sub_8226E458`'s effect** on the container.
* **What starts the ~15 % of unreached code** — still open, and this was not it.