Appearance
aowlspt_callrva.h
Source: abi/aowlspt_callrva.h — 615 lines, 20 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_callrva.h -- calling a game method AT A STATIC RVA, from a mod.
WHY THIS EXISTS
===============
Our mods can read the game and cannot call it. Every "it declines" in the
registry traces to one missing capability: there is no supported way for a
mod to invoke a method whose address we know statically.
`aowlspt_fast.h` already solves the hard half -- 189 generated cases that
cover every register shape Win64 can present -- but its only entry is
`bindMethod`, which resolves BY NAME through the runtime. And fact #145 is
that a by-NAME route is fatal the moment it is USED: calling
`CameraManager::get_Instance` by name, binding `Physics::Raycast` by name and
patching `AddActivePLayer` by name each killed the client instantly and
silently. Resolving ~70 names is harmless; using one is not.
So the call path a mod needs is: an RVA it already has, byte-verified against
the bytes that RVA is DECLARED to start with, dispatched through the shape
table that already exists.
WHAT IS MEASURED HERE AND WHAT IS ASSERTED
==========================================
The by-value aggregate convention used to be the one guess in this project.
`mods/sain/client/live.nim` said so in `shapedWhy`: it ASSERTED the Win64
rule that an aggregate not of size 1/2/4/8 passes by hidden pointer, and had
never proved it against a real callee.
It is now MEASURED, from the bytes of three unrelated methods on this build
(`tools/il2cpp_resolve.py <GameAssembly.dll> <metadata> bytes <RVA> 32`).
These are the callee's own instructions, so they are not an inference about
the convention -- they ARE the convention, as compiled:
UnityEngine.Vector3::Dot(Vector3 lhs, Vector3 rhs) -> float @0x5297BF0
66 90 nop
F3 0F 10 41 04 movss xmm0, [rcx+4] <- lhs.y THROUGH RCX
F3 0F 59 42 04 mulss xmm0, [rdx+4] <- rhs.y THROUGH RDX
F3 0F 10 09 movss xmm1, [rcx] <- lhs.x
F3 0F 59 0A mulss xmm1, [rdx] <- rhs.x
F3 0F 10 51 08 movss xmm2, [rcx+8] <- lhs.z
F3 0F 59 52 08 mulss xmm2, [rdx+8] <- rhs.z
A by-value `Vector3` argument arrives as a POINTER in the ordinary
integer-class register for its position. The callee dereferences RCX and
RDX at +0/+4/+8. It is not a guess and it never was a coin flip; it simply
had never been read. The same bytes independently confirm the unboxed
Vector3 layout x@0 y@4 z@8, and that a static method has no `this`, so the
trailing MethodInfo* here is R8.
UnityEngine.Vector3::Cross(Vector3 lhs, Vector3 rhs) -> Vector3 @0x5297A60
F3 0F 10 5A 04 movss xmm3, [rdx+4] <- lhs is RDX, not RCX
F3 0F 10 42 08 movss xmm0, [rdx+8]
F3 41 0F 59 40 04 mulss xmm0, [r8+4] <- rhs is R8
F3 41 0F 59 58 08 mulss xmm3, [r8+8]
The arguments have SHIFTED ONE REGISTER RIGHT versus `Dot`, and the only
difference between the two signatures is that this one returns a 12-byte
struct. So RCX is the hidden return buffer: for a static method returning
an aggregate wider than 8 bytes the shape is
RCX = retbuf, RDX = arg0, R8 = arg1, R9 = MethodInfo*.
UnityEngine.Camera::WorldToScreenPoint(Vector3) -> Vector3 @0x525F940
48 89 5C 24 08 mov [rsp+8], rbx
57 push rdi
48 83 EC 40 sub rsp, 0x40
F2 41 0F 10 00 movsd xmm0, [r8] <- the ARGUMENT, through R8
33 C0 xor eax, eax
48 89 01 mov [rcx], rax <- writes the RETBUF, RCX
48 8B FA mov rdi, rdx <- `this`, RDX
89 41 08 mov [rcx+8], eax
48 8B D9 mov rbx, rcx
41 8B 40 08 mov eax, [r8+8] <- argument .z, through R8
The INSTANCE form of the same rule, in one function:
RCX = retbuf, RDX = this, R8 = arg0, R9 = MethodInfo*.
Note `movsd [r8]` + `mov eax,[r8+8]` = a 12-byte read in 8+4, which is
what a by-hidden-pointer Vector3 looks like and what a by-value one in a
register could not possibly look like.
UnityEngine.Physics::Raycast(Vector3 origin, Vector3 dir, float maxDist)
-> bool @0x5328830
48 8B DA mov rbx, rdx <- dir, a POINTER
0F 29 74 24 60 movaps [rsp+0x60], xmm6
48 8B F9 mov rdi, rcx <- origin, a POINTER
0F 28 F2 movaps xmm6, xmm2 <- maxDistance, XMM2
The float lands in XMM2 -- the register for POSITION 2 -- while positions
0 and 1 consume RCX and RDX as pointers. That is exactly the model
`aowlspt_fast.h` already implements: register class is chosen per slot by
a mask, and the slot INDEX picks the register in both files. Two by-value
Vector3s in one call are therefore two pointer slots and need no new
machinery at all -- only two buffers, which is what the arena below is.
MEASURED SINCE: 8 BYTES GOES IN THE REGISTER
--------------------------------------------
The 1/2/4/8-byte case -- the one Win64 passes IN the register rather than by
pointer -- used to be refused wholesale as unproven. The 8-byte half of it
is now measured, from `UnityEngine.Vector2::Dot(Vector2,Vector2)`@0x529BB60,
whose entire body spills RCX and RDX to the stack and reads the two floats
out of the SPILLED BYTES without ever dereferencing either register. The
bytes are quoted in full at `aowl_crva_agg_class` below, and the live check
is `Dot((1,2),(3,4)) == 11.0` exactly.
1, 2 and 4 bytes stay REFUSED. The same ABI rule covers them, and that is
precisely the extrapolation that had 8 bytes filed as unknowable while the
answer sat in a callee we could already read. Nothing needs them yet;
whatever needs one first should measure it, not inherit this sentence.
MEASURED SINCE: THE STACK, NOT JUST THE REGISTERS
-------------------------------------------------
A call was capped at five slots and a wider shape was refused rather than
spilled, which blocked every 8- and 9-slot method. `aowlspt_fast.h` now
emits stack cases up to twelve slots. Shadow-space size, stack-slot stride
and argument order were read off `UnityEngine.Matrix4x4::Ortho`@0x5294C30,
which takes three of its six floats on the stack; see the header of
`tools/gen_fast_table.py` for the three `movss` instructions that fix all
three numbers. Nothing here writes assembly: every case is a C call through
a full prototype, so the compiler owns alignment and cleanup.
SAFETY
======
Everything a live path must satisfy, satisfied here rather than in each mod:
1. 16-byte prologue byte-verify, against a CAPTURE-ONCE SNAPSHOT and never
against live memory, so a second feature detouring the same function
does not make this one self-reject and blame the game build.
2. VirtualQuery on every hop: the code page, and every argument buffer.
3. ONE guard around the whole call, and it REFUSES TO ARM if a guard is
already armed on this thread rather than nesting -- nesting disarms the
outer one, which removes protection instead of adding it.
4. No iteration that is not capped.
5/6. The mod owns the flag; `aowl_crva_fault_count` is here so a mod can
self-disable after N faults without inventing its own counter.
7. The argument arena is static and thread-local: no allocation per call,
managed or otherwise.
8. Nothing is written anywhere except into the arena this file owns.
WHAT THIS FILE DELIBERATELY WILL NOT DO
=======================================
It will not resolve a NAME. Not one. That is fact #145 and it is also the
job of the offline symbol table being built separately -- see
`aowl_crva_target` below for the shape this consumes when that lands.
It will not DETOUR anything. Calling a shared RVA is fine (it is correct
code for the receiver passed); detouring one is a write with unbounded blast
radius, and two detours on one function overwrite each other's trampoline.
This header only ever reads the code page.Constants
AOWLSPT_CALLRVA_HAOWL_CRVA_AGG_INREGAOWL_CRVA_AGG_POINTERAOWL_CRVA_AGG_REFUSEAOWL_CRVA_ARENA_BYTESAOWL_CRVA_ARENA_SLOTSAOWL_CRVA_BAD_ARGAOWL_CRVA_DETOUREDAOWL_CRVA_FAULTEDAOWL_CRVA_GUARD_BUSYAOWL_CRVA_MISMATCHAOWL_CRVA_NOT_CODEAOWL_CRVA_NO_MODULEAOWL_CRVA_NO_SIGAOWL_CRVA_OKAOWL_CRVA_OUT_OF_IMAGEAOWL_CRVA_SHAPEAOWL_CRVA_SIG_BYTESAOWL_CRVA_SNAP_ROWSAOWL_CRVA_WRONG_SECTION
Functions
| Signature | Line |
|---|---|
char aowl_crva_reason(int32_t r) | 202 |
void aowl_crva_scan_sections(void) | 229 |
unsigned char aowl_crva_base(void) | 258 |
void aowl_crva_code(uint32_t rva, int32_t* why) | 265 |
int32_t aowl_crva_looks_detoured(const unsigned char* p) | 290 |
AowlCrvaSnap aowl_crva_snap_find(uint32_t rva) | 323 |
AowlCrvaSnap aowl_crva_snap_take(uint32_t rva, const unsigned char* p) | 332 |
int32_t aowl_crva_verify(AowlCrvaTarget* t) | 376 |
void aowl_crva_cell(int32_t i) | 419 |
void aowl_crva_cell_zero(int32_t i, int32_t nbytes) | 424 |
int32_t aowl_crva_agg_class(int32_t size) | 472 |
int32_t aowl_crva_agg_by_pointer(int32_t size) | 483 |
int32_t aowl_crva_agg8(const void* src, int64_t* out) | 492 |
void aowl_crva_arg_agg(int32_t i, const void* src, int32_t size) | 504 |
void aowl_crva_arg_out(int32_t i, int32_t size) | 521 |
float aowl_crva_cell_f32(int32_t i, int32_t off, int32_t* ok) | 528 |
int32_t aowl_crva_cell_i32(int32_t i, int32_t off, int32_t* ok) | 538 |
LONG CALLBACK aowl_crva_veh(PEXCEPTION_POINTERS ep) | 564 |
int32_t aowl_crva_fault_count(void) | 573 |
int32_t aowl_crva_invoke(AowlCrvaTarget* t, void* mi, int32_t nslots, uint32_t mask, const AowlFastSlot* a, int32_t retclass, /* 0=int/ptr 1=float 2=double */ int64_t* out_g, double* out_f) | 579 |

