Repository navigation
lcmem Mono Internals
.NET metadata does not store instance field offsets. Mono assigns them when
the type loads. lcmem rebuilds Mono's own layout algorithm. Two rules
matter, and both were confirmed against this game's live memory.
Inheritance. A MonoBehaviour subclass starts at +0x20: 0x10 of
object header, UnityEngine.Object.m_CachedPtr, then MonoBehaviour's own
IntPtr.
Reference fields come first. For a class with GC references, Mono uses a "GC aware" layout. It places reference-typed fields before all others, so the collector can scan one compact run of pointers. Declaration order is kept within each group.
Example: in CameraFXManager, CameraFOV is declared fourth but lives at
+0x54, after all six object references. Declaration order would put it at
+0x2C, where it reads as 6.85e-43: the high half of a pointer. That value
is the typical sign of a wrong layout.
$ python -m lcmem object 0x01E99F1C2420
0x01E99F1C2420 as CameraFX.CameraFXManager
+0x020 UnityEngine.Light _flashLight = 0x1E99F455920 (UnityEngine.Light)
+0x028 UnityEngine.Camera _camera = 0x1E99F52D720 (UnityEngine.Camera)
+0x050 u8 CameraFOVControl = 0
+0x054 f32 CameraFOV = 65.0
+0x058 f32 CameraFOVSpeed = 5.0
+0x05C f32 _defaultFov = 65.0
Every reference resolves to its declared type and every number is plausible. Hold a layout to that standard before you write to it.
Offsets are still a reconstruction. Confirm with object <addr> first.
Prediction stops at the first field whose size is not statically known;
those fields show no offset instead of a wrong one.
mono.read_list(addr) # List<T> of references, honouring _size
mono.read_array(addr) # T[] of references
mono.read_datetime(addr) # System.DateTime -> Python datetime
mono.read_guid(addr) # System.Guid
mono.read_string_field(a) # follow a string field, tolerating nullA class in Assembly-CSharp often inherits from one in VWW.CoreLibs.*.
AssemblySet loads several DLLs into one registry so those chains resolve:
from lcmem.monometa import AssemblySet
asm = AssemblySet.from_folder(managed) # 196 assemblies, 32k types
asm.get("ChatChannelInfo")-
heap_regions(): the process maps about 26 GB, but managed objects use about 1.1 GB. A two-stage sample finds that slice in about 5 s and cuts an instance sweep from about 250 s to about 18 s. It is only a hint: if it finds nothing, the caller sweeps everything. -
Session cache:
mono.save_cache(path)/load_cache(path)keep the discovered layout and class addresses, keyed by process ID. Boehm GC never moves objects, so they stay valid for the life of the process. Every cached value is checked again before use, because process IDs get reused.
lcmem/
win32.py ctypes bindings and privilege handling
process.py attach, modules, regions, typed read/write, Mono string reads
scanner.py first/refine scanning, numpy-backed, dense + sparse modes
monometa.py ECMA-335 parser for Assembly-CSharp.dll
mono.py runtime introspection: object -> vtable -> class -> name
patches.py declarative patches, AOB search, backup/revert
cli.py command line and interactive shell