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]>
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 useadd esp,4instead ofpop ecx. Calls to a callee in another file assume the worst.- Example:
0x401bb1only matches whenGetSpellByID(0x401b28) is in a different file, even though the two are adjacent in.text. - Example (EQPlayer):
0x480130only matches whenEQPlayer::HairType(0x47a523) is defined in the same file.
- Example:
- 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.