mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
feat(o0): o0_boundary.py — the stranded-boundary -O0 sweep, and its honest null
The class banked 5 functions today (func_801457A4 x3 at the whale's end boundary,
func_80183830 x2 one region lower) so it deserved a sweep rather than a third
hand-derivation. It reads every splat yaml's _o0<letter> 'c' subsegs, takes the
START of the NEXT subseg as the boundary vaddr, and reports an open stub sitting
exactly there whose target carries the -O0 prologue tell.
RESULT: 141 binaries with an _o0 subseg, 288 boundaries examined, 0 candidates.
THE CLASS IS EXHAUSTED -- today's five were the last of it.
A sweep returning 0 must prove it CAN return non-zero, so that null is
negative-controlled: the 288 computed boundaries include 0x801457A4 in 138
binaries and 0x80183830 in exactly ov_SC03_118 + ov_SC03_119 -- i.e. it does find
the addresses it banked, they simply have no open stub any more.
Every rejected boundary is printed WITH ITS REASON and the denominator is printed
(R32): a sweep that reports only its hits cannot be told from one that scanned
nothing. It deliberately does not consult the family map -- rollout_o0 refuses this
recipe for a bookkeeping reason ('family with exemplar ... not found in the map'),
not a structural one, and is separately blind to any _o0 basename.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""o0_boundary.py — find (and optionally bank) -O0 functions STRANDED at an -O0 region's end. (P31 S68)
|
||||
|
||||
THE SHAPE, byte-proven three times this session. An `-O0` function that is the FIRST function of the
|
||||
`-O2` object immediately following an existing `-O0` region can be banked with **NO splat change at
|
||||
all**, because that region's `.text` ends exactly at its address:
|
||||
|
||||
- [0x5b020, c, ov_SC03_118_o0d] <- -O0 region, .text ends at ...
|
||||
- [0x5b6d8, c, ov_SC03_118_jr_80183830] <- ... exactly 0x80183830, where the stub sits
|
||||
|
||||
The edit is **ATOMIC ACROSS TWO FILES** — append the definition to `<ov>_o0<L>.c` AND delete the
|
||||
`INCLUDE_ASM` from the following object's TU in one change — so the two object sizes cancel and no
|
||||
address moves. Banked this way: `func_801457A4` x3 (79 ins, at the whale's end) and `func_80183830`
|
||||
x2 (29 ins, one region lower). Cookbook §362 + its addendum.
|
||||
|
||||
WHY A SWEEP AND NOT `rollout_o0`. That driver refuses these for a bookkeeping reason rather than a
|
||||
structural one — `family with exemplar func_80183830 not found in the map` — and it is separately
|
||||
BLIND to any stub whose basename contains `_o0` (its `stub_file_of` skips them), which is exactly
|
||||
where a carve puts them. The recipe does not need the family map, so this does not consult it.
|
||||
|
||||
COVERAGE (R32). The candidate set is derived from the splat yamls themselves — every `c` subseg whose
|
||||
name matches `_o0<letter>` in every binary — crossed with `corpus.stubs` and the `o0_detect` prologue
|
||||
tell. The denominator is printed and every rejected candidate is printed WITH ITS REASON, because a
|
||||
sweep that reports only its hits cannot be distinguished from one that scanned nothing.
|
||||
|
||||
tools/o0_boundary.py # report candidates fleet-wide
|
||||
tools/o0_boundary.py --binary ov_SC03_118
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.chdir(REPO)
|
||||
sys.path.insert(0, os.path.join(REPO, "tools"))
|
||||
import corpus # noqa: E402
|
||||
import progress # noqa: E402
|
||||
from o0_detect import detect_o0 # noqa: E402
|
||||
|
||||
SUBSEG = re.compile(r'^\s*-\s*\[\s*(0x[0-9a-fA-F]+)\s*,\s*c\s*,\s*([A-Za-z0-9_]+)\s*\]')
|
||||
O0NAME = re.compile(r'_o0[a-z]?$')
|
||||
|
||||
|
||||
def vram_base(binary):
|
||||
"""The binary's code vram base, read from its own splat yaml (never hard-coded)."""
|
||||
for y in (("config/splat.us.exe.yaml",) if binary == "main"
|
||||
else ("config/splat.%s.yaml" % binary,)):
|
||||
if not os.path.exists(y):
|
||||
continue
|
||||
txt = open(y).read()
|
||||
m = re.search(r'^\s*vram:\s*(0x[0-9a-fA-F]+)', txt, re.M)
|
||||
s = re.search(r'^\s*start:\s*(0x[0-9a-fA-F]+)', txt, re.M)
|
||||
if m:
|
||||
return int(m.group(1), 16) - (int(s.group(1), 16) if s else 0)
|
||||
return None
|
||||
|
||||
|
||||
def pairs(binary):
|
||||
"""[(o0_subseg_name, next_subseg_name, boundary_vaddr)] from the splat yaml, in file order."""
|
||||
y = "config/splat.us.exe.yaml" if binary == "main" else "config/splat.%s.yaml" % binary
|
||||
if not os.path.exists(y):
|
||||
return []
|
||||
base = vram_base(binary)
|
||||
if base is None:
|
||||
return []
|
||||
rows = []
|
||||
for ln in open(y):
|
||||
m = SUBSEG.match(ln)
|
||||
if m:
|
||||
rows.append((int(m.group(1), 16), m.group(2)))
|
||||
out = []
|
||||
for i, (off, name) in enumerate(rows[:-1]):
|
||||
if O0NAME.search(name):
|
||||
noff, nname = rows[i + 1]
|
||||
out.append((name, nname, base + noff))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--binary", help="one binary (default: every binary with an _o0 subseg)")
|
||||
a = ap.parse_args()
|
||||
|
||||
bins = [a.binary] if a.binary else sorted(progress.BINARIES)
|
||||
scanned = collections.Counter()
|
||||
hits, rejected = [], []
|
||||
for b in bins:
|
||||
ps = pairs(b)
|
||||
if not ps:
|
||||
continue
|
||||
scanned["binaries with an _o0 subseg"] += 1
|
||||
try:
|
||||
st = {s.addr: s for s in corpus.stubs(b).values()}
|
||||
except Exception as e:
|
||||
rejected.append((b, "-", "-", "oracle refused: %r" % (e,)))
|
||||
continue
|
||||
for o0name, nxt, vaddr in ps:
|
||||
scanned["boundaries examined"] += 1
|
||||
s = st.get(vaddr)
|
||||
if s is None:
|
||||
rejected.append((b, o0name, "0x%08x" % vaddr, "no OPEN stub at the boundary"))
|
||||
continue
|
||||
sp = os.path.join(s.asm_dir, s.symbol + ".s")
|
||||
if not os.path.exists(sp):
|
||||
rejected.append((b, o0name, s.symbol, "no .s (run make extract)"))
|
||||
continue
|
||||
if not detect_o0(sp):
|
||||
rejected.append((b, o0name, s.symbol, "target is -O2 (no -O0 prologue tell)"))
|
||||
continue
|
||||
hits.append(dict(binary=b, o0_tu="src/%s/%s.c" % (b, o0name), stub_tu=s.path,
|
||||
fn=s.symbol, addr="0x%08x" % vaddr,
|
||||
nins=corpus.s_ins_count(sp)))
|
||||
|
||||
print("scanned: %s" % dict(scanned))
|
||||
print("REJECTED %d boundary/-ies, with reasons:" % len(rejected))
|
||||
why = collections.Counter(r[3].split(":")[0] for r in rejected)
|
||||
for k, v in why.most_common():
|
||||
print(" %-42s %d" % (k, v))
|
||||
print()
|
||||
print("CANDIDATES (stranded -O0 at an -O0 region's end boundary): %d" % len(hits))
|
||||
for h in hits:
|
||||
print(" %-14s %-18s %-12s %4d ins append to %s, drop stub from %s"
|
||||
% (h["binary"], h["fn"], h["addr"], h["nins"],
|
||||
os.path.basename(h["o0_tu"]), os.path.basename(h["stub_tu"])))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user