#!/usr/bin/env python3 """Read the *live* guest memory of a running Xenia Canary. Canary backs the whole guest address space with a single shared-memory file, `/dev/shm/xenia_memory_`, so the guest's RAM is readable from the host with no debugger, no emulator patch and no pause: open the file, seek, read. The file is one flat image of Xenia's *physical* backing store; guest virtual addresses map into it through Xenia's fixed table (memory.cc `map_info`). `va_to_off()` implements that table, so callers work in guest VAs. Sub-commands find search every allocated extent, print guest VAs read [len] hexdump guest memory words [n] dump n big-endian u32 / f32 pairs `` is a python literal-ish string: plain text, or `hex:0011aabb`. Numbers accept 0x form. The scan uses SEEK_DATA so the ~4.6 GB of sparse holes cost nothing. """ import os import re import sys import struct # (guest_va_lo, guest_va_hi_inclusive, file_offset_of_lo) — Xenia memory.cc. MAP = [ (0x00000000, 0x3FFFFFFF, 0x00000000), (0x40000000, 0x7EFFFFFF, 0x40000000), (0x7F000000, 0x7F0FFFFF, 0x00000000), (0x7F100000, 0x7FFFFFFF, 0x00100000), (0x80000000, 0x8FFFFFFF, 0x80000000), (0x90000000, 0x9FFFFFFF, 0x80000000), (0xA0000000, 0xBFFFFFFF, 0x100000000), (0xC0000000, 0xDFFFFFFF, 0x100000000), (0xE0000000, 0xFFFFFFFF, 0x100000000), ] def va_to_off(va): for lo, hi, base in MAP: if lo <= va <= hi: return base + (va - lo) raise ValueError(f"va {va:#x} outside the guest map") def off_to_vas(off): """All guest VAs that alias this file offset (the map is many-to-one).""" out = [] for lo, hi, base in MAP: if base <= off <= base + (hi - lo): out.append(lo + (off - base)) return out def primary_va(off): """The most useful VA for an offset: physical 0xA0000000+ / xex 0x80000000+.""" vas = off_to_vas(off) return vas[0] if vas else None def mem_path(): # A snapshot (`cp --sparse=always /dev/shm/xenia_memory_* snap.bin`, ~2 s) # reads identically and does not contend with the running emulator, which # pegs every core under lavapipe. Point $GMEM_FILE at one to work offline. env = os.environ.get("GMEM_FILE") if env: return env if len(sys.argv) > 1 and sys.argv[1].startswith("/dev/shm/"): return sys.argv.pop(1) cands = [f"/dev/shm/{n}" for n in os.listdir("/dev/shm") if n.startswith("xenia_memory_")] if not cands: sys.exit("no /dev/shm/xenia_memory_* — is Canary running?") if len(cands) > 1: sys.exit(f"several memory files, pass one explicitly: {cands}") return cands[0] def extents(fd, size): """Yield (start, end) of the file's allocated (non-hole) ranges.""" pos = 0 while pos < size: try: data = os.lseek(fd, pos, os.SEEK_DATA) except OSError: return try: hole = os.lseek(fd, data, os.SEEK_HOLE) except OSError: hole = size yield (data, hole) pos = hole def parse_pattern(s): if s.startswith("hex:"): return bytes.fromhex(s[4:]) return s.encode("latin-1") def cmd_find(f, size, args): pat = parse_pattern(args[0]) limit = int(args[1]) if len(args) > 1 else 64 hits = 0 CHUNK = 1 << 24 for start, end in extents(f.fileno(), size): pos = start carry = b"" carry_at = start while pos < end: f.seek(pos) buf = f.read(min(CHUNK, end - pos)) if not buf: break blob = carry + buf base = carry_at for m in re.finditer(re.escape(pat), blob): off = base + m.start() va = primary_va(off) print(f"{off:#013x} va {va:#010x}" if va is not None else f"{off:#013x} va ?") hits += 1 if hits >= limit: return keep = len(pat) - 1 carry = blob[-keep:] if keep else b"" carry_at = base + len(blob) - len(carry) pos += len(buf) if hits == 0: print("(no hits)", file=sys.stderr) def cmd_read(f, size, args): va = int(args[0], 0) n = int(args[1], 0) if len(args) > 1 else 256 f.seek(va_to_off(va)) data = f.read(n) for i in range(0, len(data), 16): row = data[i : i + 16] hexs = " ".join(f"{b:02x}" for b in row) txt = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in row) print(f"{va + i:08x} {hexs:<47} |{txt}|") def cmd_words(f, size, args): va = int(args[0], 0) n = int(args[1], 0) if len(args) > 1 else 32 f.seek(va_to_off(va)) data = f.read(n * 4) for i in range(0, len(data) - 3, 4): (u,) = struct.unpack_from(">I", data, i) (fl,) = struct.unpack_from(">f", data, i) fs = f"{fl:.6g}" if -1e30 < fl < 1e30 else "" print(f"{va + i:08x} +{i:04x} {u:#010x} {u:>12} {fs}") def main(): path = mem_path() if len(sys.argv) < 2: sys.exit(__doc__) cmd, args = sys.argv[1], sys.argv[2:] size = os.path.getsize(path) with open(path, "rb") as f: {"find": cmd_find, "read": cmd_read, "words": cmd_words}[cmd](f, size, args) if __name__ == "__main__": main()