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
105 lines
4.4 KiB
Python
105 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Link every used object of a PsyQ library and byte-verify each against the BFM EXE.
|
|
|
|
Driver over `psyq_identify.py` (placement) + `psyq_link.link_object` (per-object byte link).
|
|
For a library's ELF-object dir it:
|
|
1. runs psyq_identify to get {object: text_vram} for the objects the EXE actually links,
|
|
2. links each object at its vram, recovering externals from the EXE relocations,
|
|
3. byte-compares each `.text`, prints a PASS/FAIL table + summary,
|
|
4. aggregates recovered externals across objects and flags any inconsistency (the same
|
|
symbol recovered to two addresses = a bug to investigate),
|
|
5. writes a JSON report to .run/psyq_link.<lib>.json (consumed by the build-wiring step 2'.3).
|
|
|
|
Usage: psyq_link_lib.py <elf_dir> [text_lo text_hi] e.g. .run/obj40/libcd
|
|
"""
|
|
import json, os, re, subprocess, sys
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from psyq_link import link_object # noqa: E402
|
|
|
|
|
|
def placement(elf_dir, lo, hi, vram_base, exe):
|
|
cmd = ["python3", "tools/psyq_identify.py", elf_dir]
|
|
if lo and hi:
|
|
cmd += [lo, hi]
|
|
cmd += ["--vram-base", hex(vram_base), "--exe", exe]
|
|
out = subprocess.check_output(cmd, text=True)
|
|
placed = {}
|
|
for ln in out.splitlines():
|
|
m = re.match(r"\s+0x([0-9A-Fa-f]+)\s+(\S+\.o)\s+\((\d+) ins\)", ln)
|
|
if m:
|
|
placed[m.group(2)] = int(m.group(1), 16)
|
|
return placed, out
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("elf_dir")
|
|
ap.add_argument("window", nargs="*", help="optional scan-narrowing window: text_lo text_hi")
|
|
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")
|
|
args = ap.parse_args()
|
|
elf_dir = args.elf_dir
|
|
lo = args.window[0] if len(args.window) > 0 else None
|
|
hi = args.window[1] if len(args.window) > 1 else None
|
|
vram_base = int(args.vram_base, 0)
|
|
lib = os.path.basename(elf_dir.rstrip("/"))
|
|
|
|
placed, idout = placement(elf_dir, lo, hi, vram_base, args.exe)
|
|
print(idout.strip())
|
|
print(f"\n=== linking {len(placed)} located {lib} objects ===")
|
|
|
|
exe = open(args.exe, "rb").read()
|
|
results, all_ext = [], {}
|
|
conflicts = []
|
|
npass = 0
|
|
for name, vram in sorted(placed.items(), key=lambda kv: kv[1]):
|
|
obj = os.path.join(elf_dir, name)
|
|
r = link_object(obj, vram, name=name, exe_bytes=exe, vram_base=vram_base)
|
|
results.append(r)
|
|
tag = "PASS" if r["ok"] else "FAIL"
|
|
if r["ok"]:
|
|
npass += 1
|
|
extra = ""
|
|
if not r["ok"]:
|
|
extra = " " + (r.get("error") or f"{r.get('ndiff','?')} word(s) diff @ "
|
|
f"{hex(r['first_diff']) if r.get('first_diff') else '(size)'}")
|
|
print(f" [{tag}] 0x{vram:08X} {name:12s} ({r['tsize']:5d} B){extra}")
|
|
for s, a in r["externals"].items():
|
|
if s in all_ext and all_ext[s] != a:
|
|
conflicts.append((s, all_ext[s], a, name))
|
|
all_ext.setdefault(s, a)
|
|
|
|
print(f"\n{npass}/{len(results)} objects byte-identical (.text)")
|
|
if conflicts:
|
|
print(" !! external address conflicts:")
|
|
for s, a0, a1, nm in conflicts:
|
|
print(f" {s}: 0x{a0:08X} vs 0x{a1:08X} (at {nm})")
|
|
print(f" {len(all_ext)} distinct externals recovered across the library")
|
|
|
|
rep = {
|
|
"lib": lib,
|
|
"elf_dir": elf_dir,
|
|
"objects": [
|
|
{"name": r["name"], "text_vram": r["text_vram"], "tsize": r["tsize"],
|
|
"rdata_vram": r["rdata_vram"] if isinstance(r["rdata_vram"], int) else None,
|
|
"data_vram": r["data_vram"] if isinstance(r["data_vram"], int) else None,
|
|
"ok": r["ok"], "externals": {s: a for s, a in r["externals"].items()},
|
|
"unrecovered": r["unrecovered"]}
|
|
for r in results
|
|
],
|
|
"externals_union": all_ext,
|
|
"conflicts": [{"sym": s, "a": a0, "b": a1, "at": nm} for s, a0, a1, nm in conflicts],
|
|
"npass": npass, "ntotal": len(results),
|
|
}
|
|
os.makedirs(".run", exist_ok=True)
|
|
path = f".run/psyq_link.{lib}.json"
|
|
json.dump(rep, open(path, "w"), indent=1)
|
|
print(f" report -> {path}")
|
|
sys.exit(0 if npass == len(results) else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|