re: prove the guest PC -- an infinite copy loop in sub_82457780

The previous section withdrew this localisation for resting on a stale
PPCContext.  It is now re-established by a sound method.

Xenia has a CPU cvar, emit_source_annotations, that makes MarkSourceOffset emit
"nop; nop; mov eax,<guest_address>; nop; nop" at EVERY guest instruction
boundary -- 90 90 B8 xx xx xx xx 90 90 in the code bytes.  So the guest PC reads
straight out of memory around rip, with no context and no offset guessing.

Dumping backwards from rip gives a clean run of them -- 0x82457874, 78, 7C, 80,
84 -- and the annotation immediately before rip is 0x824578A0.  That is
`sth r6, 0(r9)` inside sub_82457780: the exact instruction predicted from the
disassembly last time, now reached independently.

The loop advances r11 by 8 and terminates on `bne cr6` -- exact inequality.  If
r31 - r7 is not a positive multiple of 8, the comparison never becomes equal and
the loop never exits.

And the thread is genuinely running: over 4 s, Main XThread consumed 890 ms of
CPU, the top of any thread, with the process at ~238%.  A spin, not a block.

One bad metric recorded: I tried to show the loop marching through memory by
watching the last non-zero byte above 0x70200000.  It never moved -- but that
statistic saturates in an already-written region, so it could not have moved.
Not evidence either way, though it briefly looked like a refutation.

Open and now narrow: what makes r31 - r7 non-congruent to 0 mod 8.  r7 comes
from 0(r30), r31 is the container end, and r3 holds a fresh buffer from
0x824F7240 -- read those three from HOST registers at the freeze.
This commit is contained in:
Sylpheed RE agent
2026-08-26 17:50:57 +00:00
parent e5dc3e1e4d
commit a1b6add50d

View File

@@ -809,3 +809,73 @@ it just is not established that the guest is *in* it.
function* it belongs to is not. The way to get that honestly is a host→guest code
mapping from Xenia itself rather than inference from a stale context — its code
cache knows the answer, and dumping that mapping is the next step.
---
# ✅✅ The guest PC, proven — an infinite copy loop in `sub_82457780`
**2026-08-26.** The previous section withdrew this localisation because it rested
on a stale `PPCContext`. It is now **re-established by a sound method**, and the
withdrawal was right to demand one.
## The method: Xenia's own JIT annotations
`emit_source_annotations` (a `CPU` cvar, off by default) makes `MarkSourceOffset`
emit, at **every guest instruction boundary**:
nop; nop; mov eax, <guest_address>; nop; nop -> 90 90 B8 xx xx xx xx 90 90
So the guest PC is readable straight out of the code bytes around `rip` — no
context, no offsets, no inference. Dumping backwards from `rip` gives a clean run
of them:
... B8 74 78 45 82 ... -> guest 0x82457874
... B8 78 78 45 82 ... -> guest 0x82457878
... B8 7C 78 45 82 ... -> guest 0x8245787C
... B8 80 78 45 82 ... -> guest 0x82457880
... B8 84 78 45 82 ... -> guest 0x82457884
and the annotation **immediately before `rip`** is `0x824578A0`.
**Guest PC = `0x824578A0` = `sth r6, 0(r9)`**, inside `sub_82457780` — the exact
instruction predicted from the disassembly, reached independently.
## The loop, and why it cannot end
82457868 mr r11, r7 ; r11 = start
8245786c cmplw cr6, r11, r31
82457870 beq cr6, 0x824578B4 ; skip entirely if start == end
8245787c mr r10, r11 ; <== back-edge target
82457884 addi r11, r11, 8 ; advance by 8
8245788c cmplw cr6, r11, r31
82457890 lhz r6, 0(r10) ; copy four halfwords
824578a0 sth r6, 0(r9) ; <== the guest PC
824578b0 bne cr6, 0x8245787C ; continue while r11 != r31
The termination test is **`bne` — exact inequality**, on a pointer advancing in
steps of **8**. If `r31 r7` is not a positive multiple of 8, the comparison
never becomes equal and **the loop never exits**.
## ✅ And the thread really is running
Measured with a *safe* `/proc` parse (see the trap below): over 4 seconds,
| thread | CPU |
|---|---|
| **`Main XThread`** | **890 ms** |
| `xenia_canary` | 860 ms |
| `XThreadD55FF6C0` | 690 ms |
| `llvmpipe-4` | 500 ms |
`Main XThread` is the **top consumer**, and the process burns ~238 % CPU overall.
So this is a spin, not a block — which is what an unterminatable loop looks like.
⚠️ **A bad metric on the way**: I tried to show the loop "marching" through memory
by watching the *last non-zero byte* above `0x70200000`. It never moved — but that
statistic saturates in a region that is already written, so it could not have
moved. It is not evidence either way, and it briefly looked like a refutation.
❔ Still open, and now narrow: **what makes `r31 r7` non-congruent to 0 mod 8.**
`r7` is loaded from `0(r30)` and `r31` is the container's end pointer, with a
fresh buffer from `0x824F7240` in `r3` — so the next step is to read those three
values at the moment of the freeze, from *host* registers rather than the context.