Files
Everquest-Decomp-Recomp/tools/eq.py
T
SalastilandClaude Opus 5.5 9f4e6666b3 Annotation-driven symbols, EH-aware matching, hash-checked link; spell-point TU matched
- // FUNCTION: / GLOBAL: / LIBRARY: annotations in source name symbols, so
  decompiling never needs edits to the shared symbols.txt
- check follows compiler-generated EH handlers into .text$x and verifies
  string literals / float constants by content
- link rebuilds eqgame.exe from compiled objects with relocations resolved
  through the map and checks the SHA1 (currently identical)
- Ghidra pseudo-C export script; docs/ROADMAP.md for the full-relink plan
- /GS confirmed; 0x401022-0x4013e1 (13 functions, spell casting particle
  points) match byte-for-byte

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 22:46:21 -04:00

684 lines
27 KiB
Python

#!/usr/bin/env python3
"""EverQuest (Titanium, eqgame.exe 2005-10-31) matching-decompilation toolkit.
Subcommands:
verify check orig/eqgame.exe is the expected build
import-ghidra FILE seed config/eqgame/symbols.txt from tools/ghidra/ExportFunctions.java output
build compile src/ with MSVC 7.1 (Wine) via ninja
check [NAME|ADDR...] compare compiled functions against the original, byte for byte
diff NAME|ADDR side-by-side disassembly of original vs compiled
progress matched bytes / functions
link assemble build/eqgame.exe from compiled objects + original, compare sha1
map ADDR NAME [KIND] name (or rename) a symbol in the map
todo [N] list unmatched functions, smallest first
"""
import argparse
import difflib
import fnmatch
import hashlib
import os
import re
import struct
import subprocess
import sys
import yaml
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from binfmt import (CoffObject, PEImage, REL_I386_DIR32, REL_I386_REL32, # noqa: E402
IMAGE_SCN_CNT_CODE)
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CFG_DIR = os.path.join(ROOT, "config", "eqgame")
BUILD = os.path.join(ROOT, "build")
OBJ_DIR = os.path.join(BUILD, "obj")
def load_config():
with open(os.path.join(CFG_DIR, "config.yaml")) as f:
return yaml.safe_load(f)
# ---------------------------------------------------------------- symbol map
class SymbolMap:
"""config/eqgame/symbols.txt: `ADDR SIZE KIND NAME` per line, '#' comments.
KIND: fn (game code), lib (CRT/third-party, not decompiled), thunk, data.
"""
def __init__(self, path):
self.path = path
self.entries = {} # addr -> [size, kind, name]
if os.path.exists(path):
with open(path) as f:
for line in f:
line = line.split("#", 1)[0].split()
if len(line) >= 4:
self.entries[int(line[0], 16)] = [int(line[1], 16), line[2], line[3]]
self.base = {a: list(e) for a, e in self.entries.items()}
self._reindex()
def _reindex(self):
self.by_name = {e[2]: a for a, e in self.entries.items()}
def lookup(self, key):
"""Resolve a name or hex address to an address (or None)."""
if key in self.by_name:
return self.by_name[key]
try:
a = int(key, 16)
except ValueError:
return None
return a if a in self.entries else None
def set(self, addr, name, kind=None, size=None):
old = self.by_name.get(name)
if old is not None and old != addr:
raise ValueError(f"{name} already mapped at 0x{old:08x}")
e = self.entries.setdefault(addr, [0, kind or "fn", name])
e[2] = name
if kind:
e[1] = kind
if size is not None:
e[0] = size
self._reindex()
def save(self):
"""Write the map. Names that come from source annotations stay in the source."""
for a, (name, kind) in getattr(self, "annotated", {}).items():
if a in self.base:
self.entries[a] = list(self.base[a])
else:
del self.entries[a]
with open(self.path, "w") as f:
f.write("# eqgame.exe symbol map: ADDR SIZE KIND NAME\n")
f.write("# KIND = fn | lib | thunk | data. Names are MSVC-mangled once decompiled.\n")
for a in sorted(self.entries):
size, kind, name = self.entries[a]
f.write(f"0x{a:08x} 0x{size:x} {kind} {name}\n")
# ---------------------------------------------------------------- commands
def cmd_verify(args):
cfg = load_config()
path = os.path.join(ROOT, cfg["original"])
if not os.path.exists(path):
sys.exit(f"missing {cfg['original']}: copy or symlink your eqgame.exe there")
with open(path, "rb") as f:
h = hashlib.sha1(f.read()).hexdigest()
if h != cfg["sha1"]:
sys.exit(f"{cfg['original']}: sha1 {h} != expected {cfg['sha1']} (wrong client build)")
print(f"{cfg['original']}: OK ({h})")
def cmd_import_ghidra(args):
m = SymbolMap(os.path.join(CFG_DIR, "symbols.txt"))
added = 0
with open(args.file) as f:
for line in f:
addr, size, name, kind = line.split()
addr, size = int(addr, 16), int(size, 16)
if addr in m.entries:
m.entries[addr][0] = m.entries[addr][0] or size
continue
if name.startswith("FUN_"):
name = f"FUN_{addr:08x}"
elif kind != "lib":
kind = "fn"
name = re.sub(r"[^\w@?$<>:.~-]", "_", name)
if name in m.by_name:
name = f"{name}_{addr:08x}"
m.entries[addr] = [size, kind, name]
m.by_name[name] = addr
added += 1
m.save()
print(f"imported {added} functions ({len(m.entries)} total)")
ANNOT = re.compile(r"^\s*//\s*(FUNCTION|GLOBAL|LIBRARY):\s*EQGAME\s+(0x[0-9a-fA-F]+)\s*(\S+)?\s*(NONMATCHING)?")
def mangled_prefixes(qualname):
"""Possible MSVC mangled-name prefixes for `A::B::f` / `X::X` / `X::~X` / `f` / `g_var`."""
parts = [p for p in qualname.split("::") if p]
name, scopes = parts[-1], parts[:-1][::-1]
scope = "".join(p + "@" for p in scopes)
if scopes and name == scopes[0]:
return [f"??0{scope}@"]
if scopes and name == "~" + scopes[0]:
return [f"??1{scope}@"]
# unqualified: a free function/global, or a member declared inside its class body
return [f"?{name}@{scope}@"] if scopes else [f"?{name}@", f"_{name}"]
def parse_annotations(path):
"""[(kind, addr, explicit_mangled_or_None, qualname_or_None, nonmatching)] in file order."""
with open(path, encoding="latin-1") as f:
lines = f.read().splitlines()
out = []
for i, line in enumerate(lines):
m = ANNOT.match(line)
if not m:
continue
kind, addr, explicit, nonmatch = m.group(1), int(m.group(2), 16), m.group(3), bool(m.group(4))
if explicit == "NONMATCHING":
explicit, nonmatch = None, True
qual = None
if not explicit and kind != "LIBRARY":
# the declaration follows (possibly after more annotations/comments)
for nxt in lines[i + 1:i + 8]:
t = nxt.strip()
if not t or t.startswith("//"):
continue
if kind == "FUNCTION":
mm = re.search(r"([~\w:]+)\s*\(", t)
else:
mm = re.search(r"([\w:]+)\s*(?:\[[^\]]*\]\s*)*(?:=|;|$)", t.split("=")[0] + ";")
if mm:
qual = mm.group(1)
break
out.append((kind, addr, explicit, qual, nonmatch))
return out
def apply_annotations(smap):
"""Override map names with the annotations in src/ and include/, resolved against build/obj."""
smap.annotated, smap.nonmatching, problems = {}, set(), []
objs = {}
for src in source_files() + header_files():
anns = parse_annotations(os.path.join(ROOT, src))
if not anns:
continue
syms = []
if src in source_files():
op = os.path.join(ROOT, obj_path(src))
if os.path.exists(op):
obj = objs.setdefault(op, CoffObject(op))
syms = [x for x in obj.symbols.values() if x.is_external] # definitions and references
used = set()
for kind, addr, explicit, qual, nonmatch in anns:
name = explicit
if not name and qual:
pref = mangled_prefixes(qual)
cands = [x.name for x in syms if x.name not in used and any(x.name.startswith(p) for p in pref)
and (x.is_function == (kind == "FUNCTION"))]
if cands:
name = cands[0]
elif syms:
problems.append(f"{src}: {kind} 0x{addr:08x} `{qual}`: no matching symbol in object")
if not name:
continue # header declaration of something defined elsewhere, or not built yet
used.add(name)
prev = smap.by_name.get(name)
if prev is not None and prev != addr:
problems.append(f"{src}: {name} annotated at 0x{addr:08x} and 0x{prev:08x}")
continue
k = {"FUNCTION": None, "GLOBAL": "data", "LIBRARY": "lib"}[kind]
if addr in smap.entries and smap.entries[addr][2] != name:
smap.by_name.pop(smap.entries[addr][2], None)
smap.set(addr, name, k)
smap.annotated[addr] = (name, k)
if nonmatch:
smap.nonmatching.add(addr)
return problems
def load_map():
smap = SymbolMap(os.path.join(CFG_DIR, "symbols.txt"))
for p in apply_annotations(smap):
print(f"warning: {p}", file=sys.stderr)
return smap
def header_files():
out = []
for base, _, files in os.walk(os.path.join(ROOT, "include")):
for fn in files:
if fn.endswith((".h", ".hpp")):
out.append(os.path.relpath(os.path.join(base, fn), ROOT))
return sorted(out)
def source_files():
out = []
for base, _, files in os.walk(os.path.join(ROOT, "src")):
for fn in files:
if fn.endswith((".c", ".cpp")):
out.append(os.path.relpath(os.path.join(base, fn), ROOT))
return sorted(out)
def obj_path(src):
return os.path.join("build", "obj", os.path.splitext(src)[0] + ".obj")
def cmd_build(args):
cfg = load_config()
lines = [
"ninja_required_version = 1.5",
"builddir = build",
f"cflags = {cfg['cflags']}",
"rule cl",
" command = tools/cl.sh /nologo /c $cflags $extra /Iinclude /Fo$out $in > $out.log 2>&1"
" || { cat $out.log; rm -f $out; exit 1; }",
" description = CL $in",
"",
]
for src in source_files():
with open(os.path.join(ROOT, src), encoding="latin-1") as f:
head = f.read(4096)
extra = " ".join(re.findall(r"^//\s*CFLAGS:\s*(.*)$", head, re.M))
lines.append(f"build {obj_path(src)}: cl {src} | config/eqgame/config.yaml")
if extra:
lines.append(f" extra = {extra}")
os.makedirs(BUILD, exist_ok=True)
with open(os.path.join(BUILD, "build.ninja"), "w") as f:
f.write("\n".join(lines) + "\n")
r = subprocess.run(["ninja", "-f", "build/build.ninja"] + (["-v"] if args.verbose else []), cwd=ROOT)
if r.returncode:
sys.exit(r.returncode)
def compiled_functions():
"""name -> (obj, sym, sec, start, end) for every function in build/obj."""
out = {}
for src in source_files():
p = os.path.join(ROOT, obj_path(src))
if not os.path.exists(p):
continue
obj = CoffObject(p)
for sym, sec, start, end in obj.functions():
if sym.is_external:
out[sym.name] = (obj, sym, sec, start, end)
return out
# compiler-generated constants: string literals, float/double/SSE constants. Placed wherever the
# linker likes, so they're verified by content rather than by address.
COMPILER_DATA = ("??_C@", "__real@", "__xmm@", "__mask@")
class Result:
def __init__(self, name, addr):
self.name, self.addr = name, addr
self.status = "?"
self.size = 0
self.notes = []
self.suggest = {} # symbol name -> original address
self.locals = [] # compiler-generated code verified alongside: (obj, sec, start, end, addr)
def local_code(obj, tsym):
"""Compiler-generated code in another section of this object (EH handlers, unwind funclets)."""
sec = obj.sections[tsym.section]
if not sec.chars & IMAGE_SCN_CNT_CODE:
return None
ends = [x.value for x in obj.symbols.values() if x.section == tsym.section and x.value > tsym.value]
return sec, tsym.value, min(ends + [len(sec.data)])
def compare(pe, smap, obj, sym, sec, start, end, addr=None, seen=None):
name = sym.name
addr = smap.by_name[name] if addr is None else addr
seen = set() if seen is None else seen
seen.add((obj.path, sym.name))
res = Result(name, addr)
cb = sec.data[start:end]
res.size = len(cb)
ob = pe.read(addr, len(cb))
mask = bytearray(len(cb))
relocs = [(va - start, si, t) for va, si, t in sec.relocs if start <= va < end]
for off, _, _ in relocs:
mask[off:off + 4] = b"\1\1\1\1"
diffs = [i for i in range(len(cb)) if not mask[i] and cb[i] != ob[i]]
mapsize = smap.entries[addr][0] if addr in smap.entries else 0
if mapsize > len(cb):
tail = pe.read(addr + len(cb), mapsize - len(cb))
if tail.strip(b"\xcc\x90"):
res.notes.append(f"original is longer (map size 0x{mapsize:x} vs 0x{len(cb):x})")
diffs.append(len(cb))
if diffs:
res.status = "DIFF"
res.notes.insert(0, f"{len(diffs)} byte(s) differ, first at +0x{diffs[0]:x}")
return res
unknown = bad = 0
for off, si, typ in relocs:
tsym = obj.symbols[si]
if typ == REL_I386_DIR32:
addend, = struct.unpack_from("<i", cb, off)
base = struct.unpack_from("<I", ob, off)[0] - addend
elif typ == REL_I386_REL32:
addend, = struct.unpack_from("<i", cb, off)
base = addr + off + 4 + struct.unpack_from("<i", ob, off)[0] - addend
else:
res.notes.append(f"+0x{off:x}: unhandled reloc type {typ:#x} to {tsym.name}")
unknown += 1
continue
base &= 0xFFFFFFFF
if tsym.section == sec.index:
expected = addr - start + tsym.value
elif tsym.section == -1: # absolute symbol
expected = tsym.value
elif tsym.name == "__except_list": # CRT absolute symbol: fs:[0]
expected = 0
elif tsym.section > 0 and tsym.name not in smap.by_name and local_code(obj, tsym):
if (obj.path, tsym.name) in seen:
continue
lsec, lstart, lend = local_code(obj, tsym)
sub = compare(pe, smap, obj, tsym, lsec, lstart, lend, base, seen)
res.locals.append((obj, lsec, lstart, lend, base))
res.locals += sub.locals
res.suggest.update(sub.suggest)
if sub.status in ("DIFF", "BADREF"):
res.notes.append(f"+0x{off:x}: {tsym.name} at 0x{base:08x}: {sub.status} {'; '.join(sub.notes)}")
bad += 1
elif sub.status == "MATCH?":
unknown += 1
continue
elif tsym.name in smap.by_name:
expected = smap.by_name[tsym.name]
elif tsym.section > 0 and (not tsym.is_external or tsym.name.startswith(COMPILER_DATA)) \
and not (obj.sections[tsym.section].chars & IMAGE_SCN_CNT_CODE):
# local data (string literal, float constant, static table): verify by content
tsec = obj.sections[tsym.section]
blob = tsec.data[tsym.value:]
rmask = {o - tsym.value + k for o, _, _ in tsec.relocs for k in range(4)}
got = pe.read(base, len(blob)) if pe.contains(base) else b""
if got and all(i in rmask or got[i] == blob[i] for i in range(len(blob))):
continue
res.notes.append(f"+0x{off:x}: {tsym.name} content differs at 0x{base:08x}")
bad += 1
continue
else:
res.suggest[tsym.name] = base
unknown += 1
continue
if expected != base:
res.notes.append(f"+0x{off:x}: {tsym.name} -> 0x{base:08x}, map says 0x{expected:08x}")
bad += 1
if bad:
res.status = "BADREF"
elif unknown:
res.status = "MATCH?"
for n, a in res.suggest.items():
cur = smap.entries.get(a)
res.notes.append(f"unmapped {n} = 0x{a:08x}" + (f" (currently {cur[2]})" if cur else ""))
else:
res.status = "MATCH"
return res
def run_checks(filters=()):
cfg = load_config()
pe = PEImage(os.path.join(ROOT, cfg["original"]))
smap = load_map()
results, unmapped = [], []
for name, (obj, sym, sec, start, end) in sorted(compiled_functions().items()):
if name not in smap.by_name:
if not obj.is_inline(sec.index): # inline copies are usually folded away
unmapped.append((name, obj.path))
continue
if filters and not any(fnmatch.fnmatchcase(name, f) or smap.lookup(f) == smap.by_name[name]
for f in filters):
continue
r = compare(pe, smap, obj, sym, sec, start, end)
if r.addr in smap.nonmatching and r.status in ("DIFF", "BADREF"):
r.status = "NONMAT" # known non-matching (functionally equivalent) decompilation
results.append(r)
return smap, results, unmapped
def cmd_check(args):
smap, results, unmapped = run_checks(args.names)
for r in sorted(results, key=lambda r: r.addr):
print(f"{r.status:7} 0x{r.addr:08x} {r.size:5d} {r.name}")
if args.verbose or r.status != "MATCH":
for n in r.notes:
print(f" {n}")
if not args.names:
for name, path in unmapped:
print(f"UNMAPPED {name} ({os.path.relpath(path, ROOT)}): add a // FUNCTION: EQGAME 0xADDR annotation")
if args.learn:
n = 0
for r in results:
if r.status != "MATCH?":
continue
for name, a in r.suggest.items():
if name in smap.by_name:
continue
kind = None if a in smap.entries else "data"
smap.set(a, name, kind)
n += 1
smap.save()
print(f"learned {n} symbol(s) from matching functions")
counts = {}
for r in results:
counts[r.status] = counts.get(r.status, 0) + 1
print(" ".join(f"{k}: {v}" for k, v in sorted(counts.items())) or "nothing to check")
if any(r.status in ("DIFF", "BADREF") for r in results):
sys.exit(1)
def disasm(path, extra):
"""[(address, text, reloc_target_or_None)] from objdump."""
out = subprocess.run(["i686-w64-mingw32-objdump", "-d", "-r", "-M", "intel", "--no-show-raw-insn",
*extra, path], capture_output=True, text=True).stdout
lines = []
for line in out.splitlines():
m = re.match(r"\s+([0-9a-f]+):\s+(.*)", line)
if not m:
continue
text = m.group(2).strip()
if re.match(r"(dir32|DISP32|IMAGE_REL|secrel)", text, re.I):
a, t, _ = lines[-1]
lines[-1] = (a, t, text.split()[-1])
else:
lines.append((int(m.group(1), 16), re.sub(r"\s+", " ", text), None))
return lines
BRANCH = re.compile(r"^(j\w+|call|loop\w*) (?:0x)?([0-9a-f]+)\b(?: <[^>]*>)?$")
def render(lines, base, size, names):
"""Normalise to [(offset, display, compare_key)]: intra-function branches become offsets,
references to other symbols become their names, everything else is compared literally."""
out = []
for a, text, reloc in lines:
m = BRANCH.match(text)
if reloc:
shown = re.sub(r"(?:0x)?[0-9a-f]+(?: <[^>]*>)?$", reloc, text) if m else \
re.sub(r"0x0\b", reloc, text) if "0x0" in text else f"{text} ; {reloc}"
key = f"{m.group(1)} {reloc}" if m else re.sub(r"0x[0-9a-f]+", "REF", text)
elif m and base <= int(m.group(2), 16) < base + size:
shown = key = f"{m.group(1)} +0x{int(m.group(2), 16) - base:x}"
elif m:
tgt = names.get(int(m.group(2), 16), f"0x{int(m.group(2), 16):x}")
shown = f"{m.group(1)} {tgt}"
key = f"{m.group(1)} {tgt}"
else:
shown = text
key = text
# absolute addresses in the original that the map can name
for h in re.findall(r"0x[0-9a-f]{6,8}", text):
n = names.get(int(h, 16))
if n:
shown = shown.replace(h, n)
key = re.sub(r"0x[0-9a-f]{6,8}", "REF", text)
out.append((a - base, shown, key))
return out
def cmd_diff(args):
cfg = load_config()
smap = load_map()
addr = smap.lookup(args.name)
if addr is None:
sys.exit(f"{args.name}: not in symbol map")
name = smap.entries[addr][2]
names = {a: e[2] for a, e in smap.entries.items()}
funcs = compiled_functions()
size = smap.entries[addr][0]
mine = []
if name in funcs:
obj, sym, sec, start, end = funcs[name]
size = max(size, end - start)
mine = render(disasm(obj.path, [f"--disassemble={name}"]), start, end - start, {})
else:
print(f"{name} has not been compiled yet; showing original only")
orig = render(disasm(os.path.join(ROOT, cfg["original"]),
[f"--start-address=0x{addr:x}", f"--stop-address=0x{addr + size:x}"]),
addr, size, names)
# In the original, a reference to a symbol the compiled side names should compare equal.
sm = difflib.SequenceMatcher(None, [k for _, _, k in orig], [k for _, _, k in mine], autojunk=False)
w = 60
print(f"{'ORIGINAL 0x%08x' % addr:<{w}} | COMPILED {name}")
for op, i1, i2, j1, j2 in sm.get_opcodes():
for k in range(max(i2 - i1, j2 - j1)):
left = orig[i1 + k] if i1 + k < i2 else None
right = mine[j1 + k] if j1 + k < j2 else None
mark = " " if op == "equal" else ("|" if left and right else (">" if right else "<"))
ls = f"{left[0]:5x}: {left[1]}"[:w] if left else ""
rs = f"{right[0]:5x}: {right[1]}" if right else ""
print(f"{ls:<{w}} {mark} {rs}")
def cmd_progress(args):
smap, results, _ = run_checks()
matched = {r.addr for r in results if r.status in ("MATCH", "MATCH?")}
game = [(a, e) for a, e in smap.entries.items() if e[1] == "fn"]
total = sum(e[0] for _, e in game) or 1
done = sum(e[0] for a, e in game if a in matched)
print(f"game code: {done}/{total} bytes ({100 * done / total:.3f}%), "
f"{len(matched & {a for a, _ in game})}/{len(game)} functions")
lib = [e for e in smap.entries.values() if e[1] == "lib"]
print(f"library code (excluded): {len(lib)} functions, {sum(e[0] for e in lib)} bytes")
def cmd_link(args):
"""Assemble build/eqgame.exe: compiled code/data placed at their addresses with relocations
resolved through the symbol map; everything not yet decompiled is carved from orig/."""
cfg = load_config()
pe = PEImage(os.path.join(ROOT, cfg["original"]))
smap, results, _ = run_checks()
ok = {r.addr for r in results if r.status in ("MATCH", "MATCH?")}
out = bytearray(pe.data)
from_src = carved_refs = 0
def file_off(va):
for name, sva, vsize, rawptr, rawsize, _ in pe.sections:
if sva <= va < sva + rawsize:
return rawptr + va - sva
return None
def place(obj, sec, start, end, addr):
nonlocal from_src, carved_refs
blob = bytearray(sec.data[start:end])
for va, si, typ in sec.relocs:
if not start <= va < end:
continue
off, tsym = va - start, obj.symbols[si]
addend, = struct.unpack_from("<i", blob, off)
if tsym.section == sec.index:
target = addr - start + tsym.value
elif tsym.name in smap.by_name:
target = smap.by_name[tsym.name]
else:
blob[off:off + 4] = pe.read(addr + off, 4) # unresolved here: keep original field
carved_refs += 1
continue
if typ == REL_I386_DIR32:
struct.pack_into("<I", blob, off, (target + addend) & 0xFFFFFFFF)
elif typ == REL_I386_REL32:
struct.pack_into("<I", blob, off, (target + addend - (addr + off + 4)) & 0xFFFFFFFF)
else:
blob[off:off + 4] = pe.read(addr + off, 4)
carved_refs += 1
fo = file_off(addr)
if fo is None:
return # uninitialised (.bss) data: nothing in the file
out[fo:fo + len(blob)] = blob
from_src += len(blob)
funcs = compiled_functions()
for name, (obj, sym, sec, start, end) in funcs.items():
a = smap.by_name.get(name)
if a in ok:
place(obj, sec, start, end, a)
for r in results:
if r.status in ("MATCH", "MATCH?"):
for obj, sec, start, end, a in r.locals:
place(obj, sec, start, end, a)
# initialised globals annotated in source
for a, (name, kind) in smap.annotated.items():
if kind != "data":
continue
for src in source_files():
op = os.path.join(ROOT, obj_path(src))
if not os.path.exists(op):
continue
obj = CoffObject(op)
sym = next((x for x in obj.symbols.values() if x.name == name and x.section > 0), None)
if not sym:
continue
sec = obj.sections[sym.section]
nxt = min([x.value for x in obj.symbols.values()
if x.section == sym.section and x.value > sym.value] + [len(sec.data)])
if sec.chars & 0x80: # IMAGE_SCN_CNT_UNINITIALIZED_DATA
break
place(obj, sec, sym.value, nxt, a)
break
dst = os.path.join(BUILD, "eqgame.exe")
with open(dst, "wb") as f:
f.write(out)
h = hashlib.sha1(out).hexdigest()
code = sum(s[4] for s in pe.sections if s[0] in (".text", ".rdata", ".data"))
print(f"build/eqgame.exe sha1 {h} {'== original' if h == cfg['sha1'] else '!= ORIGINAL'}")
print(f"from source: {from_src}/{code} bytes of .text/.rdata/.data ({100 * from_src / code:.3f}%), "
f"{carved_refs} references still taken from the original")
if h != cfg["sha1"]:
bad = next(i for i in range(len(out)) if out[i] != pe.data[i])
sys.exit(f"first difference at file offset 0x{bad:x}")
def cmd_map(args):
smap = SymbolMap(os.path.join(CFG_DIR, "symbols.txt"))
smap.set(int(args.addr, 16), args.name, args.kind)
smap.save()
def cmd_todo(args):
smap, results, _ = run_checks()
done = {r.addr for r in results if r.status in ("MATCH", "MATCH?")}
todo = sorted((e[0], a, e[2]) for a, e in smap.entries.items() if e[1] == "fn" and a not in done)
for size, a, name in todo[:args.n]:
print(f"0x{a:08x} {size:6d} {name}")
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("verify")
s = sub.add_parser("import-ghidra"); s.add_argument("file")
s = sub.add_parser("build"); s.add_argument("-v", "--verbose", action="store_true")
s = sub.add_parser("check"); s.add_argument("names", nargs="*")
s.add_argument("-v", "--verbose", action="store_true")
s.add_argument("--learn", action="store_true", help="add symbols resolved by matching functions to the map")
s = sub.add_parser("diff"); s.add_argument("name")
sub.add_parser("progress")
sub.add_parser("link")
s = sub.add_parser("map"); s.add_argument("addr"); s.add_argument("name"); s.add_argument("kind", nargs="?")
s = sub.add_parser("todo"); s.add_argument("n", nargs="?", type=int, default=40)
args = p.parse_args()
globals()["cmd_" + args.cmd.replace("-", "_")](args)
if __name__ == "__main__":
main()