re(challenge): the part id is never persisted -- differential search says stack only
No literal 26 exists anywhere, so the GamePart id is computed. That does not stop it being found: the id is KNOWN at each screen from the GamePart table (EXTRAS = 5, MISSION SELECT = 7), so snapshot both and intersect. New tool diff_words.py does the classic differential search over the sparse guest image, and find_partslot.sh drives the two screens and runs it. Result: 171 MB scanned, exactly 4 addresses read 5 then 7 -- 0x708FFBEC, 0x708FFCBC, 0x708FFDAC, 0x708FFE20 -- and all four are guest STACK (the same run's log puts thread stacks at 0x709...). So the requested part id exists only as a stack argument in flight; there is no persistent field, which is consistent with finding no literal store, and means there is nothing stable to poke. That closes the last memory-and-menu route to the challenge missions. Reaching them needs either the genuine in-game unlock (an in-mission attainment, per AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE) or an emulator-side hook that forces the transition -- a code change, not a poke. diff_words.py is worth keeping well beyond this question: it locates any field whose address is unknown but whose value is known at two moments.
This commit is contained in:
74
tools/re-capture/diff_words.py
Executable file
74
tools/re-capture/diff_words.py
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find guest addresses holding one value in snapshot A and another in snapshot B.
|
||||
|
||||
The classic differential search, which is how you locate a field whose ADDRESS is
|
||||
unknown but whose VALUE is known at two moments. Used here for the GamePart slot:
|
||||
`GP_EXTRAS` = 5 on the extras screen, `GP_MISSION_SELECT` = 7 on the next one, so
|
||||
the slot is a word that reads 5 then 7.
|
||||
|
||||
Both snapshots are sparse copies of `/dev/shm/xenia_memory_*`, so the ~4.6 GB of
|
||||
holes cost nothing: walk A's allocated extents with SEEK_DATA and only look at B
|
||||
where A already matched.
|
||||
|
||||
diff_words.py <snapA> <valA> <snapB> <valB> [--near <va> <span>]
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from gmem import off_to_vas # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 5:
|
||||
print(__doc__)
|
||||
return 1
|
||||
pa, va_, pb, vb_ = sys.argv[1], int(sys.argv[2], 0), sys.argv[3], int(sys.argv[4], 0)
|
||||
pat_a = struct.pack(">I", va_)
|
||||
fa, fb = open(pa, "rb"), open(pb, "rb")
|
||||
size = min(os.path.getsize(pa), os.path.getsize(pb))
|
||||
|
||||
hits, scanned = [], 0
|
||||
off = 0
|
||||
while off < size:
|
||||
try:
|
||||
off = os.lseek(fa.fileno(), off, os.SEEK_DATA)
|
||||
except OSError:
|
||||
break
|
||||
if off >= size:
|
||||
break
|
||||
try:
|
||||
end = min(os.lseek(fa.fileno(), off, os.SEEK_HOLE), size)
|
||||
except OSError:
|
||||
end = size
|
||||
while off < end:
|
||||
n = min(1 << 22, end - off)
|
||||
fa.seek(off)
|
||||
buf = fa.read(n)
|
||||
scanned += len(buf)
|
||||
i = 0
|
||||
while True:
|
||||
i = buf.find(pat_a, i)
|
||||
if i < 0:
|
||||
break
|
||||
p = off + i
|
||||
if p % 4 == 0:
|
||||
fb.seek(p)
|
||||
if fb.read(4) == struct.pack(">I", vb_):
|
||||
hits.append(p)
|
||||
i += 1
|
||||
off += n
|
||||
|
||||
print(f"scanned {scanned/1e6:.0f} MB of allocated guest memory")
|
||||
print(f"addresses reading {va_} in A and {vb_} in B: {len(hits)}")
|
||||
for h in hits[:40]:
|
||||
vas = off_to_vas(h)
|
||||
print(f" file off {h:#012x} guest VA {[hex(v) for v in vas][:2]}")
|
||||
if len(hits) > 40:
|
||||
print(f" … and {len(hits)-40} more")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user