Files
2026-06-08 12:32:03 +02:00

32 KiB
Raw Permalink Blame History

r2unity - IL2CPP Wire Versions, Layout Deltas, And Unused Metadata

This document is a technical reference for:

  1. Which global-metadata.dat wire versions exist, what changed in each, which fractional sub-versions affect the on-disk layout, and which Unity release each one maps to.
  2. Which logical records inside global-metadata.dat and 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:

1. IL2CPP metadata wire versions

1.1 Wire versions known on disk

The int32_t at file offset 4 of global-metadata.dat is known

16, 19, 20, 21, 22, 23, 24, 27, 29, 31, 38, 39

Earlier values between those releases were reserved or skipped. Current Il2CppDumper v39 sources define the new section-based header from v38 onward and have a v39-specific ParameterIndex encoding, so the old [16,31] high-water mark is no longer current.

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; newer Unity 6 metadata uses whole wire versions v38/v39 for the observed on-disk break). 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 / MetadataRegistration struct size probes)
  • the presence of specific arrays in the binary (genericAdjustorThunks exists 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._data widens from int32 → uint64 at v27.2 (reference: Il2CppDumper/Il2Cpp/MetadataClass.cs, type_post29 + _data fields). Row grows from 8 B to 12 B/16 B.
  • Il2CppMethodDefinition gains returnParameterToken at v31. Row grows from 32 B to 36 B.
  • All customAttributeIndex fields (on TypeDef, 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.hashValueIndex is 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.
  • Il2CppStringLiteral drops its length field at v32. Rows are dataIndex only; length is inferred from the next row or the end of the string-literal data section.
  • Il2CppTypeDefinition.elementTypeIndex is no longer serialized at v32.
  • TypeIndex, TypeDefinitionIndex, and GenericContainerIndex use compact reader hooks from v35, but the current v39 Il2CppDumper source initializes their widths to 4 bytes before v38 because legacy headers have no section counts.
  • The header switches from {offset,size} pairs to 31 {offset,size,count} section records at v38.
  • Starting at v38, the compact index widths are inferred from section counts and may be 1, 2, or 4 bytes.
  • ParameterIndex becomes compact at v39.
  • Il2CppAssemblyDefinition gains moduleToken at v38, moving the embedded Il2CppAssemblyNameDefinition tail from offset 16 to offset 20.

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.x 31 31 Il2CppMethodDefinition adds returnParameterToken.
Unsupported gap 36 – 37 36 – 37 No header layout declared by the v39 reference source.
6000.x (Unity 6 era) 38 38 Section header triples; compact index widths inferred from counts.

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; v39 sources add the section-triple header and compact-index annotations used above.

1.4 r2unity's current acceptance band

From r2unity_parse_metadata() in src/lib/lib.c:

if (version < 24 || version > 39 || (version > 35 && version < 38))  return NULL;
if (version >= 38)      header_size = sizeof (v38);
else if (version < 27)  header_size = sizeof (v24);
else if (version < 29)  header_size = sizeof (v27);
else                    header_size = sizeof (v29);   /* 29..35 */
/* 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.
36 – 37 Rejected Header shape is unknown; v38 is the first confirmed section-triple layout.
39 Accepted via _v38 header struct Confirmed against the latest local reproducer. Adds compact ParameterIndex.

Bottom line: the acceptance band covers every commercially relevant Unity build from 2017 onward, but within the band the old fixed 88 / 32 / 40 / 48–52 strides are correct only for a subset. The current parser now computes the decoded v38/v39 row sizes from section counts and compact-index widths; pre-v24 and native-side version gaps remain future work.

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 (doc/ptrtables.md), 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 (doc/ptrtables.md) to emit real instantiation names like List<int>::Add.

2.7 interfaces + interfaceOffsets + vtableMethods

  • interfaces — raw int32_t[]; per-type via typeDef.interfacesStart / typeDef.interfaces_count. Each entry is a type index (into MetadataRegistration.types).

  • interfaceOffsets — Il2CppInterfaceOffsetPair[] = { int interfaceTypeIndex; int offset; } (8 B). Per-type via typeDef.interfaceOffsetsStart / interface_offsets_count. Maps each implemented interface to the vtable slot where its methods begin.

  • vtableMethods — raw uint32_t[]; each slot is an EncodedMethodIndex:

    usageType = (idx & 0xE0000000) >> 29;   /* Il2CppMetadataUsage */
    decoded   = (version >= 27)
                  ? ((idx & 0x1FFFFFFE) >> 1)
                  : (idx & 0x1FFFFFFF);
    

    usageType == 3 → concrete method definition, usageType == 6 → methodSpecs (doc/ptrtables.md) 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 — flat int32_t[] of attribute type indices (into MetadataRegistration.types).
  • CodeRegistration.customAttributeGenerators — one void(void*) function pointer per range. Its body news up the attribute instance(s), baking constructor arguments as immediate operands. Reading the thunk recovers the arguments; this is what Il2CppDumper's GetCustomAttributeValuesFromGenerator does.

v29+ — inline BLOB:

  • attributesInfo* header slots repurposed as attributeDataRange: { uint token; uint startOffset; } array.
  • attributeTypes* header slots repurposed as attributeData: raw BLOB heap.
  • BLOB encoding matches ECMA-335 §II.23.3 CustomAttrib extended 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.
  • customAttributeGenerators no longer exists in CodeRegistration.

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 (doc/ptrtables.md).

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 native unresolved-call pointer tables documented in doc/ptrtables.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-table reference

Native registration structures and pointer-table shapes are documented in doc/ptrtables.md. This file keeps only version and row-layout details; put Il2CppCodeRegistration, Il2CppMetadataRegistration, Il2CppCodeGenModule, field-offset, generic-method, interop, invoker, and method-address recovery details in doc/ptrtables.md.

4. Implications for r2unity

4.1 Portability fixes (correctness)

  1. Version-gated entry sizes. The decoded tables now compute the v38/v39 row sizes from section counts and compact-index widths. Keep extending this approach instead of reintroducing literal strides. Critical historical values:

    • Il2CppMethodDefinition:
      • v≤24.0: 52 B (includes customAttributeIndex, methodIndex, invokerIndex, delegateWrapperIndex, rgctxStartIndex, rgctxCount).
      • v24.1 – v30: 32 B.
      • v31+: 36 B (returnParameterToken added).
      • v35..v37: compact reader hooks exist, but current reference initializes the widths to 4 bytes before v38.
      • v38+: declaringType, returnType, and genericContainerIndex may shrink to 1/2/4-byte compact indices.
      • v39+: parameterStart may also shrink to a compact ParameterIndex.
    • Il2CppImageDefinition:
      • v≤18: 24 B.
      • v19 – v23: 28 B (adds token).
      • v24 – v24.0: 36 B (adds exported-type range).
      • v24.1+: 40 B before compact-index shrinkage (adds customAttribute range).
      • v38+: typeStart and exportedTypeStart may shrink to compact TypeDefinitionIndex fields.
    • Il2CppTypeDefinition:
      • pre-v24: extra legacy fields (delegateWrapperFromManagedToNativeIndex, marshalingFunctionsIndex, ccwFunctionIndex, guidIndex).
      • v24 – v24.0: customAttributeIndex + byrefTypeIndex + rgctxStartIndex + rgctxCount.
      • v24.1+: 88 B before compact-index shrinkage.
      • v32+: elementTypeIndex is no longer serialized.
      • v38+: type-like indices may shrink to compact serialized widths.

    Always source the size from third_party/Il2CppDumper/Il2Cpp/MetadataClass.cs per wire version.

  2. Per-version header structs. v38+ is represented as the 31-section header. Keep v24.0/v24.1+ discrimination explicit so assembly, image, type, and method rows do not silently decode at the wrong stride.

  3. Version fixtures. Keep at least one fixture per distinct wire version we claim to support (24.1, 27, 29, 31, 38, 39), plus edge cases for rejected v24.0 and v36/v37.

  4. Fail loudly on truly unknown versions. v36/v37 are currently rejected because their header shape is not proven. Keep that behavior until a reliable reference or fixture confirms the layout.

4.2 Feature additions

Implementation tasks live in doc/todo.md. Keep this document focused on version/layout facts and portability constraints.

4.3 Architecture-neutral vs architecture-branching code

  • .dat parser stays architecture-neutral. No branching needed.
  • CodeRegistration / MetadataRegistration walkers must be written with ptrsz = is64bit ? 8 : 4 and {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 "no relocations". DT_VERSYM/DT_VERNEED not consulted (would help identify Unity version from the .so directly). ET_EXEC vs ET_DYN branching (we assume PIC shared objects).
  • Mach-O: 32-bit, FAT multi-slice selection, LC_DYLD_CHAINED_FIXUPS (0x34) interpretation, LC_ENCRYPTION_INFO_64 detection (FairPlay-encrypted App Store IPAs silently return garbage), ARM64e pointer-authentication awareness. IMAGE_DIRECTORY_ENTRY_BASERELOC (not needed today, link-time ImageBase is concrete).

4.5 Sources and caveats

  • v39 is confirmed by the local Unity 6 reproducer. v38 support is based on the current Il2CppDumper v39 source layout; keep it guarded by fixtures as samples become available.
  • The local Unity install at /Applications/Unity/Hub/Editor/6000.4.5f1 ships IL2CPP sources that assert s_GlobalMetadataHeader->version == 39 in libil2cpp/vm/GlobalMetadata.cpp. Its GlobalMetadataFileInternals.h and MetadataDeserialization.* files confirm the v39 runtime shape: no elementTypeIndex, Il2CppStringLiteral is dataIndex only, moduleToken is present on assemblies, and compact index widths are read from metadata counts.
  • Public Unity release notes and documentation searched for 2023.3/6000.x do not document the private global-metadata.dat wire format. The useful public source for the v32..v39 gap is still Il2CppDumper v39's annotated MetadataClass.cs, plus the local Unity runtime sources above.
  • Il2CppDumper's bundled snapshot is what drives these layout tables; newer Unity releases may introduce another whole wire version or sub-version that won't be caught until the vendored copy is refreshed. A quarterly bump of third_party/Il2CppDumper/ is the lightest way to track new Unity releases.
  • Il2CppInspector's MetadataClasses.cs / MetadataVersions.cs is 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