sots-engine/src/shim/main.cpp
alex b39bb290e3 L5: the interest literals verified live at a boundary, with a control that fails
`ComputeBudget`'s savings-interest term is now compared against the running game at
a treasury the corpus actually contains. Three runs on VM146 from turn1-state.sav:

  A (widened floats, as shipped)   3,895 calls, 0 diverged, 0 undeclared writes
  B (exact decimals, the control)  2,718 calls, 1,359 diverged

The game fills savingsInterest with 499 at a treasury of 50,000, and with 380 at
38,100 -- the exact decimals pay 500 and 381. Every divergence in B lands on a
treasury that is a multiple of 100 and no other state diverges at all, which is
exactly the arithmetic. G3's rule-23 reading is now measured, not inferred, and the
one-money error is shown to propagate into `available` and `researchMoney` too.

The control also settles why the earlier 4,437-call green run was green: slot 5 IS
diffed and the harness CAN see it, so that run simply presented no boundary state.
Coverage is therefore reported as distinct states, not calls: 5 distinct treasuries,
2 of them on the boundary.

Two further rule-23 constants found in the same routine by an operand-width sweep,
corrected, and honestly marked UNVERIFIED because no reference turn can see them:

  - the research-yield factor is a widened 0.85f while its two neighbours in the
    same product are exact doubles. Boundary: research money a multiple of 40,000;
    the run presented 9 distinct values and none is.
  - the three research modifiers are summed in single precision, not double.
    Boundary: two of the three non-zero; the corpus has shrm = TRM = 0.

Both are pinned by boundary cases in test_economy.cpp that fail with the decimals.

Also verified live, in the same run:
  - T31's difficulty-column recovery. The live ServerPlayer+0xf9 / NPC flags on all
    eight players are exactly what lane PL's save-only inversion claims, including
    the awkward system-owning player that is still ambiguous because it is an NPC.
  - BANKRUPTCY_PROTECTION_LIMIT_FACTOR reads 3.29999995 = (float)3.3. Its file image
    is zero because the loader fills it at run time, so lane PL-3 had to assume the
    value; it is now measured and the assumption was right.

Falsified, and recorded as such: the difficulty-mods record does NOT sit inline at
ServerPlayer+0x36c -- that field is a heap pointer on all eight players. The row IS
reachable from a ServerPlayer (which corrects the hook's standing coverage note),
but the fitted {3.0,1.5}/{1.0,1.0} pair remains unverified. The hook logs the
pointer and does not follow it.

The `verified` column stays 0, deliberately. Every phase this compare touches is
Partial for reasons upstream of it, and promoting one because part of it was checked
is the drift app_test_catalog exists to catch. What moved is models; see
docs/L5-live-verification.md for each one with its coverage.

Gates run separately: clean-room OK, host ctest 54/54, CT111 shim cross-build exit 0.
2026-09-08 17:48:42 -04:00

417 lines
20 KiB
C++

// sots-engine shim: a proxy binkw32.dll the original game loads.
//
// Every Bink export is forwarded to the real DLL by the linker (see binkw32.def); this file only
// exists to get code running inside the game process. M0 scope: log a banner, install one hook on
// Mars::Application::Initialize, and prove injection + ASLR relocation + hooking end to end.
//
// Binary facts (RVAs, calling conventions) come exclusively from the generated header.
#include <windows.h>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <string>
#include "MinHook.h"
#include "generated/sots_addresses.h"
#include "shim/fpu_force.h"
#include "shim/hooks/dictionaries.h"
#include "shim/hooks/colony_turn.h"
#include "shim/hooks/system_output.h"
#include "shim/hooks/player_turn.h"
#include "shim/hooks/compute_budget.h"
#include "shim/hooks/fleet_movement.h"
#include "shim/hooks/global_consts.h"
#include "shim/hooks/research.h"
#include "shim/hooks/draw_sites.h"
#include "shim/hooks/probe_entry.h"
#include "shim/hooks/tail_rng.h"
#include "shim/hooks/tech_effects.h"
#include "shim/hooks/watchpoints.h"
#include "shim/trace/hook.h"
#include "shim/trace/selftest.h"
#include "shim/trace/tracer.h"
namespace {
// ---- logging ------------------------------------------------------------------------------
FILE* g_log = nullptr;
char g_dir[MAX_PATH] = {}; // directory the shim DLL lives in (== game dir)
void Log(const char* fmt, ...) {
if (!g_log) return;
SYSTEMTIME st;
GetLocalTime(&st);
std::fprintf(g_log, "%02u:%02u:%02u.%03u [tid %5lu] ", st.wHour, st.wMinute, st.wSecond,
st.wMilliseconds, GetCurrentThreadId());
va_list ap;
va_start(ap, fmt);
std::vfprintf(g_log, fmt, ap);
va_end(ap);
std::fputc('\n', g_log);
std::fflush(g_log);
}
// ---- config (shim.cfg next to the DLL; "key=value" per line) ------------------------------
//
// `hooks=`, `hook.<Name>=`, `trace.*` are owned by the tracer (src/shim/trace/tracer.h);
// `hooks=off` additionally means "install nothing" (the M0 asm-stub hook included).
struct Config {
bool hooks = true; // hooks != off
shim::trace::Config trace;
};
Config ReadConfig() {
Config cfg;
char path[MAX_PATH];
std::snprintf(path, sizeof path, "%s\\shim.trace.jsonl", g_dir);
cfg.trace.path = path;
std::snprintf(path, sizeof path, "%s\\shim.cfg", g_dir);
FILE* f = std::fopen(path, "r");
if (!f) {
Log("config: %s not found, using defaults", path);
return cfg;
}
char line[512];
while (std::fgets(line, sizeof line, f)) {
char* p = line;
while (*p == ' ' || *p == '\t') ++p;
if (*p == '#' || *p == ';' || *p == '\n' || *p == '\0') continue;
char* eq = std::strchr(p, '=');
if (!eq) continue;
*eq = '\0';
char* val = eq + 1;
val[std::strcspn(val, "\r\n")] = '\0';
std::string err;
std::size_t probe_n = 0;
if (shim::hooks::probe_config(p, val, &probe_n)) {
Log("config: %s=%s -> %u lane-H entry probes", p, val,
static_cast<unsigned>(probe_n));
} else if (shim::hooks::watch_apply_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::fpu::apply_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (cfg.trace.apply(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else {
Log("config: ignoring unknown key '%s'", p);
}
}
std::fclose(f);
cfg.hooks = cfg.trace.default_mode != shim::trace::Mode::Off;
return cfg;
}
// ---- the one M0 hook ----------------------------------------------------------------------
bool g_minhookUp = false;
} // namespace
// The detour is a register-transparent asm stub rather than a C++ thiscall function: the
// prototype in sots_addresses.h is [unverified], and a C++ detour that *calls* the original
// bakes in assumptions about stack args / EDX / return value. Calling out for the log with
// everything saved and then tail-jumping to the trampoline leaves the original's own `ret`
// in charge of cleanup, so the hook is correct for any calling convention. (v1 of this hook
// was a thiscall wrapper and crashed the game inside Initialize.)
extern "C" void* g_origInitialize; // trampoline, filled in by MH_CreateHook
void* g_origInitialize = nullptr;
extern "C" void InitializeDetour();
extern "C" void LogInitializeCalled(void* self) {
Log("Application::Initialize called (this=%p)", self);
}
asm(R"(
.text
.globl _InitializeDetour
_InitializeDetour:
pushfl
pushal
pushl %ecx
call _LogInitializeCalled
addl $4, %esp
popal
popfl
jmp *_g_origInitialize
)");
// Line logger handed to the template hooks (they live outside this TU's anonymous namespace).
void ShimLogLine(const char* line) { Log("%s", line); }
namespace {
// ---- template hooks (verified cdecl prototypes; docs/shim-trace.md "Declaring a hook") ----
template <class D>
void InstallTemplateHook(shim::trace::Tracer& tracer, uintptr_t exeBase, uint32_t rva) {
using H = shim::trace::Hook<D>;
H::configure(tracer);
void* target = reinterpret_cast<void*>(exeBase + rva);
if (H::mode == shim::trace::Mode::Off) {
Log("hook: %s rva=0x%08x mode=off (not installed)", D::name, rva);
return;
}
MH_STATUS st = MH_CreateHook(target, reinterpret_cast<void*>(H::detour()),
reinterpret_cast<void**>(&H::original));
Log("hook: %s rva=0x%08x -> va=%p MH_CreateHook -> %s (trampoline=%p)", D::name, rva, target,
MH_StatusToString(st), reinterpret_cast<void*>(H::original));
if (st != MH_OK) return;
st = MH_EnableHook(target);
Log("hook: %s MH_EnableHook -> %s mode=%s", D::name, MH_StatusToString(st),
shim::trace::mode_name(H::mode));
}
using LoadFileHook = shim::trace::Hook<shim::hooks::GlobalConstsLoadFileHook>;
using WeaponInitHook = shim::trace::Hook<shim::hooks::WeaponDictionaryInitHook>;
using SectionCtorHook = shim::trace::Hook<shim::hooks::SectionDictionaryCtorHook>;
using ProcessResearchHook = shim::trace::Hook<shim::hooks::TechTreeProcessResearchHook>;
using OnTechResearchedHook = shim::trace::Hook<shim::hooks::ServerPlayerOnTechResearchedHook>;
using ComputeBudgetHook = shim::trace::Hook<shim::hooks::ComputeBudgetHook>;
using ColonyTurnHook = shim::trace::Hook<shim::hooks::ServerSystemProcessTurnHook>;
// Lane N: the population -> base-output term (docs/N-output-term.md).
using GroupOutputHook = shim::trace::Hook<shim::hooks::ServerSystemGroupOutputHook>;
using TotalOutputHook = shim::trace::Hook<shim::hooks::ServerSystemComputeTotalOutputHook>;
using PlayerTurnHook = shim::trace::Hook<shim::hooks::ServerPlayerProcessTurnHook>;
using MoveFleetHook = shim::trace::Hook<shim::hooks::StrategyServerMoveFleetHook>;
using FleetMovementHook = shim::trace::Hook<shim::hooks::StrategyServerProcessFleetMovementHook>;
// Lane Z: the per-turn RNG ledger (docs/Z-tail-rng.md).
using AutosaveHook = shim::trace::Hook<shim::hooks::StrategyHostAutosaveHook>;
using ServerTurnHook = shim::trace::Hook<shim::hooks::StrategyServerProcessTurnHook>;
using CombatDoneTailHook = shim::trace::Hook<shim::hooks::OnAllCombatDoneTailHook>;
using ApplyEncounterHook = shim::trace::Hook<shim::hooks::ApplyEncounterResultHook>;
using NodeDecayHook = shim::trace::Hook<shim::hooks::NodeLineDecayHook>;
using NodeSpaceHook = shim::trace::Hook<shim::hooks::ProcessNodeSpaceTravelHook>;
using AssignContactsHook = shim::trace::Hook<shim::hooks::EncounterDetectAssignContactsHook>;
// Lane H: the caller of AssignContacts, where lane Y's 2-word detection residual lives.
using ProcessTeamRecordHook = shim::trace::Hook<shim::hooks::EncounterDetectProcessTeamRecordHook>;
void InstallHooks(shim::trace::Tracer& tracer) {
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
void* target = reinterpret_cast<void*>(exeBase + sots::addr::Mars_Application_Initialize);
Log("hook: Mars_Application_Initialize rva=0x%08x -> va=%p", sots::addr::Mars_Application_Initialize,
target);
MH_STATUS st = MH_Initialize();
Log("hook: MH_Initialize -> %s", MH_StatusToString(st));
if (st != MH_OK) return;
g_minhookUp = true;
st = MH_CreateHook(target, reinterpret_cast<void*>(&InitializeDetour), &g_origInitialize);
Log("hook: MH_CreateHook -> %s (trampoline=%p)", MH_StatusToString(st), g_origInitialize);
if (st != MH_OK) return;
st = MH_EnableHook(target);
Log("hook: MH_EnableHook -> %s", MH_StatusToString(st));
// M1: GlobalConsts::LoadFile (cdecl, verified) through the template.
shim::hooks::init_global_consts(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::GlobalConstsLoadFileHook>(tracer, exeBase, sots::addr::GlobalConsts_LoadFile);
// M2: the id-manifest loaders (verified thiscall) through the template's Thiscall conv.
shim::hooks::init_dictionaries(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::WeaponDictionaryInitHook>(tracer, exeBase, sots::addr::WeaponDictionary_Init);
InstallTemplateHook<shim::hooks::SectionDictionaryCtorHook>(tracer, exeBase, sots::addr::SectionDictionary_ctor);
// B3: the per-turn research pass (verified thiscall). One call per player per turn.
shim::hooks::init_research(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::TechTreeProcessResearchHook>(tracer, exeBase, sots::addr::TechTree_ProcessResearch);
// B2: the hard-coded tech-effect callback (verified thiscall, vft slot 4). Fires once
// per tech completion, so on most turns not at all.
shim::hooks::init_tech_effects(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerPlayerOnTechResearchedHook>(tracer, exeBase, sots::addr::ServerPlayer_OnTechResearched);
// B1: ServerPlayer::ComputeBudget (verified thiscall) -- the first behavioural compare.
shim::hooks::init_compute_budget(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ComputeBudgetHook>(tracer, exeBase, sots::addr::ServerPlayer_ComputeBudget);
// B4: the per-system colony turn (once per system per turn) and the two movement entry
// points. All three are verified thiscall prototypes with no stack-argument surprises.
shim::hooks::init_colony_turn(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerSystemProcessTurnHook>(tracer, exeBase, sots::addr::ServerSystem_ProcessTurn);
// Lane N: the two side-effect-free output functions. GroupOutput is the population ->
// output law itself; ComputeTotalOutput is the sum the budget roll-up ultimately reads.
shim::hooks::init_system_output(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerSystemGroupOutputHook>(tracer, exeBase, sots::addr::ServerSystem_GroupOutput);
InstallTemplateHook<shim::hooks::ServerSystemComputeTotalOutputHook>(tracer, exeBase, sots::addr::ServerSystem_ComputeTotalOutput);
// Lane T: the per-player turn driver (once per player per turn). Verified thiscall with
// one ignored float argument; see docs/T-turn-driver.md.
shim::hooks::init_player_turn(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::ServerPlayerProcessTurnHook>(tracer, exeBase, sots::addr::ServerPlayer_ProcessTurn);
shim::hooks::init_fleet_movement(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::StrategyServerMoveFleetHook>(tracer, exeBase, sots::addr::StrategyServer_MoveFleet);
InstallTemplateHook<shim::hooks::StrategyServerProcessFleetMovementHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessFleetMovement);
// Lane Z: the per-turn RNG ledger. Six nested hooks bracketing one End Turn between the two
// autosaves; every one declares the strategic generator and nothing else. Installed BEFORE
// the fpu module because both want StrategyServer::ProcessTurn and MinHook allows one hook
// per target -- `fpu.sample_turn=off` is the config that hands it over cleanly, and if it is
// left on the fpu sampler's MH_CreateHook is what fails and says so.
shim::hooks::init_tail_rng(exeBase, &ShimLogLine);
InstallTemplateHook<shim::hooks::StrategyHostAutosaveHook>(tracer, exeBase, sots::addr::StrategyHost_Autosave);
InstallTemplateHook<shim::hooks::StrategyServerProcessTurnHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessTurn);
InstallTemplateHook<shim::hooks::OnAllCombatDoneTailHook>(tracer, exeBase, sots::addr::StrategyServer_OnAllCombatDone_Tail);
InstallTemplateHook<shim::hooks::ApplyEncounterResultHook>(tracer, exeBase, sots::addr::StrategyServer_ApplyEncounterResult);
InstallTemplateHook<shim::hooks::NodeLineDecayHook>(tracer, exeBase, sots::addr::StrategyServer_NodeLineDecay);
InstallTemplateHook<shim::hooks::ProcessNodeSpaceTravelHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessNodeSpaceTravel);
// Lane I's one inlined-draw site inside ProcessTurn's closure. It leaves no call-graph edge, so
// neither a sweep nor the entry-point detours below can see it; only a boundary hook can.
InstallTemplateHook<shim::hooks::EncounterDetectAssignContactsHook>(tracer, exeBase, sots::addr::EncounterDetect_AssignContacts);
// Lane H: its only caller. AssignContacts never ran on any measured turn because the gate
// here was never satisfied, so the 2-word detection residual is above that gate and no
// instrument has bracketed the function containing both.
InstallTemplateHook<shim::hooks::EncounterDetectProcessTeamRecordHook>(tracer, exeBase, sots::addr::EncounterDetect_ProcessTeamRecord);
// Per-call-site attribution: detour the SEVEN generator entry points and record
// __builtin_return_address(0) with the word cost of each call. These are NOT template hooks --
// they write no record and take no snapshot, just two 4-byte reads of `left` -- because
// NextFloat alone has 109 call sites and a record per draw would drown the log. The table is
// emitted once per turn on the post-turn autosave, where it can be reconciled against the
// bracket total the region ledger measured independently.
shim::hooks::init_draw_sites(exeBase);
{
std::size_t nsites = 0;
const shim::hooks::DrawSiteHook* sites = shim::hooks::draw_site_hooks(&nsites);
for (std::size_t i = 0; i < nsites; ++i) {
void* t = reinterpret_cast<void*>(exeBase + sites[i].rva);
MH_STATUS s1 = MH_CreateHook(t, sites[i].detour, sites[i].trampoline);
MH_STATUS s2 = s1 == MH_OK ? MH_EnableHook(t) : s1;
Log("drawsite: %s rva=0x%08x -> va=%p create=%s enable=%s", sites[i].name, sites[i].rva,
t, MH_StatusToString(s1), MH_StatusToString(s2));
}
}
// Lane H: register-transparent entry counters. Installed after the draw-site detours and
// before the fpu module; they share no target with either. See probe_entry.h for why these are
// asm stubs and not C++ detours.
shim::hooks::init_probe_entries(exeBase, &ShimLogLine);
shim::hooks::install_probe_entries();
// Lane W2: hardware data-write watchpoints. One MinHook detour (the arming point); the
// watchpoints themselves modify no code at all. Off unless `watch=on` (rule 19).
shim::hooks::install_watchpoints(exeBase, g_dir, &ShimLogLine);
// Lane F: x87 control-word forcing at the turn gate + the per-tick change sampler.
// Installed last so it is nowhere near the template hooks it is meant to measure.
shim::fpu::install(exeBase, &ShimLogLine);
}
// ---- lifecycle -----------------------------------------------------------------------------
void Shim_Init(HMODULE self) {
GetModuleFileNameA(self, g_dir, sizeof g_dir);
if (char* slash = std::strrchr(g_dir, '\\')) *slash = '\0';
char logPath[MAX_PATH];
std::snprintf(logPath, sizeof logPath, "%s\\shim.log", g_dir);
g_log = std::fopen(logPath, "a");
if (!g_log) return; // nothing we can do; the forwarders still work without us
char exePath[MAX_PATH] = {};
GetModuleFileNameA(nullptr, exePath, sizeof exePath);
const uintptr_t exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleA(nullptr));
Log("==== sots-engine shim (binkw32 proxy) build %s ====", SHIM_BUILD_ID);
Log("exe: %s", exePath);
Log("exe base=0x%08lx (link-time image base 0x%08lx, ASLR delta %+ld) pid=%lu shim=%p",
static_cast<unsigned long>(exeBase), static_cast<unsigned long>(sots::addr::IMAGE_BASE),
static_cast<long>(exeBase - sots::addr::IMAGE_BASE), GetCurrentProcessId(),
static_cast<void*>(self));
Log("addresses: %s", SOTS_ADDR_PROVENANCE);
const Config cfg = ReadConfig();
if (!cfg.hooks) {
Log("hook: disabled by config");
return;
}
// Trace log: register every template hook's policy (goes into the meta line), then open.
// Hooks read their mode from the tracer after open(); a tracer that failed to open reports
// Off for everything, so the game runs un-instrumented rather than half-instrumented.
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
tracer.configure(cfg.trace);
shim::trace::Hook<shim::selftest::FillHook>::register_policy(tracer);
LoadFileHook::register_policy(tracer);
ComputeBudgetHook::register_policy(tracer);
WeaponInitHook::register_policy(tracer);
SectionCtorHook::register_policy(tracer);
ProcessResearchHook::register_policy(tracer);
OnTechResearchedHook::register_policy(tracer);
ColonyTurnHook::register_policy(tracer);
GroupOutputHook::register_policy(tracer);
TotalOutputHook::register_policy(tracer);
PlayerTurnHook::register_policy(tracer);
MoveFleetHook::register_policy(tracer);
FleetMovementHook::register_policy(tracer);
AutosaveHook::register_policy(tracer);
ServerTurnHook::register_policy(tracer);
CombatDoneTailHook::register_policy(tracer);
ApplyEncounterHook::register_policy(tracer);
NodeDecayHook::register_policy(tracer);
NodeSpaceHook::register_policy(tracer);
AssignContactsHook::register_policy(tracer);
ProcessTeamRecordHook::register_policy(tracer);
// A hook that never stated what it does not check is a defect, not a detail: say so in
// shim.log as well as in the trace's meta line (docs/harness-audit.md).
for (const std::string& h : tracer.unstated_hooks())
Log("COVERAGE: hook %s registered with no coverage statement -- its compare results are "
"not trustworthy", h.c_str());
char exeSha[65] = {};
if (!shim::trace::sha256_file(exePath, exeSha)) Log("trace: could not hash %s", exePath);
if (tracer.open(SHIM_BUILD_ID, exeSha)) {
Log("trace: %s (default mode %s, inline_max %u, flush %s)", cfg.trace.path.c_str(),
shim::trace::mode_name(cfg.trace.default_mode), cfg.trace.inline_max,
cfg.trace.flush_always ? "always" : "lazy");
} else {
Log("trace: cannot open %s; template hooks forced off", cfg.trace.path.c_str());
}
InstallHooks(tracer);
// Self-test through the hook template (no MinHook involved): one record per launch proves
// the emitter/tracer inside the game process. `hook.Shim::SelfTest::Fill=off` silences it.
{
using H = shim::trace::Hook<shim::selftest::FillHook>;
H::configure(tracer);
const unsigned sum = shim::selftest::run_once(H::mode);
Log("selftest: %s mode=%s checksum=%08x records=%lu", shim::selftest::FillHook::name,
shim::trace::mode_name(H::mode), sum, static_cast<unsigned long>(tracer.records_written()));
}
}
void Shim_Shutdown() {
shim::hooks::watch_flush(&ShimLogLine);
Log("%s", shim::fpu::summary().c_str());
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
if (tracer.is_open()) {
Log("shutdown: trace records=%lu", static_cast<unsigned long>(tracer.records_written()));
tracer.close();
}
if (g_minhookUp) {
MH_STATUS st = MH_Uninitialize();
Log("shutdown: MH_Uninitialize -> %s", MH_StatusToString(st));
}
Log("==== shim detach ====");
if (g_log) std::fclose(g_log);
g_log = nullptr;
}
} // namespace
extern "C" BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID) {
switch (reason) {
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hinst);
Shim_Init(hinst);
break;
case DLL_PROCESS_DETACH:
Shim_Shutdown();
break;
}
return TRUE;
}