diff --git a/docs/re/structures/isl-bytecode.md b/docs/re/structures/isl-bytecode.md index 2f8be46..ca5a7ef 100644 --- a/docs/re/structures/isl-bytecode.md +++ b/docs/re/structures/isl-bytecode.md @@ -37,27 +37,44 @@ Operand kinds go through resolvers with their own 4-entry table |---|---|---| | 0 | `82263660` | integer assign — resolve rvalue (`82271D40`, kind byte[0], word@+8), resolve lvalue (`82272030`, kind byte[1], word@+4), `stw` | | 1 | `8226369C` | float assign — same shape with `82271F10`/`82272120` and `stfd` | -| 2,4,6,8 | `822636D0` | → `822713E8` (a compare/branch family; four opcodes share one handler) | -| 3,5,7,9 | `822636E4` | → `822714D0` (the sibling family) | -| 10 | `822636F8` | → `82271598` | -| 11 | `8226370C` | → `822716E0` | +| **2,4,6,8** | `822636D0` | → `822713E8` — **integer compound assign**: `+= -= *= /=` | +| **3,5,7,9** | `822636E4` | → `822714D0` — **float compound assign**: `fadd fsub fmul fdiv` | +| **10** | `822636F8` | → `82271598` — **integer compare**, sets 3 condition bits | +| **11** | `8226370C` | → `822716E0` — **float compare** (`fcmpu`; NaN clears all three) | | **12** | `82263720` | **JUMP** — `r31 = [phase+232] + word@+4` | -| 13–18 | `82263738`… | → `82271830`, `822718C8`, `82271960`, `822719F8`, `82271AC8`, `82271B60` | +| **13–18** | `82263738`… | **conditional branches** — `je`, `jne`, `jl`, `jle`, `jg`, `jge` | | **19** | `822637B0` | **CALL BUILT-IN** → `sub_82272220` | | 20 | `82263874` | `li r29,1` then the suspend path — **yield / return** | -| 21 | `822637C4` | `sub_82175C20(phase+44, phase+168)` | -| 22 | `822637E4` | `sub_82274BA0(phase+64, phase+184)` | -| 23,24 | `82263804`… | → `82271C30`, `82271CB8` | +| **21** | `822637C4` | **`push.i`** — `phase+44` deque ← `[phase+168]` (special int 1) | +| **22** | `822637E4` | **`push.f`** — `phase+64` deque ← `[phase+184]` (special float 1) | +| **23,24** | `82263804`… | **`pop.i` / `pop.f`** — back into `[+168]` / `[+184]` | Handler return codes drive the outer loop at `0x82263828`: **0** continue, **1** suspend, **2**/**3** other exits. -### ✅ Jump operands are code-base-relative +### 🔴 CORRECTED: the branch base is PER PHASE, not the file's `0x24` -Op 12 adds its operand to `[phase+232]`, the code base — i.e. the `.ssb` -header's code offset (`0x24` in every file). That settles, for this opcode, the -question `mission-script-ssb.md` left open about whether offsets are file- or -code-base-relative. +Op 12 adds its operand to `[phase+232]` — and **that is not `0x24`**. The phase +initialiser `sub_82270DF8` writes it as `0x24 + the phase's entry from the +mission-level stream`, whose three `0x1883` records carry `0xC0`, `0x14A84`, +`0x24B28` for Stage 02 → bases **`0xE4`, `0x14AA8`, `0x24B4C`**, one per phase. + +Measured on Stage 02's phase-1 segment: + +| base | branch targets landing on an instruction boundary | +|---|---| +| `0xE4` | **525 / 525** | +| `0x24` | 188 / 525 | + +So the earlier "the code base is the header's `0x24`" was wrong, and +`tools/re-capture/isl.py` printed wrong jump targets for every phase — badly for +phases 2 and 3, and mostly wrong even in phase 1. Fixed: `isl.phase_bases()` +returns the three bases, and branch ops are annotated with the base in use. + +This also settles two things `mission-script-ssb.md` left open: offsets **are** +code-base-relative, and `0x1883`'s operand **is** a code pointer (the earlier +worry that two of them "land on IEEE floats" was an artefact of adding the wrong +base). ### ✅ The call form, and a statement counter @@ -149,7 +166,10 @@ read. * Opcodes 2–11 and 13–18 are named only by handler address. The four-way sharing (2/4/6/8 and 3/5/7/9) suggests the handler re-reads the opcode to pick a comparison or a type, but that is not yet read. -* The four-way opcode sharing (2/4/6/8 and 3/5/7/9) suggests the handler - re-reads the opcode to pick a comparison or a type; not yet read. +* ✅ **How the shared handlers disambiguate — answered.** The dispatcher leaves + the opcode in `r4`, and the two shared thunks never overwrite it, so the + helpers are `f(phase, opcode, frame, &pc)` where every non-shared helper is + `f(phase, frame, &pc)`. Each helper then subtracts its base opcode and indexes + a **secondary** table (`0x82271448` for ops 2–8, `0x8227152C` for 3–9). * The mission-level stream at `+0x24` of a `.ssb` — as opposed to this ISL stream — is still only partly read. diff --git a/tools/re-capture/host_addr.py b/tools/re-capture/host_addr.py new file mode 100755 index 0000000..8ddf507 --- /dev/null +++ b/tools/re-capture/host_addr.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Translate the trigger-count guest VA into a HOST address gdb can watch. + +gdb debugs the host emulator process, so a guest VA is meaningless to it. Canary +backs guest memory with one shared-memory file, so the chain is: + + guest VA -> file offset gmem.va_to_off + file offset -> host address the shm mapping in /proc//maps, + host = map_start - map_file_offset + off + +Prints the host address on stdout, and the working on stderr. +""" +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import gmem +import isl +import squadron_state as S + +ssb = isl.load('/tmp/Stage02.ssb') +path = gmem.mem_path() +size = os.path.getsize(path) +with open(path, 'rb', buffering=0) as f: + m, _fb = S.find_mission(f, size, ssb) + if m is None: + print('NOTFOUND'); sys.exit(1) + ph = S.u32(f, m + 4) + +va = ph + 272 + 20 +off = gmem.va_to_off(va) +shm = os.path.basename(path) +pids = [p for p in os.listdir('/proc') if p.isdigit() + and os.path.exists('/proc/%s/comm' % p) + and open('/proc/%s/comm' % p).read().strip() == 'xenia_canary'] +if not pids: + print('NOPID'); sys.exit(1) +host = None +for line in open('/proc/%s/maps' % pids[0]): + if shm not in line: + continue + rng, _perm, mo = line.split()[0], line.split()[1], line.split()[2] + a, b = (int(x, 16) for x in rng.split('-')) + mo = int(mo, 16) + if mo <= off < mo + (b - a): + host = a - mo + off + break +if host is None: + print('NOMAP'); sys.exit(1) +print('mission 0x%08X phase 0x%08X va 0x%08X off 0x%X -> host 0x%X' + % (m, ph, va, off, host), file=sys.stderr) +print('0x%X' % host) diff --git a/tools/re-capture/isl.py b/tools/re-capture/isl.py index 2e32289..269358b 100755 --- a/tools/re-capture/isl.py +++ b/tools/re-capture/isl.py @@ -17,9 +17,15 @@ kind selectors passed to the operand resolvers as `r4`. (integer assignment; resolvers 0x82271D40 / 0x82272030) op 1 same shape with fmr/stfd (float assignment; 0x82271F10/0x82272120) op 12 JUMP: r31 = [phase+232] + word@+4 - -> jump operands are **relative to the code base**, which is the .ssb - header's code offset (0x24). That settles the "file- or - code-base-relative" question for this opcode at least. + -> jump operands are relative to `[phase+232]`, which is **PER PHASE**, + not the file's 0x24. The phase initialiser sub_82270DF8 writes it + as 0x24 + the phase's entry from the mission-level stream, whose + three `0x1883` records carry 0xC0 / 0x14A84 / 0x24B28 for Stage 02 + -> bases 0xE4 / 0x14AA8 / 0x24B4C. + MEASURED: with 0xE4, 525 of 525 phase-1 branch targets land on an + instruction boundary; with 0x24, only 188. Using 0x24 for every + phase -- which this tool did -- gives wrong targets in phases 2 + and 3, and mostly-wrong ones in phase 1. op 19 CALL BUILT-IN: `sub_82272220` reads the id from **word@+4** (`lwz r11,4(r28); cmplwi 0x92` -> 147 built-ins, table 0x8227226C) and word@+8 into [phase+200]. @@ -200,7 +206,7 @@ def dis(b, off, count=40, code_base=0x24, args=True, sym2=None): extra += '(' + ', '.join(parts) + ')' staged = {} elif op == 12 and words: - extra = ' -> code+0x%X (file 0x%X)' % (words[0], code_base + words[0]) + extra = ' -> code+0x%X (file 0x%X)' % (words[0], code_base + words[0]) out.append('%06X: %08X %-6s len=%-3d k=%02x,%02x %s%s' % ( off, w, name, ln, k1, k0, ' '.join('%08X' % x for x in words), extra)) @@ -252,6 +258,16 @@ def resync(b, target, back=400): return None +def phase_bases(b): + """The per-phase code bases, from the mission-level stream's 0x1883 records.""" + out = [] + off = struct.unpack_from('>I', b, CODE_BASE_FIELD)[0] + for o in range(0x24, 0x100, 4): + if struct.unpack_from('>I', b, o)[0] == 0x1883: + out.append(off + struct.unpack_from('>I', b, o + 4)[0]) + return out + + if __name__ == '__main__': b = load(sys.argv[1]) if sys.argv[2:3] == ['--calls']: diff --git a/tools/re-capture/trigger_watch.sh b/tools/re-capture/trigger_watch.sh new file mode 100755 index 0000000..04c41fb --- /dev/null +++ b/tools/re-capture/trigger_watch.sh @@ -0,0 +1,51 @@ +#!/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//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 \ + /tmp/tw-pilot.log 2>&1 & echo "--- pilot flying"; break + fi +done + +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 +{ echo 'echo === WHO WROTE IT ===\n'; echo 'bt 8'; echo 'x/3i $pc' + 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"