Appearance
aowlspt_codegen.h
Source: abi/aowlspt_codegen.h — 456 lines, 16 file-scope functions.
What this header owns
Reproduced verbatim from the header's own banner comment — these notes are frequently the only written record of why the subsystem is shaped the way it is.
text
aowlspt_codegen.h -- resolve a compiled method address from IL2CPP's OWN
per-assembly code table, at run time, when `MethodInfo.methodPointer` is null.
## The measured problem this exists for
On build 1.1.0.1.46777 `MethodInfo.methodPointer` (offset 0) reads NULL for
many methods that resolve perfectly by name. `il2cpp_class_from_name` and
`il2cpp_class_get_method_from_name` both SUCCEED; only the code pointer field
is empty. Measured live 2026-08-23 with the textures mod in `swapMode=probe`:
warn textures NOT hooking UnityEngine.AssetBundle::LoadAsset:
methodPointer is null - the host would refuse this too
and the same class of failure in the host's own startup lines ("no usable
code pointer for EFT.TarkovApplication::Update", GameWorld::Update,
CanvasUpdateRegistry::PerformUpdate, Time::get_deltaTime).
The host's only fallback was `scanMethodPointer` -- scan the first nine
qwords of the MethodInfo for an executable pointer in the il2cpp section.
That is a guess. It is flag-gated and its own log says it "may crash if it is
a wrong-but-real function". This header replaces the guess with the actual
mechanism IL2CPP uses.
## The mechanism (ground truth: tools/il2cpp_resolve.py)
IL2CPP stores compiled method addresses in a PER-ASSEMBLY table,
`Il2CppCodeGenModule.methodPointers`, indexed by `(token & 0xFFFFFF) - 1`.
A token's RID is per-IMAGE, so the table that must be used is the one
belonging to the image the method's DECLARING TYPE lives in. Using
Assembly-CSharp.dll's table for everything is a documented past bug in that
resolver (it misses BattlEye.BEClient, which is in
Assembly-CSharp-firstpass.dll, table base 0x186BF1380).
So the chain, all of it through the runtime's OWN exported C API except the
last hop:
MethodInfo* -> il2cpp_method_get_class -> Il2CppClass*
-> il2cpp_class_get_image -> Il2CppImage*
-> il2cpp_image_get_name -> "UnityEngine.AssetBundleModule.dll"
-> this table, matched BY NAME -> methodPointers, count
il2cpp_method_get_token(m) & 0xFFFFFF = rid
methodPointers[rid - 1] = the compiled function
Both `il2cpp_class_get_image` and `il2cpp_method_get_token` are confirmed
exported by this GameAssembly.dll (checked against the PE export directory:
386 exports, both present). That matters: it means this walk needs NO guessed
struct offset for MethodInfo, Il2CppClass or Il2CppImage. The only assumed
layout is `Il2CppCodeGenModule` itself, and that one is measured -- it is the
same layout `tools/il2cpp_resolve.py` reads, and that resolver reproduces all
five BE-bypass RVAs in `aowlspt_beclient.h` and every VA in `docs/SETTINGS.md`
byte-for-byte.
Il2CppCodeGenModule: +0x00 const char* moduleName
+0x08 uint32_t methodPointerCount
+0x10 void** methodPointers
## Where the table is
`Il2CppCodeRegistration` in `.rdata` holds `codeGenModulesCount` immediately
followed by `codeGenModules`. Located offline in GameAssembly.dll:
RVA 0x586EA78 uint32 codeGenModulesCount = 146 (0x92)
RVA 0x586EA80 void** codeGenModules -> VA 0x186BDC190 (RVA 0x6BDC190)
The count and the array pointer are READ from that pair rather than hardcoded
as an array address, and then the array is validated by walking it: 146 of
146 entries resolve to a readable module-name string, 145 ending in ".dll"
plus the one `__Generated` module. On any other build that validation fails
and every lookup here refuses.
## Static cross-check (offline, against tools/il2cpp_resolve.py)
Walking the array exactly as the code below does, versus the resolver's own
independent scan:
UnityEngine.AssetBundle::LoadAsset rid 6 -> 0x5250100 MATCH
UnityEngine.AssetBundle::LoadAsset rid 7 -> 0x5250340 MATCH
EFT.TarkovApplication::Update rid 50571 -> 0x977B10 MATCH
EFT.GameWorld::Update rid 39762 -> 0x2500A20 MATCH
UnityEngine.UI.CanvasUpdateRegistry::PerformUpdate rid 37 -> 0x539B340 MATCH
UnityEngine.Time::get_deltaTime rid 3094 -> 0x7E99B0 MATCH
BattlEye.BEClient::Update rid 433 -> 0x669390 MATCH
The last is the strongest: 0x669390 is an INDEPENDENTLY known-good RVA out of
`aowlspt_beclient.h`, and it lives in a different module than the rest, so it
only comes out right if the per-image table selection is right.
## Safety
Nothing here calls into the game and nothing here writes. Every hop is
`aowl_is_readable` (a VirtualQuery, not a faulting dereference), so there is
no SEH guard in this file at all and therefore nothing that could disarm an
outer `aowl_p_p_seh`. Every loop is capped. A returned pointer must be inside
the `il2cpp` PE section AND on an executable page, so a data pointer, a
vtable slot or a runtime stub cannot come back out. Every refusal sets a
reason code the host logs by name -- there is no silent decline.
## The caveat the host must keep printing
`aowlhost.nim`'s `bindMainDrain` records that a STATIC RVA out of this same
table, byte-verified, was bound on the live client and the drain never fired
(the game crashed seconds later). The later note at the static-RVA block
walks that back -- "the address is now KNOWN correct ... the earlier crash on
this exact target is unexplained". Both notes are in the tree and they do not
agree, and that disagreement has NOT been re-tested. That is why the host
gates this behind a default-OFF flag and why `aowl_codegen_agreement_*`
exists: when `methodPointer` IS readable the host compares the two and logs
whether they agree, so the live client -- not this comment -- settles it.
## ASLR: there is no 0x180000000 in this file, on purpose
GameAssembly.dll is RELOCATED -- it does not load at its preferred base
0x180000000. Measured live 2026-08-23: module base 0x7FFDB73F0000 (fact #29).
Reads against un-rebased static VAs all came back "not readable"; rebased,
every one succeeded. So a hardcoded preferred base here would be unreadable
at best and a wrong-but-mapped function at worst.
This walk cannot make that mistake, by construction:
- the ONE static address it uses, AOWL_CG_CODEREG_RVA, is an RVA added to
the LIVE base from GetModuleHandleA("GameAssembly.dll");
- everything after that -- the codeGenModules array pointer, each module
struct pointer, each moduleName, the methodPointers base and every slot
in it -- is a pointer READ OUT OF RELOCATED MEMORY. The loader has
already applied the relocations, so those are live addresses and there
is nothing left to rebase.
grep this file for 0x180000000: the only hit is the line below, in prose.
Corollary, also worth stating because it is easy to get backwards, and it was
got backwards once: `aowl_il2cpp_rva_of` in `aowlspt_bridge.h` returns
`p - GetModuleHandleA("GameAssembly.dll")`, i.e. a MODULE-relative RVA, NOT
one relative to the il2cpp section start (0x628000). That is the same space
`tools/il2cpp_resolve.py` prints, which is why the log lines this file's
callers emit can be pasted straight into a comparison with it. The live
inspector's `call`/fault RVAs are the OTHER space -- section-relative -- and
the two are not interchangeable, though both look reasonable in a log.
## Live confirmation of the whole chain (fact #28)
Hand-walked with the live inspector, module base 0x7FFDB73F0000:
UnityEngine.AssetBundleModule.dll methodPointers = static 0x187080AD0,
count 25, live 0x7FFDBE470AD0.
[0] live 0x7FFDBC63FF90 = base+0x524FF90 .ctor rid 1
[5] live 0x7FFDBC640100 = base+0x5250100 LoadAsset rid 6
[6] live 0x7FFDBC640340 = base+0x5250340 LoadAsset rid 7
and the bytes at base+0x5250100 read 48 89 5C 24 08 48 89 74 -- the
expected LoadAsset prologue. Those are the same table base, the same count
and the same two RIDs this file's offline cross-check produced.
RVAs are for imagebase 0x180000000 and build 1.1.0.1.46777.Constants
AOWLSPT_CODEGEN_HAOWL_CG_CODEREG_RVAAOWL_CG_MAX_MODULESAOWL_CG_MAX_NAMEAOWL_CG_MIN_VALID_MODSAOWL_CG_MOD_COUNT_OFFAOWL_CG_MOD_NAME_OFFAOWL_CG_MOD_PTRS_OFF
Functions
| Signature | Line |
|---|---|
char aowl_codegen_reason_text(int32_t r) | 200 |
int32_t aowl_cg_streq(const char* a, const char* b) | 254 |
int32_t aowl_cg_name_plausible(const char* a) | 270 |
int32_t aowl_codegen_init(void) | 286 |
int32_t aowl_codegen_ready(void) | 364 |
int32_t aowl_codegen_modules(void) | 365 |
int32_t aowl_codegen_valid_names(void) | 366 |
int32_t aowl_codegen_reason(void) | 367 |
int32_t aowl_codegen_attempts(void) | 368 |
int32_t aowl_codegen_waits(void) | 369 |
int32_t aowl_codegen_waiting(void) | 377 |
char aowl_codegen_reason_str(void) | 381 |
void aowl_codegen_lookup(const char* imageName, uint32_t token) | 390 |
int32_t aowl_codegen_note_agreement(void* live, void* resolved) | 447 |
int32_t aowl_codegen_agree_same(void) | 452 |
int32_t aowl_codegen_agree_diff(void) | 453 |

