53 lines
3.2 KiB
Markdown
53 lines
3.2 KiB
Markdown
# W2 — the watchpoint module, and what it answered
|
|
|
|
Companion to `docs/W2-predictions.md` (written first) and to
|
|
`sots-re/findings/control-flow/watchpoints-modcount-status.md` (the full report).
|
|
|
|
## What was added
|
|
|
|
`src/shim/hooks/watchpoints.{h,cpp}` — hardware data-write watchpoints on the live game.
|
|
|
|
- **One** MinHook detour in the whole module, on `StrategyServer::ApplyAllTurnCommands`
|
|
(`0x0078f6a0`), used only to learn `S` on the turn thread. Debug registers are per-thread, so the
|
|
arming point has to run on that thread and precede the writes; A2 established that this function
|
|
is the End-Turn command flush and that its `this` is the `S` frame.
|
|
- DR0-DR3 as 4-byte data-write breakpoints, set with `GetThreadContext`/`SetThreadContext` on the
|
|
calling thread and **read back and logged**, plus a vectored exception handler that records EIP,
|
|
the written value, `DR6`, the thread id, the `EBP` chain and the first four code-looking words
|
|
above `ESP`.
|
|
- A **canary self-test**: DR3 is pointed at a word the shim owns, that word is written once, and the
|
|
handler must report exactly one trap before any game address is believed. An arm that silently
|
|
failed is indistinguishable from "nothing writes this" (method rule 1).
|
|
- A background flusher, because the game is usually killed rather than quit and
|
|
`DLL_PROCESS_DETACH` is not guaranteed.
|
|
- Config: `watch=on|off`, `watch.players=0..2`, `watch.out=<path>`. `shim.cfg.w2watch` and
|
|
`shim.cfg.w2control` differ **only** in `watch=`, so the pair is a real rule-19 control.
|
|
|
|
The watchpoints modify no code, so the instrument's only patched bytes are the single arming
|
|
detour's five.
|
|
|
|
## Rule 19: the control was taken and it passed
|
|
|
|
One End Turn from `ref-turn2.sav` with all four watchpoints armed reproduced the determinism oracle
|
|
byte for byte — `(Autosave EndTurn).sav` `bb4fd9ac…`, `(Autosave).sav` `978041ac…`. The prediction
|
|
in `W2-predictions.md` Part 2 ("the watchpoint is byte-neutral, unlike a detour, because a `#DB` on
|
|
a data write is a trap taken after the store retires and no code is modified") held.
|
|
|
|
## What it answered
|
|
|
|
1. `ModCount` (`S+0x8`): **exactly 12 writes per End Turn** on this save, twice, values 13→24 and
|
|
25→36 with no gap. Lane A2's prediction confirmed, including both predicted addresses
|
|
(`0x007dc6f0`, `0x007d92ca`), the zero at `0x007b9e20`, and the whole
|
|
`OnMessage → ApplyAllTurnCommands → ApplyTurnCommandBatch` call chain.
|
|
2. `Frame` (`S+0xc`): **exactly one** write per End Turn, from `BeginProcessTurn + 0x2a`, value
|
|
becoming the turn number. This settles the `ModCount`-vs-`Frame` naming dispute in lane A2's
|
|
favour against both lane T and `addresses.json`.
|
|
3. `Player.Status`: the writer the S31 regression was missing is
|
|
`StrategyNetworkClient::OnMessage + 0xa15`, writing 4 after `ProcessTurn` returns and before the
|
|
autosave. Lane T2's "there is no writer between tail phase 31 and the autosave" is falsified.
|
|
|
|
## Reusing it
|
|
|
|
Change the four addresses computed in `WatchOnApplyAll`; anything reachable from `S` at the flush is
|
|
one line. Keep the canary self-test and keep the `ref-turn2` oracle control — together they cost
|
|
about two minutes and they are what makes the numbers evidence rather than output.
|