The watchpoint plus Canary's source settle it. At the write, the guest context (rsi, per x64_emitter.cc:881) holds 0x8226EAE0, inside sub_8226EAB8. That function is a generic list-node insert: it reads the count at 8(r30), guards against 0x3FFFFFFF overflow, does addi r11,r11,1 / stw r11,8(r30), then links the node. It has 16 callers, so it is a shared container helper. That explains why two static searches missed it. The trigger container at phase+272 EMBEDS an inner list object at +12 -- which is why the push does 'addi r31, r30, 12' -- and the inner object keeps its count at its own +8. So 272 + 12 + 8 = 292 = the watched word, and the instruction is stw r11, 8(r30) with r30 = phase+284. Searching for 'stw rN, 20(rM)' could never have found it. Also resolves the earlier open item on +12: it is the embedded list object, not a list head pointer, which is why it read 0x000A0009 instead of an address. Method note kept: the static hunt assumed the field's offset in the OUTER object would appear in the writing instruction. A watchpoint is indifferent to the addressing form, which is why it was the right tool after two failed offset searches.
78 lines
3.5 KiB
Bash
Executable File
78 lines
3.5 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'
|
|
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"
|