Correct my own decode bug: there is a caller, and r2 is a return value

349b498 reported that 0x3a7c30 has no jal callers. That was wrong and the bug was
mine: the scan built J-type targets as ((instr & 0x03ffffff) << 2) | 0x0c000000
without masking to the upper nibble, adding 0x0c000000 to every target so nothing
matched.

Corrected: exactly one jal caller, at 0x3a3d84. Still no j tail-calls, still not a
vtbl handler, still not a pointer in any level-0 data file.

Tracing back, r2 is never written between the call and the use:

    0x3a3ce4  lw    r3, 0x1500(r2)
    0x3a3d70  jal   0x2ee178
    0x3a3d84  jal   0x3a7c30

so r2 is the return value of 0x2ee178, passed through as the first argument. The
chain is now 0x2ee178 returns a pointer -> 0x3a7c30 -> 0x3a8060 -> addiu r17, r18,
0xc0.

Next question is one function: does 0x2ee178 return core_data + moby_classes.offset?
If so the reading is confirmed and the loader is located; if not, the 0xc0 in
0x3a8060 is an unrelated struct member.

Also recorded in docs: a negative from a hand-rolled decoder is worth nothing
until checked against a known case. jal 0x3a8060 decoded correctly under a
different constant in the same probe and I did not notice.

284 passing with the disc present, clippy clean.
This commit is contained in:
2026-10-14 05:47:29 +02:00
parent 349b498ddd
commit e1482abd75
+46
View File
@@ -2268,3 +2268,49 @@ Not distinguished. The immediate question for anyone continuing is whether
`? -> 0x3a7c30 (no static caller) -> 0x3a8060 -> addiu r17, r18, 0xc0`, with `r2`
supplied by whatever reaches `0x3a7c30`. Every remaining question is about
control flow and register provenance, not about counts.
### Correction to 349b498: there is a caller, and I had a decode bug
`349b498` reported "0x3a7c30 has no `jal` callers anywhere in level-0 `.text`".
**That was wrong, and the bug was mine.** The scan built the branch target as
((instr & 0x03ff_ffff) << 2) | 0x0c00_0000
without masking to the upper four bits. J-type targets take their top nibble from
the opcode field, which is zero here, so the correct form is just
`(instr & 0x03ffffff) << 2`. The un-masked constant added `0x0c000000` to every
target, so the comparison never matched anything.
Corrected scan: **`0x3a7c30` has exactly one `jal` caller, at `0x3a3d84`.** No
`j` tail-calls, and the function is still not a `lvl.vtbl` handler and still not a
pointer in any level-0 data file.
### r2 is a return value, not a caller argument
Tracing backwards from `0x3a3d84` inside the calling function:
0x3a3ce4 lw r3, 0x1500(r2) r2 already live, used as a base
0x3a3d70 jal 0x2ee178
0x3a3d84 jal 0x3a7c30 r2 is v0, i.e. the *result* of the call above
No instruction between the two writes `r2`. It is simply the return value of the
function at `0x2ee178`, passed straight through as the first argument to
`0x3a7c30`.
So the chain is
0x2ee178 --returns a pointer--> 0x3a7c30 --passes it in r2-->
0x3a8060 --addiu r17, r18, 0xc0--> 16-byte-stride walk
**The next question is now one function:** does `0x2ee178` return
`core_data + moby_classes.offset`? It is a small, self-contained function and it is
the only thing between the program and the constant. If it returns a pointer built
with `addiu r2, base, 0xc0`, the reading is confirmed and the moby class loader
has been located. If it returns something else, the `0xc0` in `0x3a8060` is an
unrelated struct member.
**Recorded as the lesson it is:** a negative result from a hand-rolled decoder is
worth nothing until the decoder is checked against a case where the answer is
already known. `jal 0x3a8060` decoded correctly under a different constant in the
same probe and I did not notice, because one probe reporting a plausible number is
as convincing as one reporting zero.