Files
WeirdUtils/src/luagc/lua_offsets.zig
T
MarcelineVQ e996796d39 luagc: Lua 5.1 tri-color GC with full forward barrier coverage
Major port of Lua 5.1 incremental GC patterns:

Tri-color system:
- Two-white (WHITE0/WHITE1) with currentwhite flip in atomic phase
- Objects born currentwhite via hooked luaC_link (0x6F7B20)
- String birth mark patched to currentwhite at each flip
- isDead: otherwhite check with FIXED exception for WoW
- makewhite on survivors (preserves KEYWEAK/VALUEWEAK/FIXED)
- Bootstrap: first cycle sets all existing objects to currentwhite

Mark system (5.1 reallymarkobject/propagatemark):
- Strings: stringmark inline (no gray stack)
- Userdata: go black inline, mark metatable
- Tables: gray2black, traverse, black2gray if weak
- Threads: NEVER black, always grayagain (re-traversed in atomic)
- Closures: go black normally

Forward barriers (5.1 luaC_barrierf at every write site):
- OP_SETUPVAL: VM patch at 0x6F8A97 with naked asm trampoline
- lua_setmetatable (0x6F4020): hooked, barrier on new metatable
- lua_setupvalue (0x6F47B0): hooked, barrier grays closure
- lua_setfenv (0x6F40D0): hooked, barrier grays object

Backward barriers (5.1 luaC_barrierback):
- lua_table_set_value (0x6FA840): black2gray + grayagain
- lua_table_set_int_key (0x6FAD80): black2gray + grayagain

Atomic phase (5.1 ordering):
- propagateall, re-traverse weak tables, mark running thread,
  grayagain drain, separateudata, marktmu, cleartable, flip white

Status: enters world successfully with CHUNK_SIZE=5000 (incremental).
Crashes on UI reload (different crash pattern -- NULL+8 deref in mark,
not the old hash chain crash). Progress from "crashes on enter world"
to "crashes on UI reload."
2026-04-09 23:50:37 -07:00

359 lines
16 KiB
Zig

//! Hardcoded addresses and struct offsets for WoW 1.12.1's Lua 5.0.
//! All addresses are client-absolute (build 5875).
//!
//! All struct offsets below are verified via fresh Ghidra disassembly
//! of the mark_table (0x6F7590), mark_closure (0x6F77B0), mark_thread
//! (0x6F7860), and mark_proto (0x6F7710) functions. Each offset is
//! annotated with the exact instruction that proves it.
//!
//! DO NOT "correct" offsets based on the Lua 5.0 source without also
//! checking the disasm: WoW's layout diverges in a few places (TValue
//! is 16 bytes including padding; globals table TValue is at thread+0x40
//! rather than +0x38; etc.).
/// ---------------------------------------------------------------------------
// GC function addresses (all confirmed from cross-reference)
/// ---------------------------------------------------------------------------
pub const luaC_collectgarbage: usize = 0x6F7340;
pub const lua_gc_full_collection: usize = 0x6F73E0;
pub const lua_gc_remove_objects: usize = 0x6F7210;
pub const lua_gc_sweep_all_lists: usize = 0x6F72F0;
pub const lua_gc_shrink_memory: usize = 0x6F7370;
pub const luaCallUserDataGC: usize = 0x6F7080;
pub const lua_gc_free_object: usize = 0x6F7260;
/// Walks rootudata list. Moves dead udata with __gc metamethods to tmudata.
/// __fastcall: ECX = global_State*.
pub const luaC_separateudata: usize = 0x6F6FF0;
/// Re-marks tmudata list so __gc-pending userdata survive the sweep.
/// __fastcall: ECX = global_State*.
pub const marktmu: usize = 0x6F7470;
/// Clears weak-value table values in an atomic post-mark pass.
pub const cleartablevalues: usize = 0x6F7A20;
/// Clears weak-key table keys in an atomic post-mark pass.
pub const cleartablekeys: usize = 0x6F7970;
/// Resize string hash table. __fastcall(ECX=L, EDX=new_size).
pub const lua_string_hash_resize: usize = 0x6F9C50;
/// Forward barrier hook targets (Lua C API functions that create cross-object refs).
pub const lua_setmetatable: usize = 0x6F4020;
pub const lua_setupvalue: usize = 0x6F47B0;
pub const lua_pushcclosure: usize = 0x6F3920;
pub const lua_setfenv: usize = 0x6F40D0;
/// OP_SETUPVAL patch point: right after TValue copy, EDX=uv->v.
pub const vm_setupval_patch: usize = 0x6F8A97;
/// lua_setgcthreshold: __fastcall(ECX=L, EDX=newthreshold).
/// Writes (newthreshold << 10) to g->gcthreshold, then calls luaC_checkGC.
/// WoW calls this on screen transitions with threshold=256 (=262144 bytes).
pub const lua_setgcthreshold: usize = 0x6F4400;
/// lua_close (luaCloseState): __fastcall(ECX=L).
/// Calls luaF_close, luaC_sweep(L,1), frees g and L.
/// Must hook to reset incremental GC state before teardown.
pub const lua_close: usize = 0x6F6EF0;
/// Mark functions (for reference; zluagen reimplements these to support
/// incremental chunking).
pub const markroot: usize = 0x6F7AB0;
pub const propagatemarks: usize = 0x6F7510;
pub const mark_object_gray: usize = 0x6F74A0;
/// ---------------------------------------------------------------------------
// Write barrier hook addresses
/// ---------------------------------------------------------------------------
pub const lua_table_set_value: usize = 0x6FA840;
pub const lua_table_set_int_key: usize = 0x6FAD80;
/// ---------------------------------------------------------------------------
// Birth mark patch addresses
/// ---------------------------------------------------------------------------
/// luaC_link: __fastcall(ECX=L, EDX=obj, stack=tt). RET 0x4.
/// Links object to rootgc, sets marked and tt. Hooked for two-white birth.
pub const luaC_link: usize = 0x6F7B20;
/// `MOV byte [EDX+0x05], 0x00` inside luaC_link. Birth mark patch address.
pub const birth_mark_luaC_link: usize = 0x6F7B37;
/// `MOV byte [EBX+0x05], 0x00` inside lua_create_string_object. Same idea.
pub const birth_mark_string: usize = 0x6F9DC1;
/// ---------------------------------------------------------------------------
// global_State struct offsets
/// ---------------------------------------------------------------------------
/// stringtable struct starts at g+0x04 (after WoW's pool_ptrs at g+0x00).
/// Layout: {hash: GCObject**, nuse: int, size: int} = 12 bytes.
/// Verified: strt+12 = 0x04+12 = 0x10 = rootgc.
pub const GS_strt_hash: u32 = 0x04; // GCObject** bucket array
pub const GS_strt_nuse: u32 = 0x08; // int: number of strings
pub const GS_strt_size: u32 = 0x0C; // int: number of buckets
pub const GS_rootgc: u32 = 0x10; // main GC list head
pub const GS_rootudata: u32 = 0x14; // userdata list head
pub const GS_tmudata: u32 = 0x18; // finalizer-pending udata list
pub const GS_gcthreshold: u32 = 0x24; // bytes-alive threshold for next GC
pub const GS_totalbytes: u32 = 0x28; // currently allocated bytes
pub const GS_registry: u32 = 0x38; // registry table pointer
pub const GS_defaultmeta: u32 = 0x48; // default metatable pointer
pub const GS_mainthread: u32 = 0x50; // main lua_State*
/// ---------------------------------------------------------------------------
// GCObject common header (all GC types start with this prefix)
/// ---------------------------------------------------------------------------
pub const OBJ_next: u32 = 0x00; // linked list next pointer
pub const OBJ_tt: u32 = 0x04; // type tag (u8)
/// GC mark byte. Lua 5.1-style tri-color layout (ported from lgc.h):
///
/// bit 0: WHITE0 (white color type 0)
/// bit 1: WHITE1 (white color type 1)
/// bit 2: BLACK (fully traversed)
/// bit 3: KEYWEAK (tables with __mode 'k') / FINALIZED (userdata)
/// bit 4: FIXED (luaS_fix -- reserved words, tmnames; native writes this)
/// bit 5: VALUEWEAK (tables with __mode 'v')
/// bits 6-7: unused
///
/// Colors: WHITE = either white bit set. GRAY = no white, no black.
/// BLACK = bit 2 set. Dead = has "other white" (opposite of current).
///
/// CRITICAL: upvalues in Lua closures use the WHOLE BYTE as a "processed"
/// flag (TEST AL, AL at 0x6F7828). During mark, we set upval marked = 1
/// (which is WHITE0). The native check sees non-zero and skips. This is
/// compatible because we only need the skip behavior during a single cycle.
pub const OBJ_marked: u32 = 0x05;
// Tri-color bit positions (matching Lua 5.1 lgc.h)
pub const WHITE0BIT: u3 = 0;
pub const WHITE1BIT: u3 = 1;
pub const BLACKBIT: u3 = 2;
pub const KEYWEAKBIT: u3 = 3;
pub const FINALIZEDBIT: u3 = 3; // shared with KEYWEAK (different types)
pub const FIXEDBIT: u3 = 4;
pub const VALUEWEAKBIT: u3 = 5;
pub const WHITEBITS: u8 = (1 << WHITE0BIT) | (1 << WHITE1BIT); // 0x03
pub const KEYWEAK: u8 = 1 << KEYWEAKBIT; // 0x08
pub const VALUEWEAK: u8 = 1 << VALUEWEAKBIT; // 0x20
/// Mask to clear color bits (WHITE0|WHITE1|BLACK) while preserving flags.
/// Used by makewhite: marked = (marked & MASKMARKS) | currentwhite
pub const MASKMARKS: u8 = ~@as(u8, WHITEBITS | (1 << BLACKBIT)); // ~0x07 = 0xF8
/// ---------------------------------------------------------------------------
// Type tags (from propagate dispatch at 0x6F7510 + standard Lua 5.0)
/// ---------------------------------------------------------------------------
pub const LUA_TNONE: u8 = 0xFF; // -1 as u8, marks dead keys in hash nodes
pub const LUA_TNIL: u8 = 0;
pub const LUA_TBOOLEAN: u8 = 1;
pub const LUA_TLIGHTUSERDATA: u8 = 2;
pub const LUA_TNUMBER: u8 = 3;
pub const LUA_TSTRING: u8 = 4;
pub const LUA_TTABLE: u8 = 5;
pub const LUA_TFUNCTION: u8 = 6;
pub const LUA_TUSERDATA: u8 = 7;
pub const LUA_TTHREAD: u8 = 8;
pub const LUA_TPROTO: u8 = 9;
/// ---------------------------------------------------------------------------
// TValue (16 bytes in WoW Lua 5.0)
//
// Verified from mark_table array loop:
// 0x6F766A: SHL ESI, 0x4 ; count * 16 = TValue stride
// 0x6F7673: SUB ESI, 0x10 ; decrement by 16 per iteration
// 0x6F7680: MOV EAX, [EAX+0x8] ; gcptr at +0x08 (earlier CMP at +0x00 for tt)
/// ---------------------------------------------------------------------------
pub const TVALUE_size: u32 = 16;
pub const TVALUE_tt: u32 = 0x00;
pub const TVALUE_gcptr: u32 = 0x08;
/// ---------------------------------------------------------------------------
// Table struct
//
// Verified from mark_table 0x6F7590:
// 0x6F759D: MOV EDX, [EDI+0x08] ; metatable
// 0x6F7661: MOV EAX, [EDI+0x1C] ; sizearray (used as int count)
// 0x6F7670: MOV EAX, [EDI+0x0C] ; array pointer (dereferenced)
// 0x6F7697: MOV CL, [EDI+0x07] ; lsizenode (u8)
// 0x6F769A: MOV EAX, 1
// 0x6F769F: SHL EAX, CL ; sizenode = 1 << lsizenode
// 0x6F76B0: MOV ESI, [EDI+0x10] ; node pointer
/// ---------------------------------------------------------------------------
pub const TABLE_flags: u32 = 0x06; // TM cache / weak flags (also in marked byte)
pub const TABLE_lsizenode: u32 = 0x07; // u8: log2 of sizenode
pub const TABLE_metatable: u32 = 0x08; // Table* metatable
pub const TABLE_array: u32 = 0x0C; // TValue* array part
pub const TABLE_node: u32 = 0x10; // Node* hash part
pub const TABLE_sizearray: u32 = 0x1C; // int size of array part
/// ---------------------------------------------------------------------------
// Node (hash bucket, 40 bytes)
//
// Verified from mark_table node loop (0x6F76B0-0x6F76FE):
// 0x6F76A5: LEA EBX, [EAX+EAX*4] ; count*5
// 0x6F76A8: SHL EBX, 0x3 ; count*40 = Node size * count
// 0x6F76B6: MOV EAX, [ESI+EBX+0x10] ; value.tt at node+0x10
// 0x6F76BC: TEST EAX, EAX ; skip dead nodes (nil value)
// 0x6F76C0: CMP [ESI], 0x4 ; key.tt at node+0x00
// 0x6F76C5: MOV EDX, [ESI+0x08] ; key.gcptr at node+0x08
// 0x6F76E3: MOV EDX, [ESI+0x18] ; value.gcptr at node+0x18
//
// Iteration goes count-1 down to 0 (full 0-indexed range), NOT starting at 1.
/// ---------------------------------------------------------------------------
pub const NODE_size: u32 = 40;
pub const NODE_key_tt: u32 = 0x00;
pub const NODE_key_gcptr: u32 = 0x08;
pub const NODE_value_tt: u32 = 0x10;
pub const NODE_value_gcptr: u32 = 0x18;
pub const NODE_next: u32 = 0x20;
/// ---------------------------------------------------------------------------
// Closure (from mark_closure 0x6F77B0)
//
// Verified:
// 0x6F77B9: MOV AL, [EDI+0x06] ; isC flag
// 0x6F77C0: MOV AL, [EDI+0x07] ; nupvalues (u8)
//
// C closure branch (LEA ESI, [EDI+0x18] then [ESI-8]/[ESI] with stride 16):
// elements start at cl+0x10, each 16 bytes (TValue)
// tt at element+0x00, gcptr at element+0x08
//
// Lua closure branch (0x6F77F6-0x6F7854):
// 0x6F77F6: MOV EDX, [EDI+0x18] ; mark this pointer
// 0x6F7804: MOV EDX, [EDI+0x0C] ; mark this pointer
// 0x6F7820: LEA EBX, [EDI+0x20] ; upval pointer array start
// 0x6F7823: MOV ESI, [EBX] ; ESI = upvals[i] (the UpVal* itself)
// upval ptr array stride = 4 bytes per entry
/// ---------------------------------------------------------------------------
pub const CLOSURE_isC: u32 = 0x06;
pub const CLOSURE_nupvalues: u32 = 0x07;
/// First pointer marked for Lua closures (see disasm above).
pub const CLOSURE_lua_mark1: u32 = 0x18;
/// Second pointer marked for Lua closures.
pub const CLOSURE_lua_mark2: u32 = 0x0C;
/// C closure: upvalue array (inline TValues, each 16 bytes) starts here.
pub const CLOSURE_c_upvals: u32 = 0x10;
/// Lua closure: upvalue POINTER array (each 4 bytes = UpVal*) starts here.
pub const CLOSURE_lua_upval_ptrs: u32 = 0x20;
/// ---------------------------------------------------------------------------
// UpVal (verified from mark_closure's Lua upvalue loop 0x6F7820-0x6F7840)
//
// 0x6F7823: MOV ESI, [EBX] ; ESI = UpVal* (upvals[i])
// 0x6F7825: MOV AL, [ESI+0x05] ; marked byte
// 0x6F7828: TEST AL, AL ; any non-zero = skip (whole byte)
// 0x6F782C: CMP [ESI+0x10], 0x4 ; value.tt at upv+0x10
// 0x6F7832: MOV EDX, [ESI+0x18] ; value.gcptr at upv+0x18
// 0x6F7840: MOV byte [ESI+0x05], 0x01 ; mark as processed
/// ---------------------------------------------------------------------------
pub const UPVAL_marked: u32 = 0x05;
pub const UPVAL_value_tt: u32 = 0x10;
pub const UPVAL_value_gcptr: u32 = 0x18;
/// ---------------------------------------------------------------------------
// Thread / lua_State (from mark_thread 0x6F7860)
//
// Verified:
// 0x6F7869: CMP [EDI+0x40], 0x4 ; gt.tt at thread+0x40 (TValue)
// 0x6F7872: MOV EDX, [EDI+0x48] ; gt.gcptr at thread+0x48
// 0x6F7880: MOV EAX, [EDI+0x28] ; base_ci pointer (CallInfo*)
// 0x6F7883: MOV ESI, [EDI+0x14] ; ci pointer (CallInfo*)
// 0x6F7888: MOV ECX, [EDI+0x08] ; top (StkId)
// 0x6F78A6: MOV ESI, [EDI+0x1C] ; stack base (StkId)
//
// Stack iteration (0x6F78B0-0x6F78CE):
// stride 16 bytes per slot (TValue)
// tt at slot+0x00, gcptr at slot+0x08
// loop: from [thread+0x1C] to [thread+0x08]
//
// CallInfo iteration stride: 24 bytes (0x18) — used to find max stack top.
/// ---------------------------------------------------------------------------
pub const THREAD_top: u32 = 0x08; // StkId top
pub const THREAD_ci: u32 = 0x14; // CallInfo* current
pub const THREAD_stack: u32 = 0x1C; // StkId base (first slot)
pub const THREAD_base_ci: u32 = 0x28; // CallInfo* base
pub const THREAD_gt_tt: u32 = 0x40; // gt.tt (TValue holding globals)
pub const THREAD_gt_gcptr: u32 = 0x48; // gt.gcptr
/// ---------------------------------------------------------------------------
// Proto (from mark_proto 0x6F7710)
//
// Verified:
// 0x6F7714: MOV EAX, [ESI+0x20] ; source (TString*)
// 0x6F7727: MOV EAX, [ESI+0x08] ; k (constants array TValue*)
// 0x6F772A: tt read from EAX + offset (start of TValue)
// 0x6F772F: CMP EBX, 0x4 ; check tt == LUA_TSTRING (stringmark only)
// 0x6F7734: MOV EAX, [EAX+0x08] ; gcptr at TValue+0x08
// 0x6F773F: ADD EDX, 0x10 ; stride 16 (TValue)
// 0x6F773B: MOV EAX, [ESI+0x28] ; sizek at proto+0x28
// 0x6F7746: MOV EDX, [ESI+0x24] ; sizeupvalues at proto+0x24
// 0x6F7750: MOV EDX, [ESI+0x1C] ; upvalues array (TString*) at proto+0x1C
// 0x6F7762: MOV EAX, [ESI+0x34] ; sizep at proto+0x34
// 0x6F7770: MOV EAX, [ESI+0x10] ; p (nested Proto** array) at proto+0x10
// 0x6F7789: MOV EAX, [ESI+0x38] ; sizelocvars at proto+0x38
// 0x6F7794: MOV EAX, [ESI+0x18] ; locvars array at proto+0x18
// 0x6F77A2: ADD EDX, 0x0C ; LocVar stride 12
/// ---------------------------------------------------------------------------
pub const PROTO_k: u32 = 0x08; // TValue* constants array
pub const PROTO_p: u32 = 0x10; // Proto** nested protos array
pub const PROTO_locvars: u32 = 0x18; // LocVar* (12 bytes each)
pub const PROTO_upvalues: u32 = 0x1C; // TString** upvalue names
pub const PROTO_source: u32 = 0x20; // TString* source
pub const PROTO_sizeupvalues: u32 = 0x24; // int
pub const PROTO_sizek: u32 = 0x28; // int
pub const PROTO_sizep: u32 = 0x34; // int
pub const PROTO_sizelocvars: u32 = 0x38; // int
pub const LOCVAR_size: u32 = 12;
/// ---------------------------------------------------------------------------
// lua_State non-thread fields (not all threads, just the active L pointer)
/// ---------------------------------------------------------------------------
/// Pointer to global_State (at L + 0x10).
pub const L_global: u32 = 0x10;
/// Check if a lua_State is active. When the state is being destroyed,
/// the game stores 0 in L+0x60.
pub const L_active_check: u32 = 0x60;
/// ---------------------------------------------------------------------------
// Userdata (from reallymarkobject LUA_TUSERDATA case)
//
// Udata layout (Lua 5.0):
// CommonHeader (next+0x00, tt+0x04, marked+0x05)
// metatable at +0x08 (same offset as Table.metatable)
// len at +0x0C
/// ---------------------------------------------------------------------------
pub const UDATA_metatable: u32 = 0x08;
/// ---------------------------------------------------------------------------
// Constants
/// ---------------------------------------------------------------------------
/// Number of objects to process per incremental step.
/// ~200k objects in ~80ms = ~400ns/object → 5000 ≈ 2ms/step.
pub const CHUNK_SIZE: u32 = 5000;
/// Headroom added to totalbytes when setting gcthreshold between steps.
pub const BATCH_HEADROOM: u32 = 128 * 1024;