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

76 lines
4.4 KiB
Markdown

# 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`.