Files
SalastilandClaude Opus 5.5 9506990c95 tus: estimate translation-unit boundaries from .rdata marker tables
Each game object's .rdata starts with a header's static int[5]
{2000,2500,3000,4000,10000}: 261 blocks. config/eqgame/tus.txt maps
each block to the code referencing its own (non-pooled) strings and vtables.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 19:12:15 -04:00

4.4 KiB

Matching notes (MSVC 7.1, eqgame.exe)

These are things learned while matching. Each one changes the generated bytes.

Translation units matter

  • MSVC 7.1 compiles a .cpp's functions callee-first and remembers which registers each callee clobbers. Calls to a callee in the same file can skip reloads (ecx, …) or use add esp,4 instead of pop ecx. Calls to a callee in another file assume the worst.
    • Example: 0x401bb1 only matches when GetSpellByID (0x401b28) is in a different file, even though the two are adjacent in .text.
    • Example (EQPlayer): 0x480130 only matches when EQPlayer::HairType (0x47a523) is defined in the same file.
  • So callers and callees tell you where file boundaries are. Own whole files, not address ranges.
  • Inline/template functions (header code) are placed by the linker in the first object that emitted them. Put them in include/ with annotations; any object may emit them.

Finding file boundaries

Every game .cpp includes a header with a static const int[5] = {2000, 2500, 3000, 4000, 10000}, so each object's .rdata starts with that table: 261 of them. python3 tools/eq.py tus writes config/eqgame/tus.txt, which maps each block to the code that references its strings and vtables. Read CODE_LO as where the file starts, and the end as the next file's start. Shared constants and pooled literals (/GF) are filtered out, but treat the estimate as a hint and confirm it with the register-usage rule above.

Flags

Global: /O1 /MT /GX /GS /Gy. /MT matters: without _MT, Dinkumware's locale/stream code compiles without locks and STL instances don't match.

Template and compiler-generated instances

Write the real STL usage (e.g. std::list<Record> with push_back, remove, sort) and let MSVC emit the instances. Then python3 tools/eq.py pair src/eqgame/X.cpp --range 0xSTART-0xEND --write appends // FUNCTION: / // LIBRARY: annotations for every compiled function with a unique byte match in the range. It checks relocations to tell ICF-folded twins apart, and std:: members become LIBRARY.

Code generation clues

Original shows Source had
and dword ptr [x],0, push 0x6; pop eax, pop ecx after cdecl calls /O1 (global flag)
mov eax,cookie … call @__security_check_cookie@4 /GS with a char buffer (global flag)
mov eax,handler; call __EH_prolog EH frame under /O1: an object with a dtor, or new of a class with a ctor
delete[] calls 0x60d682 rather than 0x60d0d5 #include <new> (declares operator delete[])
only a vptr store, no mov eax,ecx a destructor; a ctor returns this
vptr of the derived class only, no base vptr store base class is __declspec(novtable)
ret N but ecx never read a member function that doesn't use this
push ebp; mov ebp,esp in a small function, param slot reused for a local member function taking the address of a local/param; let MSVC coalesce, don't fake it
member store order differs from declaration order assign in the body, not the init list
a std::string temporary lands in a different stack slot construct from const char* directly (istringstream in(p)), let the conversion make the temp
a small local shares a slot with a dead one the local is smaller than you think, e.g. a 4-byte event base struct, not the 12-byte derived one
a bool result kept in bl across two returns those returns return a variable, the others a constant
an if/else whose tails merge after a hoisted load write the else branch explicitly rather than a fall-through if (!x) x = …
two jmp-only functions in a row (jmp Reset) out-of-line copies of inline dtors, used by EH unwind funclets

Library code

python3 tools/eq.py libscan tools/probes/stl.cpp --apply marks copies of Dinkumware STL instances as lib. They need no decompiling, because any file that includes the same headers and uses the same types emits them. A probe function only counts if every callee is itself library code. That stops game classes' deleting destructors (same bytes, different callee) from being misclassified.

Some std::basic_stringbuf members (e.g. seekoff at 0x40224b) differ from the probe's copy in register allocation. Most likely the same-file callee effect above, so the probe's surrounding code matters. These are still listed as fn.