Matching decomp scaffold for EverQuest Titanium eqgame.exe

MSVC 7.1 (cl 13.10.3077) under Wine, PE/COFF matcher with relocation
verification, side-by-side diff, progress, Ghidra-seeded symbol map
(15,323 functions), and the first three matched functions (/O1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
SalastilandClaude Opus 5.5 committed 2026-10-19 22:33:18 -04:00
commit bbc4bbfdd5
11 files changed
+16149

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# Copyrighted originals: never commit. Users supply their own client.
/orig/*
!/orig/README.md
*.exe
*.dll
*.s3d
*.eqg
# Toolchain (downloaded by tools/setup.sh)
/tools/msvc7.1/
# Build output, Ghidra project
/build/
__pycache__/
.ninja_*
+85
View File
@@ -0,0 +1,85 @@
# EverQuest Decomp/Recomp
A byte-for-byte matching decompilation of the EverQuest **Titanium** client
(`eqgame.exe`, linked 2005-10-31 18:36:26 UTC). The goal is C++ source that
recompiles with the original toolchain to the exact same machine code, which
can then be ported, fixed and recompiled freely.
This repository contains **no game code or data**. You need your own copy of the
client; the tools check it against the expected hash.
| | |
|---|---|
| Target | `eqgame.exe`, SHA1 `e8fdc68b46df69dde0094d57fd7ba2cb33aef1c5` |
| Compiler | MSVC 7.1 (Visual Studio .NET 2003), `cl` 13.10.3077, run under Wine |
| Flags so far | `/O1 /GX /Gy` (size-optimised; confirmed by matching code) |
| Functions | ~15.3k found by Ghidra: ~8.4k game code, ~6.9k CRT/STL/library |
## Setup
Requirements: Linux (or WSL/macOS) with `wine`, `python3` + PyYAML, `ninja`,
`i686-w64-mingw32-objdump` (binutils for mingw), and optionally Ghidra.
```sh
tools/setup.sh /path/to/EverQuest/eqgame.exe # links orig/, fetches MSVC 7.1, verifies both
python3 tools/eq.py build # compile src/ -> build/obj/
python3 tools/eq.py check # compare against the original
python3 tools/eq.py progress
```
## Workflow
1. Pick a function: `python3 tools/eq.py todo` lists unmatched game functions,
smallest first. Look at it with `python3 tools/eq.py diff 0x00401050`.
2. Write C++ for it under `src/` and mark it with `// FUNCTION: EQGAME 0xADDR`.
3. `python3 tools/eq.py build`, then map the compiled (mangled) name to the address:
`python3 tools/eq.py map 0x00401050 '?Append@CList@@QAEXPAUCListNode@@@Z'`.
`check` lists compiled functions that aren't mapped yet, with their mangled names.
4. Iterate with `diff` until `check` says `MATCH`.
5. `python3 tools/eq.py check --learn` adds the globals and callees that a
matching function proves (e.g. `operator new` at `0x0060d071`) to the map.
### Check statuses
| Status | Meaning |
|---|---|
| `MATCH` | every byte equal, every relocation points at the right symbol |
| `MATCH?` | bytes equal, but some referenced symbols aren't in the map yet (`--learn` fills them) |
| `BADREF` | bytes equal, but a call/global resolves to a different address than the map says |
| `DIFF` | code differs |
Relocated fields (calls, globals, string literals) are masked when comparing
bytes and then verified separately: against the symbol map for named symbols,
and by content for string literals and float constants.
## Layout
```
config/eqgame/config.yaml target hash, compiler flags
config/eqgame/symbols.txt ADDR SIZE KIND NAME for every known function/global
src/ decompiled source (one .cpp per original translation unit, eventually)
include/ shared headers
tools/eq.py build / check / diff / progress / map
tools/binfmt.py PE + COFF readers
tools/cl.sh MSVC 7.1 under Wine
tools/ghidra/ headless export used to seed symbols.txt
orig/ your eqgame.exe (git-ignored)
```
Per-file compiler flags: put `// CFLAGS: /O2` (etc.) near the top of a source file.
### Re-seeding the symbol map from Ghidra
```sh
/opt/ghidra/support/analyzeHeadless build/ghidra eqgame -import orig/eqgame.exe \
-scriptPath tools/ghidra -postScript ExportFunctions.java "$PWD/build/ghidra_functions.txt"
python3 tools/eq.py import-ghidra build/ghidra_functions.txt # keeps existing names
```
## Notes
- The Rich header shows ~640 C++ objects from cl 13.10.3077, plus libraries built
with VC6 (build 8447/9178), likely Miles Sound System, DirectX helpers, etc.
Those are classified as `lib` and excluded from progress.
- Names are placeholders until identified. The EQEmu project and MacroQuest's
Titanium-era offsets are good references for structures and function names.
+7
View File
@@ -0,0 +1,7 @@
# Target: EverQuest Titanium client, eqgame.exe linked 2005-10-31 18:36:26 UTC
# Toolchain (from Rich header): MSVC 7.1 / Visual Studio .NET 2003, cl 13.10.3077
original: orig/eqgame.exe
sha1: e8fdc68b46df69dde0094d57fd7ba2cb33aef1c5
image_base: 0x400000
# Global compiler flags. Per-file additions: a `// CFLAGS: ...` line near the top of the source.
cflags: /O1 /GX /Gy /Zl /W3
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
Place (or symlink) your own `eqgame.exe` from the EverQuest Titanium client here.
Expected: linked 2005-10-31 18:36:26 UTC, SHA1 `e8fdc68b46df69dde0094d57fd7ba2cb33aef1c5`.
`python3 tools/eq.py verify` checks it. Nothing in this folder is committed.
+61
View File
@@ -0,0 +1,61 @@
// Intrusive doubly-linked list helpers near the start of .text.
// Real names unknown; placeholders until identified.
struct CListNode {
CListNode* next;
CListNode* prev;
};
struct CList {
CListNode* head;
CListNode* tail;
void Append(CListNode* node);
void Remove(CListNode* node);
};
struct CListPair {
CListPair() : a(0), b(0), head(0), tail(0) {}
CListNode* a;
CListNode* b;
CListNode* head;
CListNode* tail;
};
CListPair* g_pListPair;
// FUNCTION: EQGAME 0x00401022
CListPair* GetListPair()
{
if (!g_pListPair)
g_pListPair = new CListPair;
return g_pListPair;
}
// FUNCTION: EQGAME 0x00401050
void CList::Append(CListNode* node)
{
if (!head)
head = node;
else {
tail->next = node;
node->prev = tail;
}
tail = node;
}
// FUNCTION: EQGAME 0x0040106e
void CList::Remove(CListNode* node)
{
if (head == node)
head = node->next;
if (tail == node)
tail = node->prev;
if (node->prev)
node->prev->next = node->next;
if (node->next)
node->next->prev = node->prev;
node->next = 0;
node->prev = 0;
}
+141
View File
@@ -0,0 +1,141 @@
"""Minimal PE image and COFF object readers (i386 only, no dependencies)."""
import struct
# i386 COFF relocation types
REL_I386_DIR32 = 0x06
REL_I386_DIR32NB = 0x07
REL_I386_SECTION = 0x0A
REL_I386_SECREL = 0x0B
REL_I386_REL32 = 0x14
IMAGE_SCN_CNT_CODE = 0x20
IMAGE_SCN_LNK_COMDAT = 0x1000
class PEImage:
def __init__(self, path):
with open(path, "rb") as f:
self.data = f.read()
d = self.data
pe = struct.unpack_from("<I", d, 0x3C)[0]
if d[pe:pe + 4] != b"PE\0\0":
raise ValueError(f"{path}: not a PE file")
nsec, = struct.unpack_from("<H", d, pe + 6)
optsize, = struct.unpack_from("<H", d, pe + 20)
opt = pe + 24
self.image_base, = struct.unpack_from("<I", d, opt + 28)
self.sections = []
for i in range(nsec):
o = opt + optsize + i * 40
name = d[o:o + 8].rstrip(b"\0").decode("latin-1")
vsize, va, rawsize, rawptr = struct.unpack_from("<IIII", d, o + 8)
chars, = struct.unpack_from("<I", d, o + 36)
self.sections.append((name, self.image_base + va, vsize, rawptr, rawsize, chars))
def section(self, name):
for s in self.sections:
if s[0] == name:
return s
raise KeyError(name)
def read(self, va, size):
for name, sva, vsize, rawptr, rawsize, _ in self.sections:
if sva <= va < sva + max(vsize, rawsize):
off = va - sva
out = self.data[rawptr + off:rawptr + min(off + size, rawsize)]
return out + b"\0" * (size - len(out)) # bss tail
raise ValueError(f"VA 0x{va:08x} not in any section")
def contains(self, va):
return any(s[1] <= va < s[1] + s[2] for s in self.sections)
class CoffSymbol:
__slots__ = ("index", "name", "value", "section", "type", "storage")
def __init__(self, index, name, value, section, type_, storage):
self.index, self.name, self.value = index, name, value
self.section, self.type, self.storage = section, type_, storage
@property
def is_function(self):
return (self.type >> 4) == 2
@property
def is_external(self):
return self.storage == 2
class CoffSection:
__slots__ = ("index", "name", "data", "chars", "relocs")
def __init__(self, index, name, data, chars, relocs):
self.index, self.name, self.data, self.chars, self.relocs = index, name, data, chars, relocs
IMAGE_COMDAT_SELECT_ANY = 2
class CoffObject:
def __init__(self, path):
with open(path, "rb") as f:
d = f.read()
self.path = path
machine, nsec, _, symptr, nsym, optsize, _ = struct.unpack_from("<HHIIIHH", d, 0)
if machine != 0x14C:
raise ValueError(f"{path}: not an i386 COFF object")
strtab = symptr + nsym * 18
def cstr(raw):
if raw[:4] == b"\0\0\0\0":
off, = struct.unpack_from("<I", raw, 4)
end = d.index(b"\0", strtab + off)
return d[strtab + off:end].decode("latin-1")
return raw.rstrip(b"\0").decode("latin-1")
self.symbols = {}
self.comdat_select = {} # section number -> IMAGE_COMDAT_SELECT_*
i = 0
while i < nsym:
o = symptr + i * 18
raw = d[o:o + 8]
value, secnum, type_, storage, naux = struct.unpack_from("<IhHBB", d, o + 8)
self.symbols[i] = CoffSymbol(i, cstr(raw), value, secnum, type_, storage)
if storage == 3 and value == 0 and naux and secnum > 0: # section definition
self.comdat_select[secnum] = d[o + 18 + 14]
i += 1 + naux
self.sections = {}
for s in range(nsec):
o = 20 + optsize + s * 40
raw = d[o:o + 8]
name = raw.rstrip(b"\0").decode("latin-1")
if name.startswith("/"):
off = int(name[1:])
name = d[strtab + off:d.index(b"\0", strtab + off)].decode("latin-1")
size, rawptr, relptr, _, nrel, _, chars = struct.unpack_from("<IIIIHHI", d, o + 16)
data = d[rawptr:rawptr + size] if rawptr else b"\0" * size
relocs = []
for r in range(nrel):
va, symidx, typ = struct.unpack_from("<IIH", d, relptr + r * 10)
relocs.append((va, symidx, typ))
self.sections[s + 1] = CoffSection(s + 1, name, data, chars, relocs)
def is_inline(self, secnum):
"""Inline/template functions are emitted as pick-any COMDATs."""
return self.comdat_select.get(secnum) == IMAGE_COMDAT_SELECT_ANY
def functions(self):
"""Yield (symbol, section, start, end) for each function defined in a code section."""
by_sec = {}
for sym in self.symbols.values():
if sym.section > 0 and sym.is_function:
by_sec.setdefault(sym.section, []).append(sym)
for secnum, syms in by_sec.items():
sec = self.sections[secnum]
if not sec.chars & IMAGE_SCN_CNT_CODE:
continue
syms.sort(key=lambda s: s.value)
for i, sym in enumerate(syms):
end = syms[i + 1].value if i + 1 < len(syms) else len(sec.data)
yield sym, sec, sym.value, end
Executable
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# Run MSVC 7.1 (cl 13.10.3077) under Wine.
# Usage: tools/cl.sh <cl args...> (paths may be POSIX; they're passed through winepath-free via Z:)
ROOT=$(cd "$(dirname "$0")/.." && pwd)
MSVC="$ROOT/tools/msvc7.1"
[ -x "$MSVC/Bin/cl.exe" ] || { echo "MSVC 7.1 not found in $MSVC (run tools/setup.sh)" >&2; exit 1; }
export WINEDEBUG=-all
export INCLUDE="Z:$(echo "$MSVC/Include" | tr / '\\')"
export PATH_MSVC="$MSVC/Bin"
exec wine "$MSVC/Bin/cl.exe" "$@"
+445
View File
@@ -0,0 +1,445 @@
#!/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
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._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):
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)")
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
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
def compare(pe, smap, obj, sym, sec, start, end):
name = sym.name
addr = smap.by_name[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 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.name in smap.by_name:
expected = smap.by_name[tsym.name]
elif tsym.section > 0 and not tsym.is_external \
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 = SymbolMap(os.path.join(CFG_DIR, "symbols.txt"))
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
results.append(compare(pe, smap, obj, sym, sec, start, end))
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)})")
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 = SymbolMap(os.path.join(CFG_DIR, "symbols.txt"))
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_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")
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()
+28
View File
@@ -0,0 +1,28 @@
// Exports every function Ghidra found as: start_va size name kind
// kind: fn | thunk | lib (FID-identified library code)
// Usage: analyzeHeadless <proj> <name> -import eqgame.exe -postScript ExportFunctions.java <out.txt>
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import java.io.*;
public class ExportFunctions extends GhidraScript {
@Override
public void run() throws Exception {
String out = getScriptArgs().length > 0 ? getScriptArgs()[0] : "functions.txt";
try (PrintWriter w = new PrintWriter(new FileWriter(out))) {
for (Function f : currentProgram.getFunctionManager().getFunctions(true)) {
if (f.isExternal()) continue;
Address entry = f.getEntryPoint();
AddressRange r = f.getBody().getRangeContaining(entry);
long size = r == null ? 0 : r.getMaxAddress().subtract(entry) + 1;
String kind = f.isThunk() ? "thunk" : "fn";
Symbol s = f.getSymbol();
if (s.getSource() == SourceType.ANALYSIS && !s.getName().startsWith("FUN_")) kind = "lib";
w.printf("0x%08x 0x%x %s %s%n", entry.getOffset(), size, f.getName(true).replace(' ', '_'), kind);
}
}
println("exported to " + out);
}
}
Executable
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# One-time setup: fetch MSVC 7.1 (cl 13.10.3077) and check the original binary.
# Usage: tools/setup.sh [path/to/eqgame.exe]
set -e
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
if [ -n "$1" ]; then
ln -sf "$(realpath "$1")" orig/eqgame.exe
fi
if [ ! -x tools/msvc7.1/Bin/cl.exe ]; then
# Same package decomp.me uses for its "msvc7.1" compiler (the asset name says 7.0; contents are 7.1).
URL=https://github.com/OmniBlade/decomp.me/releases/download/msvcwin9x/msvc7.0.tar.gz
TMP=$(mktemp -d)
curl -fL -o "$TMP/msvc.tar.gz" "$URL"
mkdir -p tools/msvc7.1
tar -xzf "$TMP/msvc.tar.gz" -C tools/msvc7.1
rm -rf "$TMP"
fi
if ! WINEDEBUG=-all wine tools/msvc7.1/Bin/cl.exe 2>&1 | grep -q "Version 13.10.3077 "; then
echo "error: tools/msvc7.1/Bin/cl.exe is not cl 13.10.3077 (or wine is missing)" >&2
exit 1
fi
echo "MSVC 7.1 (13.10.3077): OK"
python3 tools/eq.py verify