re: the base-solver -- 277 name-block loaders indexed, and the analog block is SOLVED
New reusable tool, tools/re-capture/name_block_bases.py -> docs/re/data/name-block-bases.txt (2880 lines, ~65 s, byte-identical across two runs). A loader that reads a table by field name keeps one base pointer and emits "addi rX, rBASE, -N" per name, so no static xref sees the strings. Solve the base from the DISPLACEMENT SET alone: every (string address, displacement) pair implies a candidate base, and the true base collects a vote from every name it explains, so it wins outright. My first cut took candidates from ONE displacement and scored the unit loader at 52/226 against the right answer's 217/226 -- vote over the whole set, not a probe. Control passes with no prior knowledge: the tool recovers sub_82341A20 -> r30 = 0x82088F94 at 217/226, and independently recovers sub_8230D1F8 (129/132), sub_822F9498 (90/91) and sub_822AE628 (81/108). 277 name-block-reading functions image-wide, with the schema each names. The analog block is SOLVED: sub_821A6CF0, r29 = 0x820A1630, 22/24. In code order it names ControlTweakName, YawMagForNormal, the 12 Tweak fields, the 8 AnalogRevice_* curves and GP_MAIN_GAME -- the whole schema in the object's own order plus its pak. r29 is built at 0x821A6D34 as addi r29, r11, 5680 = 0x820A0000 + 5680, matching the solved base exactly. It is the same function that reads PlayerParams. Two of my own verdicts withdrawn: "referenced by nothing" and the softened "not found by these routes". The measurements behind them were right; the conclusions were wrong. The base was solvable from the data the whole time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
108
tools/re-capture/name_block_bases.py
Normal file
108
tools/re-capture/name_block_bases.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Solve the BASE of every register-relative name block, and print the schema it names.
|
||||
|
||||
A loader that reads a table by field name usually does not take each string's
|
||||
address directly -- it keeps one base pointer and emits `addi rX, rBASE, -N` per
|
||||
name. No static xref sees that, so those blocks look unreferenced.
|
||||
|
||||
This solves the base from the DISPLACEMENT SET alone: for a group of `addi`s
|
||||
sharing a source register, try every candidate base implied by (string address -
|
||||
displacement) and keep the one that lands the most displacements on a string
|
||||
start. Control: it must recover `r30 = 0x82088F94` for the unit loader
|
||||
`sub_82341A20` with no prior knowledge.
|
||||
|
||||
Regenerates docs/re/data/name-block-bases.txt.
|
||||
"""
|
||||
import sys, collections, bisect
|
||||
|
||||
DB = '/work/xenia-rs/sylpheed.db'
|
||||
MIN_GROUP = 8 # displacements needed before a group is worth solving
|
||||
MIN_RESOLVED = 12 # report a function only if the base explains this many
|
||||
LOW, HIGH = 0x82000000, 0x82400000
|
||||
|
||||
def main():
|
||||
import duckdb
|
||||
con = duckdb.connect(DB, read_only=True)
|
||||
S = set(a for (a,) in con.execute("SELECT address FROM strings").fetchall())
|
||||
txt = dict(con.execute("SELECT address, content FROM strings").fetchall())
|
||||
funcs = con.execute(
|
||||
"SELECT address,end_address,name FROM functions ORDER BY address").fetchall()
|
||||
starts = [f[0] for f in funcs]
|
||||
ins = con.execute(
|
||||
"SELECT address,operands FROM instructions WHERE mnemonic='addi' ORDER BY address"
|
||||
).fetchall()
|
||||
|
||||
print("# Register-relative name blocks: the base solved from the displacements")
|
||||
print("# Regenerate: python3 tools/re-capture/name_block_bases.py")
|
||||
print("# See docs/re/structures/player-tuning-tables.md")
|
||||
print("\n strings in the image: %d addi instructions: %d" % (len(S), len(ins)))
|
||||
|
||||
byf = collections.defaultdict(lambda: collections.defaultdict(list))
|
||||
order = collections.defaultdict(lambda: collections.defaultdict(list))
|
||||
for a, o in ins:
|
||||
i = bisect.bisect_right(starts, a) - 1
|
||||
if i < 0 or funcs[i][1] <= a:
|
||||
continue
|
||||
p = [x.strip() for x in o.split(',')]
|
||||
if len(p) != 3 or p[0] == p[1]:
|
||||
continue
|
||||
try:
|
||||
d = int(p[2], 0)
|
||||
except ValueError:
|
||||
continue
|
||||
byf[funcs[i][2]][p[1]].append(d)
|
||||
order[funcs[i][2]][p[1]].append((a, d))
|
||||
|
||||
rows = []
|
||||
for name in sorted(byf):
|
||||
for reg in sorted(byf[name]):
|
||||
D = sorted(set(byf[name][reg]))
|
||||
if len(D) < MIN_GROUP or reg == 'r1': # r1 is the stack pointer
|
||||
continue
|
||||
# Vote: every (string, displacement) pair implies one candidate base.
|
||||
# The true base collects a vote from each name it explains, so it wins
|
||||
# outright -- taking candidates from ONE displacement misses it.
|
||||
cand = collections.Counter()
|
||||
for A in S:
|
||||
for d in D:
|
||||
B = A - d
|
||||
if LOW <= B <= HIGH:
|
||||
cand[B] += 1
|
||||
best = None
|
||||
# sorted, not most_common(): a Counter's tie order varies per run
|
||||
top = sorted(cand.items(), key=lambda kv: (-kv[1], kv[0]))[:40]
|
||||
for B, _v in top:
|
||||
tot = sum(1 for d in D if (B + d) in S)
|
||||
if best is None or tot > best[1] or (tot == best[1] and B < best[0]):
|
||||
best = (B, tot)
|
||||
if best and best[1] >= MIN_RESOLVED:
|
||||
rows.append((name, reg, best[0], best[1], len(D)))
|
||||
|
||||
rows.sort(key=lambda r: (-r[3], r[0]))
|
||||
print("\n## %d functions read a name block through a base register" % len(rows))
|
||||
print(" %-18s %-5s %-12s %s" % ('function', 'reg', 'base', 'resolved / displacements'))
|
||||
for name, reg, B, tot, n in rows:
|
||||
print(" %-18s %-5s 0x%08X %d / %d" % (name, reg, B, tot, n))
|
||||
|
||||
print("\n## The schema each one names, in code order")
|
||||
for name, reg, B, tot, n in rows:
|
||||
seen, names = set(), []
|
||||
for a, d in order[name][reg]:
|
||||
t = B + d
|
||||
if t in txt and t not in seen:
|
||||
seen.add(t)
|
||||
names.append(txt[t])
|
||||
if not names:
|
||||
continue
|
||||
print("\n %s (%s, base 0x%08X) names %d:" % (name, reg, B, len(names)))
|
||||
line = " "
|
||||
for nm in names:
|
||||
if len(line) + len(nm) > 96:
|
||||
print(line)
|
||||
line = " "
|
||||
line += nm + " "
|
||||
if line.strip():
|
||||
print(line)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user