mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
1b9397c462
Every binary-specific value is now a REQUIRED parameter — the phase's #1-risk mitigation (no EXE default an overlay could silently inherit): - psyq_link.py: removed module EXE/VRAM_BASE; recover_sym_addrs/unique_byte_vram vram_base now positional-required; link_object vram_base keyword-required (*,...); CLI --vram-base/--exe required - psyq_identify.py: removed EXE/VRAM_BASE globals; --vram-base/--exe required - psyq_link_region.py + psyq_link_lib.py: dropped 'VRAM_BASE' import + EXE; defaults removed (placement/classify/build_region required); CLI required - psyq_integrate.py: dropped VRAM_BASE import + EXE; integrate() vram_base/exe_path/ symbols_path keyword-required; CLI --vram-base/--exe/--symbols required - PROOFS: psyq_link bare -> 'required: --vram-base, --exe' (loud); build with AND without SDK -> 143dbb89; wrong base -> cae22f7e; curated dirs regenerate identical
102 lines
4.3 KiB
Python
102 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Locate where PsyQ library objects are linked in the target EXE.
|
|
|
|
For each ELF .o (converted from a PsyQ .LIB member), extract its `.text` and the
|
|
relocation offsets, build a relocation-masked word pattern (relocated immediate
|
|
fields zeroed), and scan the EXE text for the single position where every
|
|
NON-relocated word matches. That position is the object's link address in the EXE
|
|
(or "absent" if the EXE doesn't link it). This is the placement map the library
|
|
linker step consumes.
|
|
|
|
Usage: psyq_identify.py <elf_dir> [text_lo_vram text_hi_vram] [--vram-base HEX] [--exe PATH]
|
|
(window defaults to the BFM .text 0x80010000..0x800629DC; --vram-base/--exe default to
|
|
the EXE's values, becoming required in Phase-9 T8 once every caller passes them)
|
|
"""
|
|
import struct, subprocess, re, sys, glob, os, argparse
|
|
|
|
# Phase 9: --vram-base (fileoff->vram delta) and --exe are REQUIRED (no EXE default an overlay
|
|
# could silently inherit). The optional [lo hi] window only narrows the placement scan.
|
|
_ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
_ap.add_argument("elf_dir", nargs="?", default=".run/obj40/libcd")
|
|
_ap.add_argument("window", nargs="*", help="optional scan-narrowing window: text_lo_vram text_hi_vram")
|
|
_ap.add_argument("--vram-base", required=True,
|
|
help="fileoff->vram delta of the target binary (e.g. the EXE's 0x8000F800)")
|
|
_ap.add_argument("--exe", required=True, help="target binary path")
|
|
_a = _ap.parse_args()
|
|
ELF_DIR = _a.elf_dir
|
|
EXE = _a.exe
|
|
VRAM_BASE = int(_a.vram_base, 0)
|
|
TLO = int(_a.window[0], 0) if len(_a.window) > 0 else 0x80010000
|
|
THI = int(_a.window[1], 0) if len(_a.window) > 1 else 0x800629DC
|
|
|
|
b = open(EXE, "rb").read()
|
|
text = b[TLO - VRAM_BASE: THI - VRAM_BASE]
|
|
twords = [struct.unpack_from("<I", text, i)[0] for i in range(0, len(text), 4)]
|
|
|
|
def obj_text_pattern(o):
|
|
"""Return (words, mask) for the object's .text; mask[i]=0 on relocated/jump words.
|
|
|
|
A data-only object (no `.text` section — e.g. libgs GLOBAL.o, which defines only
|
|
globals) makes `objdump -j .text` exit non-zero; treat that as an empty .text so the
|
|
caller's `no-.text` path handles it instead of crashing."""
|
|
p = subprocess.run(["mipsel-linux-gnu-objdump", "-dr", "-j", ".text", o],
|
|
capture_output=True, text=True)
|
|
if p.returncode != 0:
|
|
return [], []
|
|
d = p.stdout
|
|
words, mask = [], []
|
|
pending_reloc = False
|
|
for line in d.splitlines():
|
|
mi = re.match(r"\s+([0-9a-f]+):\s+([0-9a-f]{8})\s", line)
|
|
if mi:
|
|
w = int(mi.group(2), 16)
|
|
words.append(w)
|
|
# mask jal/j (opcode 2/3) always (R_MIPS_26 target is link-resolved)
|
|
mask.append(0 if (w >> 26) in (2, 3) else 0xFFFFFFFF)
|
|
elif "R_MIPS" in line and words:
|
|
# relocation annotation follows its instruction line -> mask low 16 (hi/lo/pc16)
|
|
if "_26" in line:
|
|
mask[-1] = 0
|
|
else:
|
|
mask[-1] = 0xFFFF0000
|
|
return words, mask
|
|
|
|
def find(words, mask):
|
|
n = len(words)
|
|
if n < 2:
|
|
return None # too short to anchor uniquely
|
|
hits = []
|
|
for s in range(0, len(twords) - n + 1):
|
|
ok = True
|
|
for i in range(n):
|
|
if (twords[s + i] & mask[i]) != (words[i] & mask[i]):
|
|
ok = False; break
|
|
if ok:
|
|
hits.append(s)
|
|
if len(hits) > 1:
|
|
break
|
|
if len(hits) == 1:
|
|
return TLO + hits[0] * 4
|
|
return ("ambiguous" if len(hits) > 1 else None)
|
|
|
|
results = []
|
|
for o in sorted(glob.glob(os.path.join(ELF_DIR, "*.o"))):
|
|
words, mask = obj_text_pattern(o)
|
|
if not words:
|
|
results.append((os.path.basename(o), "no-.text", len(words))); continue
|
|
r = find(words, mask)
|
|
results.append((os.path.basename(o), r, len(words)))
|
|
|
|
found = [(n, a, l) for n, a, l in results if isinstance(a, int)]
|
|
found.sort(key=lambda x: x[1])
|
|
print(f"{ELF_DIR}: {len(found)}/{len(results)} objects located in EXE text "
|
|
f"[0x{TLO:X}..0x{THI:X}]")
|
|
for n, a, l in found:
|
|
print(f" 0x{a:08X} {n:18s} ({l} ins)")
|
|
absent = [n for n, a, l in results if a is None]
|
|
amb = [n for n, a, l in results if a == "ambiguous"]
|
|
if absent:
|
|
print(f" not linked by EXE ({len(absent)}): {', '.join(absent[:12])}{' …' if len(absent)>12 else ''}")
|
|
if amb:
|
|
print(f" ambiguous ({len(amb)}): {', '.join(amb)}")
|