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.
87 lines
4.0 KiB
Bash
Executable File
87 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Find what WRITES the trigger-queue count, by watching the word rather than
|
|
# hunting the instruction statically.
|
|
#
|
|
# The static hunt failed (isl-builtins.md): the only writes to `+20` in the
|
|
# container's code are block initialisations, yet the count demonstrably moves
|
|
# 0 -> 1 -> 2 during a mission. A watchpoint names the writer directly.
|
|
#
|
|
# The address translation is the fiddly part, so it is explicit:
|
|
# guest VA -> file offset via gmem.va_to_off
|
|
# file offset -> HOST address via the /dev/shm/xenia_memory_* mapping in
|
|
# /proc/<pid>/maps: host = map_start - map_off + off
|
|
# gdb debugs the HOST process, so a guest VA cannot be watched directly.
|
|
set -u
|
|
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
|
|
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
|
|
export XENIA_BIN=/sylph-home/re/bin/gdb-wrap/xenia_canary
|
|
SD="$(cd "$(dirname "$0")" && pwd)"; export SD
|
|
CMD=/tmp/gdb-cmd; OUT=/tmp/gdb-out.log
|
|
WATCH_S="${1:-240}"
|
|
sleepfor(){ python3 -c "import time,sys; time.sleep(float(sys.argv[1]))" "$1"; }
|
|
|
|
"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; }
|
|
CFG=/tmp/nav-tw.json
|
|
for t in 1 2 3; do
|
|
python3 "$SD/pad.py" set "rt=1" >/dev/null 2>&1; sleepfor 3
|
|
python3 "$SD/pad.py" clear >/dev/null 2>&1
|
|
if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then
|
|
SYLPH_HUNT=1 SYLPH_KEEPOUT=1400 nohup python3 "$SD/pilot.py" "$CFG" 900 \
|
|
</dev/null >/tmp/tw-pilot.log 2>&1 & echo "--- pilot flying"; break
|
|
fi
|
|
done
|
|
|
|
# One run reached "IN FLIGHT" with the script NOT resident and the mission
|
|
# unlocatable -- and it was frozen on a black screen soon after
|
|
# (script-runtime-probe.md). A healthy run has the script resident at the first
|
|
# sample, so a short poll separates "not loaded yet" from "this run is broken"
|
|
# without waiting on a doomed one.
|
|
ok=0
|
|
for _ in 1 2 3 4 5 6; do
|
|
if python3 -c "
|
|
import sys,os; sys.path.insert(0,'$SD')
|
|
import squadron_state as S, isl, gmem
|
|
ssb=isl.load('/tmp/Stage02.ssb'); p=gmem.mem_path()
|
|
with open(p,'rb',buffering=0) as f:
|
|
sys.exit(0 if S._find(f, os.path.getsize(p), ssb[:20]) else 1)"; then ok=1; break; fi
|
|
echo "--- script not resident yet"; sleepfor 10
|
|
done
|
|
[ $ok = 1 ] || { echo "SCRIPT NEVER BECAME RESIDENT -- run is broken, not slow"; exit 3; }
|
|
|
|
HOSTADDR=$(python3 "$SD/host_addr.py" 2>/tmp/tw-addr.err)
|
|
echo "--- translation: $(cat /tmp/tw-addr.err)"
|
|
echo "--- host addr: $HOSTADDR"
|
|
case "$HOSTADDR" in 0x*) ;; *) echo "could not translate"; exit 2;; esac
|
|
|
|
pid=$(pgrep -x gdb | head -1); before=$(wc -c < "$OUT")
|
|
kill -INT "$pid"; sleepfor 3
|
|
{ echo 'echo === WATCH SET ===\n'
|
|
echo "watch *(unsigned int*)$HOSTADDR"
|
|
echo "continue"; } >> "$CMD"
|
|
echo "--- watching ${WATCH_S}s"
|
|
sleepfor "$WATCH_S"
|
|
kill -INT "$pid"; sleepfor 3
|
|
# The host stack is useless here: the write happens in JIT-compiled guest code,
|
|
# which is unsymbolised and not host-unwindable. But Xenia's x64 backend keeps
|
|
# the guest context in %rsi (x64_emitter.cc: `GetContextReg() { return rsi; }`),
|
|
# so the guest register file is right there. Dump it and pick out the words that
|
|
# look like guest code addresses (0x82xxxxxx) -- the guest LR is among them, and
|
|
# that names the calling guest function.
|
|
{ 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
|
|
tail -c +$((before + 1)) "$OUT" | grep -vE '^\s*$' | tail -60
|
|
echo "TRIGGER WATCH DONE"
|