A good reference

We can find all of the functions that use the metadata header by simply navigating to the static variables:

.data:00007FFF43D74AD0 ; Il2CppGlobalMetadataHeader *s_GlobalMetadata
.data:00007FFF43D74AD0 s_GlobalMetadata dq ?
.data:00007FFF43D74AD8 ; Il2CppGlobalMetadataHeader *s_GlobalMetadataHeader
.data:00007FFF43D74AD8 s_GlobalMetadataHeader Il2CppGlobalMetadataHeader <?>

and using List cross references to to produce a list of every function that references these addresses. There will be a lot of results since the metadata is used by many functions in the IL2CPP source code:

alt

Some of these functions will be trivial to reverse engineer. Others will be horrifying. Start with the simple ones. Here is a nice example:

char *__fastcall sub_7FFF41E80080(int a1)
{
  return s_GlobalMetadata + 28 * a1 + s_GlobalMetadataHeader->unknown118;
}

This tells us that unknown118 is an offset into an array, of which each entry uses 28 (0x1C) bytes – or a size of 7 32-bit integers. Referring to il2cpp-metadata.h again, there is only one struct that fits the bill: Il2CppEventDefinition.

We can improve the decompilation further by importing all of the types from il2cpp-metadata.h so that we can assign them to the header fields. Use File -> Load file -> Parse C header file to do this. You will likely get errors, which you can resolve by temporarily removing the #includes from the top of the file so that only the struct definitions are present. Once you’ve done this, the Local Types window will fill with these structs at the bottom:

alt

Be careful not to import Il2CppGlobalMetadataHeader as it will overwrite your work! (remove it from the file before importing, remembering to keep a backup)

To apply this to the function we just found, you are now able to redefine the function signature as:

Il2CppEventDefinition* GetEvent(int eventIndex)

which will produce the following updated decompilation:

Il2CppEventDefinition *__stdcall GetEvent(int eventIndex)
{
  return (s_GlobalMetadata + 28 * eventIndex + s_GlobalMetadataHeader->eventsOffset);
}

In addition – and this is crucial – any function you decompile which calls this one will now treat the return type as a pointer to an Il2CppEventDefinition. As it happens, this particular function is only called once. Let’s take a look at a snippet of part of it:

v10 = GetEvent(v7);
*(v9 - 8) = sub_7FFF41E806C0(v10->typeIndex);
*(v9 - 16) = sub_7FFF41E81110(v10->nameIndex);
*v9 = v2;
if ( v10->add != -1 )
  *(v9 + 8) = *(*(v2 + 128) + 8i64 * v10->add);
if ( v10->remove != -1 )
  *(v9 + 16) = *(*(v2 + 128) + 8i64 * v10->remove);
if ( v10->raise != -1 )
  *(v9 + 24) = *(*(v2 + 128) + 8i64 * v10->raise);
++v7;
*(v9 + 32) = v10->customAttributeIndex;
*(v9 + 36) = v10->token;

As you can see, all of the fields of the event definition returned from GetEvent are used in the caller’s decompilation.