Files
BFM-decomp/tools/main_seed_ends.py
T

148 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""main_seed_ends.py — the EXE's game-code function boundaries, DERIVED from the build (P33 A2, R33).
Emits `0xVRAM NINS` per function (the two-column `--seeds` form tools/sig_image.py accepts, where a
seeded nins is authoritative) for every function in main's GAME-CODE objects:
* the `.text` input sections of the link map (build/us/SLUS_007.26.map) give each object's vram
and size — the same map the byte-identical link wrote, so the numbers are the build's own;
* objects that are LINKED PsyQ subsegs (progress._main_linked_segs_from_makefile — the Makefile's
psyq_integrate stub lists, R33) or that live under build/psyq/ are skipped: Sony's objects are
outside the game-code denominator whether the machine linked the real SDK objects or the
INCLUDE_ASM fallback tiles;
* inside one object `.text` is contiguous code — the interleaved rodata islands and the linked
library blocks sit BETWEEN objects — so slicing at the object's own `nm` function symbols, with
the last function ending at the section size, is exact. No func_end heuristic, no Ghidra.
Why this exists: main's only whole-EXE sig was Ghidra's (.run/sig.SLUS_007.26.jsonl, 2026-08-05),
which a public clone cannot regenerate and whose flow-derived boundary on FUN_80023bf0 is 22 ins
short (P31 S79: the corrected game-code denominator is 41,556, the digest printed 41,534). The old
`make sig-main` seeded from `corpus.py main --seed-ends` = the open STUB list, which is empty at
100% — a sig of nothing.
Coverage assertions (R32 — a silent skip is a defect): every game-code `.text` section is
non-empty and 4-aligned, starts with a function symbol at offset 0, and its functions tile it
exactly; every object the map lists is either counted, LINKED-skipped, or psyq-skipped, and all
three counts are printed with the instruction totals (R41). Any object counted from outside
build/src/ is named, so an unexpected code source cannot slip in unnoticed (R14).
"""
import argparse
import pathlib
import re
import subprocess
import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "tools"))
NM = "mipsel-linux-gnu-nm"
# gas emits these two at .text+0 for every gcc-2.7.2 TU; they are markers, not functions.
NOT_FUNCS = {"__gnu_compiled_c", "gcc2_compiled."}
MAP_TEXT = re.compile(r"^\s*\.text\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+(\S+\.o)\s*$")
MAP_TEXT_WRAPPED = re.compile(r"^\s*\.text\s*$")
MAP_CONT = re.compile(r"^\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+(\S+\.o)\s*$")
def text_sections(map_path):
"""[(vram, size, obj_path)] for every `.text` input section in a GNU ld map. A long object
name makes ld wrap the record onto the next line; both forms are read."""
out, lines = [], map_path.read_text(errors="replace").splitlines()
i = 0
while i < len(lines):
m = MAP_TEXT.match(lines[i])
if m:
out.append((int(m.group(1), 16), int(m.group(2), 16), m.group(3)))
elif MAP_TEXT_WRAPPED.match(lines[i]) and i + 1 < len(lines):
m2 = MAP_CONT.match(lines[i + 1])
if m2:
out.append((int(m2.group(1), 16), int(m2.group(2), 16), m2.group(3)))
i += 1
i += 1
return out
def object_functions(obj_path):
"""[(offset, name)] of the function symbols in an object's .text, sorted, one per offset."""
res = subprocess.run([NM, "-n", "--defined-only", str(obj_path)], capture_output=True, text=True)
if res.returncode != 0:
sys.exit(f"main_seed_ends: {NM} failed on {obj_path}: {res.stderr.strip()}")
seen, funcs = set(), []
for ln in res.stdout.splitlines():
parts = ln.split()
if len(parts) != 3 or parts[1] not in ("T", "t"):
continue
off, name = int(parts[0], 16), parts[2]
if name in NOT_FUNCS or name.startswith((".L", "$L")):
continue
if off in seen: # an alias for the same entry — one function
continue
seen.add(off)
funcs.append((off, name))
funcs.sort()
return funcs
def derive(map_path, repo):
import progress
linked = progress._main_linked_segs_from_makefile()
rows, n_linked, n_psyq, n_code, foreign = [], 0, 0, 0, []
for vram, size, obj in text_sections(map_path):
if size == 0:
continue
stem = pathlib.Path(obj).stem
if obj.startswith("build/psyq/"):
n_psyq += 1
continue
if stem in linked:
n_linked += 1
continue
if size % 4:
sys.exit(f"main_seed_ends: {obj} .text size {size:#x} is not 4-aligned")
p = repo / obj
if not p.exists():
sys.exit(f"main_seed_ends: {obj} listed in the map but absent on disk — rebuild main")
funcs = object_functions(p)
if not funcs or funcs[0][0] != 0:
sys.exit(f"main_seed_ends: {obj} .text does not start with a function symbol "
f"(first symbol at {funcs[0][0] if funcs else 'none'}) — bytes owned by no function")
if not obj.startswith("build/src/"):
foreign.append(obj)
n_code += 1
for i, (off, name) in enumerate(funcs):
end = funcs[i + 1][0] if i + 1 < len(funcs) else size
if end <= off or (end - off) % 4:
sys.exit(f"main_seed_ends: {obj}:{name} bad extent {off:#x}..{end:#x}")
rows.append((vram + off, (end - off) // 4, name))
rows.sort()
addrs = [a for a, _, _ in rows]
if len(set(addrs)) != len(addrs):
sys.exit("main_seed_ends: duplicate vram across objects — the map and the objects disagree")
return rows, n_code, n_linked, n_psyq, foreign
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--map", default="build/us/SLUS_007.26.map", help="the main link map")
ap.add_argument("--out", default=None, help="write `0xVRAM NINS` lines here (default: stdout)")
a = ap.parse_args()
map_path = pathlib.Path(a.map)
if not map_path.is_absolute():
map_path = ROOT / map_path
if not map_path.exists():
sys.exit(f"main_seed_ends: no link map at {map_path} — run `make check BINARY=main` first")
rows, n_code, n_linked, n_psyq, foreign = derive(map_path, ROOT)
text = "".join(f"0x{a:08X} {n}\n" for a, n, _ in rows)
if a.out:
pathlib.Path(a.out).write_text(text)
else:
sys.stdout.write(text)
tot = sum(n for _, n, _ in rows)
print(f"main_seed_ends: {len(rows)} functions / {tot} ins from {n_code} game-code objects "
f"(skipped {n_linked} LINKED subseg objects + {n_psyq} build/psyq objects)"
+ (f"; NOTE code counted from outside build/src/: {', '.join(foreign)}" if foreign else ""),
file=sys.stderr)
if __name__ == "__main__":
main()