37 KiB
r2unity - IL2CPP Wire Versions, Layout Deltas, And Unused Metadata
This document is a technical reference for:
- Which
global-metadata.datwire versions exist, what changed in each, which fractional sub-versions affect the on-disk layout, and which Unity release each one maps to. - Which logical records inside
global-metadata.datand which native-binary structures are declared but not yet decoded by r2unity, with enough detail to add decoders against without re-reading Il2CppDumper.
The numbers and field deltas below are cross-checked against three authoritative reference implementations vendored or cited:
third_party/Il2CppDumper/— C# reference (Il2CppDumper/Il2Cpp/MetadataClass.csis the per-version struct layout source of truth). https://github.com/Perfare/Il2CppDumperthird_party/lib-global-metadata/— Java rewrite, independent cross-check of every record range. https://github.com/REAndroid/lib-global-metadata- djkaty/Il2CppInspector — public Unity-version matrix. https://github.com/djkaty/Il2CppInspector
1. IL2CPP metadata wire versions
1.1 Wire versions observed on disk
The int32_t at file offset 4 of global-metadata.dat has only
ever taken these values:
16, 19, 20, 21, 22, 23, 24, 27, 29, 31
17, 18, 25, 26, 28, 30 were reserved but never shipped. The
Il2CppDumper dispatcher
(third_party/Il2CppDumper/Il2Cpp/Metadata.cs:55) rejects anything
outside [16, 31]:
if (version < 16 || version > 31)
throw new NotSupportedException(...);
1.2 Fractional sub-versions
Within each wire version Il2CppDumper tracks fractional sub-versions
(24.1, 24.2, 24.3, 24.4, 24.5, 27.1, 27.2, 29.1).
These are not written to disk. They are reverse-engineered
signatures inferred by probing:
- section sizes divided by row counts (e.g.
assembliesSize / imageCount→ 48 B or 52 B tail) - field counts and pointer layouts on the native side
(
CodeRegistration/MetadataRegistrationstruct size probes) - the presence of specific arrays in the binary (
genericAdjustorThunksexists from v24.5 onward)
For most wire versions the on-disk .dat is byte-identical across
the fractional family; the sub-version only selects how the native
binary's registration structures are laid out. A few exceptions do
leak into the .dat:
Il2CppRGCTXDefinition._datawidens fromint32→uint64at v27.2 (reference:Il2CppDumper/Il2Cpp/MetadataClass.cs,type_post29+_datafields). Row grows from 8 B to 12 B/16 B.Il2CppMethodDefinitiongainsreturnParameterTokenat v31. Row grows from 32 B to 36 B.- All
customAttributeIndexfields (onTypeDef,MethodDef,FieldDef,ParameterDef,EventDef,PropertyDef,AssemblyDef,ImageDef) are removed at v24.1 in favour of token-based lookup. Affects row sizes of every def type. Il2CppAssemblyNameDefinition.hashValueIndexis removed at v24.3+, shrinking assembly rows by 4 B.- Attribute tables (
attributesInfo+attributeTypes) are replaced by a BLOB layout (attributeDataRange+attributeData) at v29. Same header slots, entirely new semantics.
1.3 Unity release → wire-version mapping
Reconstructed from Il2CppDumper's probe logic and Il2CppInspector's published matrix. "Wire version" is what lands on disk; "Sub-version" is the fractional binary-side refinement.
| Unity release | Wire version | Sub-version | Notes |
|---|---|---|---|
| 5.3.0 – 5.3.5 | 21 | 21 | First public IL2CPP on iOS. |
| 5.3.6 – 5.4 | 22 | 22 | Drops delegate-wrapper tables; adds unresolvedVirtualCall*. |
| 5.5 | 23 | 23 | Adds interopData, WinRT tables. |
| 5.6 – 2017.x – 2018.2 | 24 | 24 | Adds exportedTypeDefinitions. |
| 2018.3 – 2018.4 | 24 | 24.1 | Token-based attribute ranges; drops customAttributeIndex from defs. |
| 2019.1 – 2019.3.6 | 24 | 24.2 | Il2CppCodeGenModule introduced; per-image method tables. |
| 2019.3.7 – 2019.4.14 | 24 | 24.3 | WinRT factory table. |
| 2019.4.15 – 2019.4.20 | 24 | 24.4 | genericAdjustorThunks added; hashValueIndex dropped. |
| 2019.4.21+ | 24 | 24.5 | Drops customAttributeGenerators from CodeRegistration. |
| 2020.1 | 24 | 24.4 | Same layout as 2019.4.15. |
| 2020.2 – 2020.3 | 27 | 27 | customAttributeCacheGenerator moves per-image into CodeGenModule. |
| 2021.1 | 27 | 27.1 | Re-adds genericAdjustorThunks. |
| 2021.2 – 2021.3 | 27 | 27.2 | RGCTX widens to 64 bits on disk; Il2CppType repacks valuetype bit. |
| 2022.1 | 29 | 29 | Inline custom-attribute BLOB; customAttributeGenerators removed. |
| 2022.2 – 2022.3 | 29 | 29.1 | unresolvedVirtualCallPointers splits into instance + static arrays. |
| 2023.1 – 2023.2 | 31 | 31 | Il2CppMethodDefinition adds returnParameterToken. |
| 6000.x (Unity 6) | 31 | 31 | Provisional — no on-disk change known. |
Il2CppDumper's bundled README claims "Unity 5.3 – 2022.2", i.e.
wire versions 21–29. Support for wire v31 (Unity 2023+) is present
in current master via the [Version(Min = 31)] annotation on
returnParameterToken in MetadataClass.cs.
1.4 r2unity's current acceptance band
From r2unity_parse_metadata() in src/lib/lib.c:
if (version < 24 || version > 31) return NULL;
if (version < 27) header_size = sizeof (v24);
else if (version < 29) header_size = sizeof (v27);
else header_size = sizeof (v29); /* 29, 30, 31 */
/* After normalizing header fields, reject v24.0 via image-stride probe. */
if (version == 24 && is_v24_0 (meta)) return NULL;
| Wire version | Status | Reality |
|---|---|---|
| 16, 19, 20 | Rejected | Pre-Unity-5.5 builds, effectively extinct. |
| 21, 22, 23 | Rejected | Unity 5.3 – 5.5 IPAs exist but are rare. |
| 24.0 | Rejected after sub-version probe | On-disk version is 24 for both v24.0 and v24.1+. The parser probes image-row stride (32 B vs 40 B) and rejects v24.0 with an explicit error; TypeDefinition/MethodDefinition/ImageDefinition rows would otherwise decode at wrong offsets. |
| 24.1 – 24.5 | Accepted via _v24 header struct |
Row decoders target the v24.1+ strides. |
| 27 | Accepted via _v27 header struct |
Header correct. Row decoders still assume the v24.1+ layout; v27.2 RGCTX widening not handled (only affects RGCTX which is unused). |
| 29 | Accepted via _v29 header struct |
Header correct. Method rows decoded as 32 B. Reverse-P/Invoke BLOB path is the only v29-specific code. |
| 30 | Accepted | Never shipped on disk; dead slot, safe. |
| 31 | Accepted | Method rows decoded as 36 B (dedicated v31 branch); other row types still assume v24.1+ layout. |
Bottom line: the acceptance band covers every commercially relevant Unity build from 2017 onward, but within the band the fixed 88 / 32 / 40 / 48–52 strides in the row decoders are correct only for a subset. A per-version size table is the single highest-priority portability fix. The required values are itemised in §4.
1.5 Architecture-independence of global-metadata.dat
The file contains zero pointers. Every scalar is 16- or 32-bit
little-endian; inline fixed arrays (public_key_token[8]) are
byte-addressed. The reference implementations (Il2CppDumper,
REAndroid Java port) both read the file with unconditional
ReadInt32() / ReadUInt16(); there is no architecture branch.
Therefore, for a given wire version, global-metadata.dat is
byte-identical across:
- iOS ARM64 / ARM64e
- Android arm64-v8a
- Android armv7a (32-bit)
- Android x86 / x86_64 (emulator)
- Windows x64 (
GameAssembly.dll) - macOS x64 / arm64 standalone (
GameAssembly.dylib) - Linux x64 (
GameAssembly.so) - UWP / HoloLens
- Nintendo Switch
- WebAssembly (wasm32)
r2unity's RD_LE32 / RD_LE16 helpers are correct for every
target. The only endianness concern is a big-endian host, which the
in-place r_read_le32 sweep already handles.
The native side is not architecture-independent:
Il2CppCodeRegistration, Il2CppMetadataRegistration,
Il2CppCodeGenModule, and Il2CppInteropData all contain
host-width ulong pointers. Format and relocation semantics differ
between ELF, Mach-O and PE:
- 8-byte pointers: iOS ARM64/ARM64e, Android arm64-v8a, Windows x64, macOS, Switch, Linux x64.
- 4-byte pointers: Android armv7a, Android x86, WebAssembly (wasm32).
r2unity's ELF/Mach-O paths handle 64-bit correctly; 32-bit Android ELF is parsed but not validated; WebAssembly is unsupported.
2. Tables declared but not decoded
src/lib/lib.h already lays out the full
Il2CppGlobalMetadataHeader with every offset/size pair, but
src/lib/lib.c consumes only six of them in general (string,
stringLiteral, stringLiteralData, methods, typeDefinitions,
images) plus assemblies / referencedAssemblies / v29
attributesInfo+attributeTypes for SBOM and reverse-P/Invoke.
Everything else is dead weight today.
Each subsection below gives the on-disk shape, the indexing invariant, and the reverse-engineering value of decoding it.
2.1 parameters — Il2CppParameterDefinition[]
uint32_t nameIndex;
uint32_t token; /* 0x08?????? */
int32_t customAttributeIndex; /* removed at v24.1 */
int32_t typeIndex; /* -> MetadataRegistration.types */
Size: 16 B (v≤24.0) / 12 B (v≥24.1). Sliced by
methodDef.parameterStart, length methodDef.parameterCount.
Value: every method currently dumps as Method(). With this table
we emit real C# signatures Foo(string name, int count, Vector3 pos). Combined with parameterDefaultValues (§2.3) we also
recover optional parameters. This is the highest-leverage table
r2unity does not read today.
2.2 fields — Il2CppFieldDefinition[]
uint32_t nameIndex;
int32_t typeIndex; /* -> MetadataRegistration.types */
int32_t customAttributeIndex; /* removed at v24.1 */
uint32_t token; /* 0x04??????, added at v19 */
Size: 12 B (v≤24.0) / 8 B (v≥24.1). Sliced by
typeDef.fieldStart / typeDef.field_count.
Value: names and types for every field. Indirectly unlocks
MetadataRegistration.fieldOffsets (§3.12), which is what actually
lets us label ldr X, [x0, #0x18] as Player.health. Without
fields, struct-internal code is unreadable.
2.3 fieldDefaultValues / parameterDefaultValues + BLOB
typedef struct { int fieldIndex; int typeIndex; int dataIndex; } Il2CppFieldDefaultValue;
typedef struct { int parameterIndex; int typeIndex; int dataIndex; } Il2CppParameterDefaultValue;
Each entry indexes into
fieldAndParameterDefaultValueData (raw BLOB heap), decoded by
Il2CppExecutor.GetConstantValueFromBlob
(Il2CppDumper/Utils/Il2CppExecutor.cs:343). The decoder handles
ECMA-335 primitive type codes (ECMA-335 §II.23.1.16):
I1/U1/I2/U2/I4/U4/I8/U8 — little-endian scalars
R4/R8 — IEEE-754 floats
STRING — compressed-int length + UTF-8 bytes
CHAR/BOOLEAN — 2 B / 1 B
SZARRAY — compressed length + elements
IL2CPP_TYPE_INDEX — type reference
CLASS — null reference marker
v29 reuses this same compressed-int scheme for the custom-attribute BLOB (§2.9).
Value: every const string URL = "...", static readonly int MaxHp = 42, and default argument value. Major for finding hardcoded
endpoints, feature flags, encryption constants.
2.4 properties — Il2CppPropertyDefinition[]
uint32_t nameIndex;
int32_t get; /* MethodDefIndex, -1 if no getter */
int32_t set; /* MethodDefIndex, -1 if no setter */
uint32_t attrs; /* MethodSemanticsAttributes, ECMA-335 §II.23.1.12 */
int32_t customAttributeIndex; /* ≤24 */
uint32_t token; /* ≥19, 0x17?????? */
Value: rejoin get_X / set_X method pairs into a single C#
property. Auto-properties identifiable via <X>k__BackingField
patterns. [JsonProperty] / [SerializeField] most often decorate
properties.
2.5 events — Il2CppEventDefinition[]
uint32_t nameIndex;
int32_t typeIndex; /* -> MetadataRegistration.types */
int32_t add; /* MethodDefIndex for add_X */
int32_t remove; /* MethodDefIndex for remove_X */
int32_t raise; /* MethodDefIndex for raise_X, often -1 */
int32_t customAttributeIndex; /* ≤24 */
uint32_t token; /* ≥19, 0x14?????? */
Value: C# event declarations. Labels every add_ / remove_
delegate-subscription method. Unity UnityEvents, C# events on
MonoBehaviour, INotifyPropertyChanged patterns.
2.6 Generics: genericContainers + genericParameters +
genericParameterConstraints
typedef struct { /* Il2CppGenericContainer, 16 B */
int32_t ownerIndex; /* TypeDef or MethodDef index */
int32_t type_argc;
int32_t is_method; /* 0 class, 1 method */
int32_t genericParameterStart;
} Il2CppGenericContainer;
typedef struct { /* Il2CppGenericParameter, 16 B */
int32_t ownerIndex;
uint32_t nameIndex; /* "T", "TKey", … */
int16_t constraintsStart;
int16_t constraintsCount;
uint16_t num; /* argument position */
uint16_t flags; /* GenericParameterAttributes
(variance + constraint kinds),
ECMA-335 §II.23.1.7 */
} Il2CppGenericParameter;
/* genericParameterConstraints: raw int32_t[] of type indices */
Value: reconstruct Dictionary<TKey, TValue> where TKey : IComparable<TKey>. Gate for using methodSpecs / genericInsts
(§3) to emit real instantiation names like List<int>::Add.
2.7 interfaces + interfaceOffsets + vtableMethods
-
interfaces— rawint32_t[]; per-type viatypeDef.interfacesStart/typeDef.interfaces_count. Each entry is a type index (intoMetadataRegistration.types). -
interfaceOffsets—Il2CppInterfaceOffsetPair[] = { int interfaceTypeIndex; int offset; }(8 B). Per-type viatypeDef.interfaceOffsetsStart/interface_offsets_count. Maps each implemented interface to the vtable slot where its methods begin. -
vtableMethods— rawuint32_t[]; each slot is anEncodedMethodIndex:usageType = (idx & 0xE0000000) >> 29; /* Il2CppMetadataUsage */ decoded = (version >= 27) ? ((idx & 0x1FFFFFFE) >> 1) : (idx & 0x1FFFFFFF);usageType == 3→ concrete method definition,usageType == 6→methodSpecs(§3.11) index.
Value: fully resolved virtual dispatch. Every callvirt / indexed
vtable load gets a real method name. class Foo : IDisposable, IEnumerable<int> declarations. Find-all-implementers searches for a
given interface.
2.8 nestedTypes — TypeDefinitionIndex[]
Raw int32_t[]; per-type via typeDef.nestedTypesStart /
nested_type_count. Reconstructs Outer.Inner hierarchies —
important for accurate symbol naming in compiler-generated types
(<DisplayClass> etc.).
2.9 Attributes — two very different encodings
Pre-v29 (wire 21..27.2) — token-range table + flat type list + native-side generators:
attributesInfo—Il2CppCustomAttributeTypeRange[] = { uint token; int start; int count; }(12 B; 8 B pre-24.1 without the token).attributeTypes— flatint32_t[]of attribute type indices (intoMetadataRegistration.types).CodeRegistration.customAttributeGenerators— onevoid(void*)function pointer per range. Its bodynews up the attribute instance(s), baking constructor arguments as immediate operands. Reading the thunk recovers the arguments; this is what Il2CppDumper'sGetCustomAttributeValuesFromGeneratordoes.
v29+ — inline BLOB:
attributesInfo*header slots repurposed asattributeDataRange:{ uint token; uint startOffset; }array.attributeTypes*header slots repurposed asattributeData: raw BLOB heap.- BLOB encoding matches ECMA-335 §II.23.3
CustomAttribextended with IL2CPP's 7-bit compressed-int (same as ECMA-335 §II.23.2): leading compressed count of constructor references; each ref encoded as a method index plus inline-serialized argument values. - Decoded by
Il2CppDumper/Utils/CustomAttributeDataReader.cs. customAttributeGeneratorsno longer exists inCodeRegistration.
Value: [Obsolete], [Serializable], [SerializeField],
[Preserve], [RequireComponent(typeof(Rigidbody))],
[RuntimeInitializeOnLoadMethod],
[DllImport("x.dll",EntryPoint="y")]. [Preserve] in particular
flags code reachable only via reflection — a high-value RE signal.
Cross-reference: https://ecma-international.org/publications-and-standards/standards/ecma-335/
2.10 Pre-v27 metadata usage tables
metadataUsageLists—Il2CppMetadataUsageList[] = { uint start; uint count; }(8 B).metadataUsagePairs—Il2CppMetadataUsagePair[] = { uint destinationIndex; uint encodedSourceIndex; }(8 B).
encodedSourceIndex's top three bits encode Il2CppMetadataUsage ∈ { Invalid, TypeInfo, Il2CppType, MethodDef, FieldInfo, StringLiteral, MethodRef }. destinationIndex is the slot in the
native MetadataRegistration.metadataUsages[] pointer table (§3.14).
Value: on v19–v26 builds, the compiled code is full of ldr X, [g_MetadataUsages + offset] loads. These tables plus
metadataUsages together let a decoder label each slot as
[TypeInfo] System.String, [StringLiteral_42] "hello world", etc.
From v27+ the mechanism was replaced by per-site inline globals
lazily initialized by Il2CppCodeGenModule.moduleInitializer, and
these tables disappear from the .dat.
2.11 assemblies + referencedAssemblies
typedef struct { /* Il2CppAssemblyDefinition */
int32_t imageIndex;
uint32_t token; /* ≥24.1, 0x20?????? */
int32_t customAttributeIndex; /* ≤24 */
int32_t referencedAssemblyStart; /* ≥20 */
int32_t referencedAssemblyCount; /* ≥20 */
Il2CppAssemblyNameDefinition aname;
} Il2CppAssemblyDefinition;
typedef struct { /* Il2CppAssemblyNameDefinition */
uint32_t nameIndex, cultureIndex, publicKeyIndex;
uint32_t hashValueIndex; /* removed at v24.3+ */
uint32_t hash_alg; /* AssemblyHashAlgorithm enum */
int32_t hash_len;
uint32_t flags; /* AssemblyFlags, ECMA-335 §II.22.2 */
int32_t major, minor, build, revision;
uint8_t public_key_token[8]; /* low 8 bytes of SHA1(publicKey), byte-reversed */
} Il2CppAssemblyNameDefinition;
Value: distinguish Unity-shipped assemblies (mscorlib.dll,
UnityEngine.*, UnityEditor.*) from game assemblies. Recover
AssemblyVersion quadruples and strong-name public-key tokens.
Dependency graph between assemblies — needed for emitting a
compilable C# skeleton or for filtering dumps by assembly.
This is the one large "still-declared" table r2unity does
decode today, and it drives the -S SBOM output.
2.12 fieldRefs (v19+)
typedef struct { int typeIndex; int fieldIndex; } Il2CppFieldRef;
Canonical list of field references used at metadata-usage
destinations, typically for fields of generic-instantiation types
that cannot be addressed directly by (typeDef, fieldDef).
Dereferences the FieldInfo kind of metadata-usage slot (§2.10).
2.13 exportedTypeDefinitions (v24+)
Raw TypeDefinitionIndex[]. Per-image via
imageDef.exportedTypeStart / exportedTypeCount. These are .NET
type forwarders — matters for faithfully reproducing runtime
assemblies (System.Runtime.dll re-exports into mscorlib.dll).
Optional for most application-level dumping.
2.14 RGCTX tables (≥v24.1 on disk; moved to
codeGenModule from v24.2)
typedef struct { /* Il2CppRGCTXDefinition (v≤27.1) */
int32_t type; /* Il2CppRGCTXDataType enum */
Il2CppRGCTXDefinitionData data; /* typically int rgctxDataDummy */
} Il2CppRGCTXDefinition; /* 8 B */
/* v≥27.2: type_post29 widens to ulong; _data widens to ulong -> 16 B */
Il2CppRGCTXDataType = { Invalid, Type, Class, Method, Array, Constrained }. Per-type via typeDef.rgctxStartIndex /
rgctxCount (pre-v24.1); per-method similarly. From v24.2 onward
these tables move per-image into
Il2CppCodeGenModule.rgctxs + rgctxRanges.
Value: Runtime Generic Context — per-instantiation lookup table.
Resolves generic-virtual-dispatch sites and typeof(T) /
default(T) accesses inside generic methods. "The 3rd RGCTX slot
of List<T>::Sort" → concrete type/method resolution.
2.15 fieldMarshaledSizes
typedef struct { int fieldIndex; int typeIndex; int size; } Il2CppFieldMarshaledSize;
Marshalled size for [MarshalAs] / [StructLayout] interop
structs. Relevant only when the game talks heavily to native
plugins via custom marshalling; safe to ignore for plain RE.
2.16 unresolvedVirtualCallParameterTypes /
unresolvedVirtualCallParameterRanges (v22+)
/* Types: raw TypeIndex[] */
typedef struct { int start; int length; } Il2CppRange;
Correlates with
CodeRegistration.unresolvedVirtualCallPointers (§3.5 of
r2unity.md). Lets us name each unresolved-virtual-call stub with
its parameter signature.
2.17 windowsRuntimeTypeNames + windowsRuntimeStrings +
windowsRuntimeFactoryTable
UWP / HoloLens only. For iOS + Android + desktop builds these sizes are always zero; a defensive parser bounds-checks and skips.
3. Native binary — pointer arrays we don't follow
src/lib/elf.c and src/lib/macho.c currently locate exactly one
array: CodeRegistration.methodPointers (or its v24.2+ per-image
equivalent, if the heuristic happens to land on the right
Il2CppCodeGenModule). The registration structures actually expose
many more pointer arrays, each with its own metadata table partner.
Each entry below is a { ulong count; ulong ptr; } pair inside the
registration (see §3 of doc/r2unity.md for the full struct).
3.1 methodPointers (≥v24.1 global; v≥24.2 per-module)
What r2unity extracts today. From v24.2 onwards this field is on
Il2CppCodeGenModule, not CodeRegistration, and there is one
module per image. If r2unity finds a single {count, ptr} on a
v24.2+ binary it is only extracting one image's methods.
Walking codeGenModules[] and enumerating each module is mandatory
for full coverage.
3.2 invokerPointers
Reflection-dispatch invokers — one per unique method-signature
shape (return type + param count + blittable-ness). Methods
reference them via methodDef.invokerIndex (≤v24.1) or
Il2CppCodeGenModule.invokerIndices[] (v≥24.2).
Value: naming invokers doesn't rename methods directly, but it clusters methods by signature and makes reflection-based hook points discoverable.
3.3 customAttributeGenerators (pre-v29)
Per-range void(void*) — each one news up the attribute objects
for one range. Given attributesInfo (§2.9), a tiny
arch-specific interpreter walking the generator body recovers every
attribute argument (strings, typeof references, enums). Required
for pre-v29 reverse-P/Invoke DLL-name recovery.
3.4 reversePInvokeWrappers (v22+)
Array of native-to-managed thunks for [MonoPInvokeCallback]
methods. Every callback the game registers with a C library routes
through these. Naming them ReversePInvoke_<method> lights up
native plugin callback surfaces (ads SDKs, analytics, audio,
networking).
3.5 genericMethodPointers + genericAdjustorThunks
Array of function pointers for every generic-method instantiation
present in the binary. Bridged to metadata via
MetadataRegistration.genericMethodTable and methodSpecs:
foreach (genericMethodTable[k]) {
spec = methodSpecs[k.genericMethodIndex];
addr = genericMethodPointers[k.indices.methodIndex];
/* emit "List<int>::Add @ 0x401234" */
}
genericAdjustorThunks (v24.5, v27.1+) are struct-vs-class T
fixup thunks: mov x0, [x0, #8]; b real_body (skip the
Il2CppObject header for value-type T). Labelling both separates
the thunk from the real body.
Value: enormous on generics-heavy games. Without this, every
instantiation is a distinct unnamed sub_xxx.
3.6 unresolvedVirtualCallPointers (v22..v29); split at v29.1
into unresolvedInstanceCallPointers +
unresolvedStaticCallPointers
Stubs the runtime patches when a virtual / interface call target becomes known. 1:1 with the metadata ranges in §2.16.
3.7 interopData (v23+)
Array of Il2CppInteropData, each containing a bundle of
P/Invoke / COM-interop function pointers for one type. See §3.6 of
doc/r2unity.md for the struct. Lights up every [DllImport]
target wrapper and every COM bridge (CCW/RCW).
3.8 codeGenModules (v24.2+)
One Il2CppCodeGenModule per image. Struct definition in §3.7 of
doc/r2unity.md.
Highlights that are particularly useful on obfuscated games:
moduleInitializer— each image's<Module>..cctor, the per- image static-init entry point.staticConstructorTypeIndices— types whose.cctorruns at load. Prime targets when keys/strings are materialised at startup.adjustorThunks+adjustorThunkCount— the generic-method struct-vs-class fixup thunks.
3.9 MetadataRegistration.types — the Il2CppType pool
Every typeIndex anywhere in metadata indexes this array. Without
it, we can't resolve any method return type, parameter type, field
type, or interface name that goes beyond a plain TypeDef.
Il2CppExecutor.GetTypeName
(third_party/Il2CppDumper/Utils/Il2CppExecutor.cs:61) walks it
recursively to produce qualified names, decoding the ECMA-335
ELEMENT_TYPE_* byte (§II.23.1.16) to branch on:
- primitive (
I1,U4,R8, …) VALUETYPE,CLASS(→typeDef.name)SZARRAY,ARRAY(→ rank + element recursion)PTR,BYREF(→ pointee recursion)VAR,MVAR(→ generic-parameter name)GENERICINST(→ open type + type-argument tuple fromgenericInsts)FNPTR,MODOPT,MODREQ
3.10 MetadataRegistration.genericInsts + genericClasses
Pool of concrete type-argument tuples ((int), (string, int),
(Player, Dictionary<int, float>), …). Every generic-instantiation
reference in the binary indexes into this array.
3.11 MetadataRegistration.methodSpecs + genericMethodTable
See §3.9 of doc/r2unity.md for the structs and consumption
pattern. GetMethodSpecName reconstructs the full
Foo<string>.Bar<int> form.
3.12 MetadataRegistration.fieldOffsets
- Pre-v22: flat
int32_t[]indexed by global field index. - v22+: pointer-of-pointers — one
int32_t *per type definition, pointing at that type's field-offset array.
GetFieldOffsetFromIndex
(third_party/Il2CppDumper/Il2Cpp/Il2Cpp.cs:274) has the full
logic, including the -8 / -16 adjustment for non-static
value-type fields (to account for the Il2CppObject header:
MonitorData * + Il2CppClass *; see ECMA-335 §II.25.4 and Unity's
Il2CppClass.cs).
Value: the byte-level layout of every class / struct. Once read, we
can label every ldr/str [this + N] with a field name and type.
Second only to method pointers in RE value.
3.13 MetadataRegistration.typeDefinitionsSizes
typedef struct {
uint32_t instance_size; /* sizeof(object) w/ Il2CppObject header */
uint32_t native_size; /* P/Invoke marshal size, 0 if N/A */
uint32_t static_fields_size;
uint32_t thread_static_fields_size;
} Il2CppTypeDefinitionSizes; /* 16 B */
Total size of every class / struct. Distinguishes 16-byte vs 24-byte
Vector types. Gives us the element stride for array indexing
(ldr X, [Xn + Xindex * instance_size]). static_fields_size
lets us label every static-field access once we find the per-class
statics pointer.
3.14 MetadataRegistration.metadataUsages (v19..v26)
The native side of §2.10. Huge void *[]. Each slot is an
Il2CppClass *, Il2CppType *, MethodInfo *, FieldInfo *,
managed string *, or generic MethodInfo * — decoded by
combining with metadataUsageLists + metadataUsagePairs.
Value: on pre-v27 binaries, labelling this array turns every
indirect metadata load in the disassembly into a readable reference.
From v27+ the mechanism was replaced by per-site inline globals and
this array is gone; labels must instead be derived per-site via
codeGenModule.staticConstructorTypeIndices + moduleInitializer
and code scanning.
4. Implications for r2unity
4.1 Portability fixes (correctness)
-
Version-gated entry sizes. Replace the literal 88 / 32 / 40 in
r2unity_get_type_definitions/r2unity_get_method_definitions/r2unity_get_imageswith a lookup keyed on wire version. Critical values:Il2CppMethodDefinition:- v≤24.0: 52 B (includes
customAttributeIndex,methodIndex,invokerIndex,delegateWrapperIndex,rgctxStartIndex,rgctxCount). - v24.1 – v30: 32 B (current assumption — correct only here).
- v31+: 36 B (
returnParameterTokenadded).
- v≤24.0: 52 B (includes
Il2CppImageDefinition:- v≤18: 24 B.
- v19 – v23: 28 B (adds
token). - v24 – v24.0: 36 B (adds exported-type range).
- v24.1+: 40 B (adds
customAttributerange) — current assumption.
Il2CppTypeDefinition:- pre-v24: extra legacy fields
(
delegateWrapperFromManagedToNativeIndex,marshalingFunctionsIndex,ccwFunctionIndex,guidIndex). - v24 – v24.0:
customAttributeIndex+byrefTypeIndex+rgctxStartIndex+rgctxCount. - v24.1+: 88 B — current assumption.
- pre-v24: extra legacy fields
(
Always source the size from
third_party/Il2CppDumper/Il2Cpp/MetadataClass.csper wire version. -
Per-version header struct. Distinguish v24.0 from v24.1+ headers so assembly / image entries are decoded correctly on unresolved-virtual-call pointer arrays.
distinct wire version we claim to support (21, 22, 23, 24, 27, 29, 31). Pull candidates from the Il2CppDumper issue-tracker corpus if new ones are needed.
-
Fail loudly on truly unknown versions. Current code treats v30/v31 as v29-shaped; that's correct for the header but should be an informative warning (not silence) when row decoders haven't been audited for a new sub-version.
4.2 Feature additions (coverage)
Ordered by RE value vs. implementation effort:
parameterstable — small, unlocks real method signatures.fieldstable — small, prerequisite for field offsets.CodeRegistration/MetadataRegistrationstructural anchor detection — unlocks everything in §3.MetadataRegistration.types+fieldOffsets+typeDefinitionsSizes— class layouts + field labels.codeGenModuleswalk for v24.2+ — currently dropping most methods on modern Unity.- Attributes bundle:
- pre-v29:
attributesInfo+attributeTypes+customAttributeGeneratorsthunk interpreter - v29+:
attributeDataRange+attributeDataBLOB decoder
- pre-v29:
- Generics bundle:
genericContainers+genericParameters+methodSpecs+genericInsts+genericMethodPointers+genericMethodTable. RecoversList<int>::Add-style native functions. properties+events— recomposeget_X/set_X/add_X/remove_Xinto clean declarations.metadataUsageLists/Pairs+MetadataRegistration.metadataUsages— pre-v27 only, labels every indirect metadata load.interfaces+interfaceOffsets+vtableMethods— full resolved virtual dispatch tables.nestedTypes— correctOuter.Innernaming.- Richer SBOM: native dependencies (
DT_NEEDED,LC_LOAD_DYLIB, PE import table), file hashes, ELF build IDs, Mach-O UUIDs, PE debug GUIDs, SPDX output. - Manual
-a/-cpointer read with pointer-width detection.
4.3 Architecture-neutral vs architecture-branching code
.datparser stays architecture-neutral. No branching needed.CodeRegistration/MetadataRegistrationwalkers must be written withptrsz = is64bit ? 8 : 4and{u32|u64}readers accordingly. This generalises cleanly from the existing Mach-O / ELF scanner scaffolding.
4.4 Format-specific scanner gaps
- ELF: packed Android relocations (
DT_ANDROID_RELA = 0x60000010,DT_ANDROID_RELR = 0x6fffe000) not handled — Play-Store builds compressed with packed relocs read as "noDT_VERSYM/DT_VERNEEDnot consulted (would help identify Unity version from the.sodirectly).ET_EXECvsET_DYNbranching (we assume PIC shared objects). - Mach-O: 32-bit, FAT multi-slice selection,
LC_DYLD_CHAINED_FIXUPS(0x34) interpretation,LC_ENCRYPTION_INFO_64detection (FairPlay-encrypted App Store IPAs silently return garbage), ARM64e pointer-authentication awareness.IMAGE_DIRECTORY_ENTRY_BASERELOC(not needed today, link-timeImageBaseis concrete).
4.5 Sources and caveats
- The Unity 6 (
6000.x) row in §1.3 is provisional. The mapping is inferred from[Version(Min = 31)] returnParameterTokenbeing the current high-water mark inIl2CppDumperplus release-date alignment. No Unity 6 metadata binary was directly inspected. - Il2CppDumper's bundled snapshot is what drives these layout
tables; newer Unity releases may introduce a
v33/v31.xsub-version that won't be caught until the vendored copy is refreshed. A quarterly bump ofthird_party/Il2CppDumper/is the lightest way to track new Unity releases. - Il2CppInspector's
MetadataClasses.cs/MetadataVersions.csis an independent second opinion; pulling it as a cross-check would let us confirm a few uncertain sub-version boundaries (notably the 2020.x → v24.4 vs v27 transition month).
5. Further reading
- ECMA-335, "Common Language Infrastructure" (6th edition) — the
primary source for token encoding (§II.22), metadata BLOB
signatures (§II.23.2, §II.23.3), element-type codes
(§II.23.1.16), attribute enums (
TypeAttributes,MethodAttributes,MethodSemanticsAttributes,GenericParameterAttributes,AssemblyFlags), instance layout andIl2CppObjectheader (§II.25.4), strong-name public-key derivation (§II.6.3). https://ecma-international.org/publications-and-standards/standards/ecma-335/ - ECMA-334, "C# Language Specification" — attribute semantics, property/event syntactic sugar. https://ecma-international.org/publications-and-standards/standards/ecma-334/
- Il2CppDumper — vendored reference implementation.
Il2Cpp/MetadataClass.csis the authoritative per-version layout source;Utils/Il2CppExecutor.csis the authoritative BLOB decoder. https://github.com/Perfare/Il2CppDumper - Il2CppInspector — per-version deltas and Unity-version matrix. https://github.com/djkaty/Il2CppInspector
- REAndroid/lib-global-metadata — independent Java port. https://github.com/REAndroid/lib-global-metadata
- Unity blog series on IL2CPP internals (method calls, generics, strings, P/Invoke): https://blog.unity.com/technology/il2cpp-internals-method-calls
- Android linker packed-relocation format: https://android.googlesource.com/platform/bionic/+/master/linker/linker_relocate.cpp
- Mach-O chained fixups (dyld source): https://github.com/apple-oss-distributions/dyld
- PE/COFF specification (Microsoft): https://learn.microsoft.com/windows/win32/debug/pe-format