204 lines
7.7 KiB
C++
204 lines
7.7 KiB
C++
#include "shim/hooks/probe_entry.h"
|
|
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#if defined(_WIN32)
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
|
|
#include "MinHook.h"
|
|
#endif
|
|
|
|
#include "generated/sots_addresses.h"
|
|
|
|
// ---- the counters ------------------------------------------------------------------------------
|
|
//
|
|
// Plain 32-bit counters, incremented from a stub that has already saved the flags, so no atomic is
|
|
// needed for correctness of the arithmetic on x86 and none is used: the turn pipeline is
|
|
// single-threaded (lane Z §7) and a torn read here would cost a count, not a crash. If a probe ever
|
|
// reads a wild number, that is itself the finding -- it would mean the address is entered off the
|
|
// turn thread.
|
|
|
|
namespace {
|
|
constexpr std::size_t kMaxProbes = 16;
|
|
std::uint32_t g_calls[kMaxProbes];
|
|
std::uint32_t g_total[kMaxProbes];
|
|
} // namespace
|
|
|
|
extern "C" void ProbeHit(int index) {
|
|
if (index < 0 || static_cast<std::size_t>(index) >= kMaxProbes) return;
|
|
++g_calls[index];
|
|
++g_total[index];
|
|
}
|
|
|
|
// ---- one trampoline slot and one asm stub per probe ---------------------------------------------
|
|
//
|
|
// The stub is written out per index rather than generated at runtime because a runtime-generated
|
|
// thunk would need an executable allocation and a relocation, and MinHook already owns that
|
|
// problem. Sixteen slots is the cap; `kProbes` below is shorter and the rest are inert.
|
|
|
|
#if defined(_WIN32)
|
|
#define PROBE_STUB(i) \
|
|
extern "C" void* g_tr_probe##i; \
|
|
void* g_tr_probe##i = nullptr; \
|
|
extern "C" void ProbeStub##i(void); \
|
|
asm(".text\n" \
|
|
".globl _ProbeStub" #i "\n" \
|
|
"_ProbeStub" #i ":\n" \
|
|
" pushfl\n" \
|
|
" pushal\n" \
|
|
" pushl $" #i "\n" \
|
|
" call _ProbeHit\n" \
|
|
" addl $4, %esp\n" \
|
|
" popal\n" \
|
|
" popfl\n" \
|
|
" jmp *_g_tr_probe" #i "\n")
|
|
|
|
PROBE_STUB(0);
|
|
PROBE_STUB(1);
|
|
PROBE_STUB(2);
|
|
PROBE_STUB(3);
|
|
PROBE_STUB(4);
|
|
PROBE_STUB(5);
|
|
PROBE_STUB(6);
|
|
PROBE_STUB(7);
|
|
PROBE_STUB(8);
|
|
PROBE_STUB(9);
|
|
PROBE_STUB(10);
|
|
PROBE_STUB(11);
|
|
#undef PROBE_STUB
|
|
#endif
|
|
|
|
namespace shim::hooks {
|
|
|
|
namespace {
|
|
|
|
namespace A = sots::addr;
|
|
|
|
struct ProbeDef {
|
|
const char* name;
|
|
std::uint32_t rva;
|
|
void* stub;
|
|
void** trampoline;
|
|
};
|
|
|
|
std::uintptr_t g_exe_base = 0;
|
|
void (*g_log)(const char*) = nullptr;
|
|
bool g_installed[kMaxProbes];
|
|
|
|
void logf(const char* fmt, ...) {
|
|
if (!g_log) return;
|
|
char line[512];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
std::vsnprintf(line, sizeof line, fmt, ap);
|
|
va_end(ap);
|
|
g_log(line);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
#define PROBE(name, rva, i) ProbeDef{name, rva, reinterpret_cast<void*>(&ProbeStub##i), &g_tr_probe##i}
|
|
#else
|
|
#define PROBE(name, rva, i) ProbeDef{name, rva, nullptr, nullptr}
|
|
#endif
|
|
|
|
// The probe set. Order is the report order and the stub index, so it is fixed.
|
|
//
|
|
// Rows 0-3 are the four phase-23/33 callees lane V2 proved can reach the generator; rows 4-6 are
|
|
// the three inner functions those bodies reach the draw *through*, so a row-0..3 hit with no
|
|
// row-4..6 hit localises the gate to the outer body. Rows 7-8 are the trade-raid pair (P4). Rows
|
|
// 9-11 are CONTROLS, and they are the reason a zero in rows 0-6 can be believed: 9 and 10 are two
|
|
// phase-23/33 callees lane V2 measured as draw-free, and 11 is `EncounterDetect_Run`, which every
|
|
// prior lane has observed running once per turn. If the controls read 0 the instrument is broken,
|
|
// not the game (method rule 1 -- a hook that compares nothing prints a clean verdict).
|
|
const ProbeDef kProbes[] = {
|
|
PROBE("Game::ServerSpyManager::vslot13", A::ServerSpyManager_vslot13, 0),
|
|
PROBE("Game::ServerSpyManager::vslot14", A::ServerSpyManager_vslot14, 1),
|
|
PROBE("Game::ServerTradeManagerImpl::vslot13", A::ServerTradeManagerImpl_vslot13, 2),
|
|
PROBE("Game::ServerTradeManagerImpl::vslot15", A::ServerTradeManagerImpl_vslot15, 3),
|
|
PROBE("Game::SpyManager::Slot13RngCallee", A::SpyManager_Slot13RngCallee, 4),
|
|
PROBE("Game::TradeManager::Slot13RngCalleeA", A::TradeManager_Slot13RngCalleeA, 5),
|
|
PROBE("Game::TradeManager::Slot13RngCalleeB", A::TradeManager_Slot13RngCalleeB, 6),
|
|
PROBE("Game::ServerTradeManager::CreateRaidEncounter", A::ServerTradeManager_CreateRaidEncounter, 7),
|
|
PROBE("Game::ServerTradeManager::GenerateTradeRaidEncounters",
|
|
A::ServerTradeManager_GenerateTradeRaidEncounters, 8),
|
|
PROBE("Game::ServerSpyManager::vslot15 [control]", A::ServerSpyManager_vslot15, 9),
|
|
PROBE("Game::ServerTradeManagerImpl::vslot14 [control]", A::ServerTradeManagerImpl_vslot14, 10),
|
|
PROBE("Game::EncounterDetect::Run [control]", A::EncounterDetect_Run, 11),
|
|
};
|
|
#undef PROBE
|
|
|
|
constexpr std::size_t kProbeCount = sizeof kProbes / sizeof kProbes[0];
|
|
static_assert(kProbeCount <= kMaxProbes, "add more PROBE_STUB() slots");
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
std::size_t g_install_count = kProbeCount; // `probes=` overrides; default is all of them
|
|
} // namespace
|
|
|
|
bool probe_config(const char* key, const char* value, std::size_t* count_out) {
|
|
if (std::strcmp(key, "probes") != 0) return false;
|
|
if (std::strcmp(value, "off") == 0 || std::strcmp(value, "none") == 0) {
|
|
g_install_count = 0;
|
|
} else if (std::strcmp(value, "all") == 0 || std::strcmp(value, "on") == 0) {
|
|
g_install_count = kProbeCount;
|
|
} else {
|
|
char* end = nullptr;
|
|
const long n = std::strtol(value, &end, 10);
|
|
if (end == value || n < 0) return true; // ours, but unparseable: leave the default
|
|
g_install_count = static_cast<std::size_t>(n) < kProbeCount
|
|
? static_cast<std::size_t>(n)
|
|
: kProbeCount;
|
|
}
|
|
if (count_out) *count_out = g_install_count;
|
|
return true;
|
|
}
|
|
|
|
void init_probe_entries(std::uintptr_t exe_base, void (*log_line)(const char* line)) {
|
|
g_exe_base = exe_base;
|
|
g_log = log_line;
|
|
}
|
|
|
|
void install_probe_entries() {
|
|
#if defined(_WIN32)
|
|
logf("probe: installing %u of %u (probes= in shim.cfg)",
|
|
static_cast<unsigned>(g_install_count), static_cast<unsigned>(kProbeCount));
|
|
for (std::size_t i = 0; i < g_install_count; ++i) {
|
|
void* target = reinterpret_cast<void*>(g_exe_base + kProbes[i].rva);
|
|
MH_STATUS s1 = MH_CreateHook(target, kProbes[i].stub, kProbes[i].trampoline);
|
|
MH_STATUS s2 = s1 == MH_OK ? MH_EnableHook(target) : s1;
|
|
g_installed[i] = (s2 == MH_OK);
|
|
logf("probe: %s rva=0x%08x -> va=%p create=%s enable=%s", kProbes[i].name, kProbes[i].rva,
|
|
target, MH_StatusToString(s1), MH_StatusToString(s2));
|
|
if (!g_installed[i])
|
|
logf("COVERAGE: probe %s NOT INSTALLED -- its call count is meaningless, not zero",
|
|
kProbes[i].name);
|
|
}
|
|
#else
|
|
for (std::size_t i = 0; i < kProbeCount; ++i) g_installed[i] = false;
|
|
#endif
|
|
}
|
|
|
|
void probe_entries_reset() {
|
|
for (std::size_t i = 0; i < kProbeCount; ++i) g_calls[i] = 0;
|
|
}
|
|
|
|
std::size_t probe_entries_count() { return kProbeCount; }
|
|
|
|
std::size_t probe_entries_snapshot(ProbeEntryRow* out, std::size_t max) {
|
|
const std::size_t n = kProbeCount < max ? kProbeCount : max;
|
|
for (std::size_t i = 0; i < n; ++i) {
|
|
out[i].name = kProbes[i].name;
|
|
out[i].rva = kProbes[i].rva;
|
|
out[i].calls = g_calls[i];
|
|
out[i].total = g_total[i];
|
|
out[i].installed = g_installed[i];
|
|
}
|
|
return n;
|
|
}
|
|
|
|
} // namespace shim::hooks
|