Skip to content

Mono IL2CPP Dissector

brkzlr edited this page Jun 19, 2026 · 7 revisions

The Mono/IL2CPP Dissector is PINCE's built-in tool for inspecting and interacting with games built using the Mono or IL2CPP runtimes. Use it to inspect the managed code layer of Unity games and other .NET/Mono applications running on Linux (native or via WINE/Proton).

Unlike native code where you're working with raw memory and assembly, managed runtimes maintain rich metadata about every class, field and method in the program. The Dissector taps into this metadata to browse the game's type hierarchy, inspect live object instances, call managed methods directly and export class layouts as structures, all without needing to reverse engineer the IL2CPP binary or Mono AOT output yourself.

Please note that this feature currently supports native Linux Mono and IL2CPP games, as well as WINE/Proton targets, both x64 and x86. If no managed runtime is detected in the attached process, you'll see the error "No supported managed runtime found in this process".

How can I access it?

  • You need to be attached to a process first.
  • After that, open Memory View.
  • Go to Tools -> Dissect Mono/IL2CPP.

If the target process doesn't have a supported managed runtime loaded, you'll see the error message mentioned above. There's no keyboard shortcut for this menu item.

Understanding the Tree View

The Dissector presents the game's managed types in a hierarchical tree with two columns: Name and Value. The Name column shows the hierarchy (assemblies, classes, fields, methods) and the Value column shows type and offset information for fields.

The tree uses lazy loading, meaning children are fetched from the collector only when you expand a node. This keeps the UI responsive even with games that have thousands of classes.

Assembly-CSharp.dll
└── Game.Player
    ├── Fields
    │   ├── health                 Int32 @ 0x18
    │   ├── position               Vector3 @ 0x20
    │   └── inventory              Inventory @ 0x30
    └── Methods
        ├── TakeDamage(System.Int32)
        └── GetHealth() : System.Int32

The text after each field name (like Int32 @ 0x18) is the Value column content. The Value column is empty for assemblies, classes and methods.

Assemblies

The top level shows all loaded assemblies (DLLs). Assembly-CSharp.dll is typically where Unity game code lives, while other assemblies might contain engine code or third-party libraries.

Classes

Expanding an assembly reveals all classes defined within it. Classes are shown with their full namespace (e.g., Game.Player), so it's easy to find what you're looking for using the search box at the top.

Fields and Methods

Each class has two children: Fields and Methods. Expanding them loads the field or method list from the collector.

Fields show their type and offset in the Value column:

  • Instance fields: {type} @ {offset} (e.g., Int32 @ 0x18)
  • Static fields: static {type} @ {offset} (e.g., static String @ 0x18). The offset here is within the static field data area, not a runtime address. The actual runtime address is only resolved on demand when you use the context menu.
  • Const fields (compile-time constants): const {type} (e.g., const Int32). These have no runtime address at all.

Methods show their full signature in the Name column, including parameter types and return types (e.g., TakeDamage(System.Int32) or GetHealth() : System.Int32). The Value column is empty for methods.

Searching and Filtering

The search box at the top filters the tree as you type. It matches against the Name column only, so it will find assembly names, class names, field names and method names but not types or offsets in the Value column. Parent nodes of matching items stay visible so you can see the full path.

The Show inherited members checkbox controls whether fields and methods from parent classes are displayed. When enabled, inherited members appear greyed out with their declaring class name in parentheses in the Value column, like Int32 @ 0x18 (PlayerBase). Toggling this checkbox rebuilds the whole tree and collapses everything, so you'll need to re-expand any nodes you were looking at.

Inherited members are walked up to 32 levels deep in the parent chain.

Context Menus

Right-clicking on different items in the tree reveals context-specific actions:

Methods

  • Disassemble: Opens the method's compiled native code in the disassembly view. Useful for understanding what a method actually does at the CPU level.
  • Set Breakpoint: Sets a breakpoint at the method's entry point. The debugger will pause execution when the method is called.
  • Copy Address: Copies the method's native address to the clipboard.
  • Invoke...: Opens the method invocation dialog (see Invoking Methods below).

Fields

Field context menus vary depending on the field type:

Static fields (non-const):

  • Add this address to address list: Adds the field to the address table with its resolved runtime address. Note that only the field name and address are passed, the value type defaults to empty so you'll need to set it manually after adding.
  • Copy Address: Copies the field's runtime address to the clipboard.

Instance fields (in the main tree):

  • Add this address to address list: Only appears if a singleton root is found for the class (see Instance Fields via Singleton below). Creates a pointer chain rooted at the singleton.
  • Copy Offset: Copies the field's offset to the clipboard.

Instance fields (in the Find Instances tree):

  • Add this address to address list: Always available (no singleton needed). Adds the field with its absolute address or pointer chain from the concrete instance.
  • Copy Offset: Copies the field's offset to the clipboard.

Const static fields have no context menu at all.

Classes

  • Find Instances: Scans memory for all live instances of this class (see Finding Instances below).
  • Export as structure: Creates a PINCE structure definition from the class layout (see Exporting as Structure below). Opens the Structure Editor for confirmation, if you cancel it deletes the structure.
  • Dissect as structure: Only appears when the class has a singleton pattern (a static field of its own type, like Player.Instance). Opens the Structure View directly on the live singleton object. If a structure with the same name already exists, it reuses it instead of creating a duplicate.

Reference Field Drilling

When a field's type is another managed class (like inventory of type Inventory) and the field is non-static, you can expand it to see the referenced object's fields. Static object-typed fields cannot be drilled into.

You can drill down through object hierarchies this way:

inventory              Inventory @ 0x30
    ├── items          Item[] @ 0x10  [Inventory]
    └── gold           Int32 @ 0x18  [Inventory]

The class name in brackets shows the referenced class that the drilled fields belong to. Each level accumulates the offset path, so when you add a deeply nested field to the address table, PINCE creates the correct pointer chain automatically.

Drilling is limited to 16 levels deep to prevent infinite loops in circular references. When you hit the limit, you'll see (maximum drill depth reached) instead of further fields.

If a reference field's type cannot be resolved (for example, if it's a generic type parameter or an interface), you'll see (type could not be resolved) instead of the nested fields.

Finding Instances

The Find Instances feature scans the game's memory to locate all live objects of a particular class. This is very useful when you need to find "which of these 500 enemies is the one I'm looking at" or "where are all the Player objects in memory".

How it works

Every managed object starts with a header word that identifies its type (the vtable pointer in Mono or the class pointer in IL2CPP). The Dissector uses this marker to scan memory and find all objects of a given class.

The scan uses a private scanner that doesn't interfere with your main scan tab. It searches all readable and writable memory regions with alignment matching the pointer size (4 bytes on x86, 8 bytes on x64).

Using Find Instances

  1. Right-click on a class in the tree and select Find Instances.
  2. The scanner runs and opens a new window showing all found instances.
  3. The window title shows the class name (e.g., Find Instances - Game.Player).
  4. The header shows how many instances were found, like 5 instance(s) found. If there are more than 2000, it shows 5 instance(s) found (showing first 2000).
  5. Each instance is labeled #0, #1, etc. with its hex address in the Value column.
  6. Each instance can be expanded to show its fields at their actual memory addresses.
  7. You can right-click on any field to add it to the address table or drill into nested objects.

If no instances are found, you'll get a message saying No live instances found.

If the runtime doesn't expose the type information needed to identify instances, you'll see Could not resolve an instance marker for this class. This can happen with certain IL2CPP builds or stripped assemblies.

Instance Context Menu

Right-clicking on an instance in the Find Instances window gives you these options:

  • Add this address to address list: Adds the object's base address to the address table.
  • Dissect as structure: Opens the Structure view on this specific instance. Reuses an existing structure if one with the same name exists.
  • Copy Address: Copies the object's address to the clipboard.
  • Invoke...: A submenu listing all the class's methods. Selecting one opens the Invoke dialog with the instance pointer already filled in, so you can call methods directly on that specific object. The submenu is disabled if the class has no methods.

The instance scan is capped at 2000 results to keep the UI responsive. If you have more objects, consider narrowing your search or using other filtering techniques.

Invoking Methods

The Dissector can call managed methods directly, which means you can trigger game logic, read computed values or test your understanding of how the code works.

Opening the Invoke Dialog

Right-click on a method and select Invoke.... The dialog (titled Invoke) shows:

  • The method's full signature at the top
  • An Instance (this) field for instance methods: you can type an address or click Find instance... to pick from live objects. For static methods, this field is disabled and set to 0.
  • Input fields for each parameter, with appropriate editors based on the parameter type
  • A Call button at the bottom to execute the method

Parameter Types

The invoke dialog handles different parameter types:

  • Primitives (int, float, bool, etc.): Simple text input with appropriate parsing.
  • Strings: Text input for managed string arguments.
  • Objects: Hex address input for object references.
  • Structs (value types): If the struct contains only primitive fields, you get individual inputs for each field. Otherwise, you enter the raw bytes in hex, with a hint showing the expected byte count (like 24 raw bytes (hex)).

If a parameter type can't be marshalled at all, the input field is disabled and the Call button will show This method has a parameter type that can't be marshalled yet when you try to call the method.

When parameter names aren't exposed by the runtime, the collector synthesizes names like arg0, arg1, etc.

Return Values

After calling a method, the result is displayed at the bottom of the dialog:

  • Primitive return values are shown directly as Result: {value}.
  • Struct returns are displayed as Result: field=value, field=value, ... if all fields are primitive, or as hex bytes otherwise.
  • String returns show the managed string content.
  • Void returns show Returned (void).
  • If the method throws an exception, you'll see Exception thrown (object @ {hex}) showing the exception object's address.

If the instance pointer is invalid, you'll get Invalid instance pointer. If the argument values can't be parsed, you'll get Could not parse the argument values.

Instance Methods

For instance methods, you need to provide a valid object pointer. The Find instance... button scans for live objects of the class so you can pick one. The picker shows a dropdown with up to 2000 live instance addresses, labeled Select a live instance: (or Select a live instance: (showing first 2000) if truncated). The button is disabled when the declaring class can't be resolved.

If the class has a singleton pattern (like Player.Instance), the dialog tries to use that automatically when you open it from the main tree.

Exporting as Structure

Once you understand a class's layout, you can export it as a PINCE structure definition. This integrates with PINCE's Structure system so you can view and edit object fields with proper type information.

How to Export

  1. Right-click on a class and select Export as structure.
  2. PINCE creates a structure with all instance fields, including inherited ones (walked up to 32 levels in the parent chain).
  3. Reference type fields become pointers to nested structures.
  4. Value type fields (structs like Vector3) are inlined if their fields are all primitives, otherwise they become byte arrays.
  5. The Structure Editor opens for you to review and confirm the definition. If you cancel, the structure is deleted.
  6. The structure appears in your Structure list and can be applied to any address.

If a structure with the same name already exists, PINCE appends a counter (like Player_1, Player_2) to avoid conflicts. Self-referential types are handled with cycle detection so they don't create infinite nested structures.

Managed Strings

When a class has System.String fields, PINCE automatically creates a System.String structure that handles the managed string layout. The offsets differ between 32-bit and 64-bit:

64-bit:

  • vtable_ptr (0x00): The object's type identifier
  • sync (0x08): Synchronization block
  • length (0x10): The string's character count
  • chars (0x14): The UTF-16 character data

32-bit:

  • vtable_ptr (0x00): The object's type identifier
  • sync (0x04): Synchronization block
  • length (0x08): The string's character count
  • chars (0x0C): The UTF-16 character data

When you add a System.String structure to the address table, PINCE dynamically reads the length field at runtime and adjusts the chars member's length accordingly (capped at 4096 characters). The default preview length is 32 bytes before this dynamic adjustment kicks in.

Adding Fields to the Address Table

The quickest way to monitor or modify a managed field is to add it directly to the address table:

Static Fields

Static fields have a fixed runtime address. Right-click and select Add this address to address list and the field appears in the address table with its absolute address. Note that the value type isn't automatically set, so you'll need to pick the right type manually after adding it.

If the runtime can't resolve the static field's address (more common with IL2CPP where static field offsets may not be calibrated), you'll see Static field address is unavailable for this runtime.

Instance Fields via Singleton

Many game classes follow the singleton pattern with a static field like Player.Instance or GameManager.Current. When you add an instance field from such a class, PINCE automatically creates a pointer chain:

Player.Instance -> +0x18 (health)

This pointer chain resolves correctly every time you restart the game, as long as the singleton pattern holds. The singleton detection looks for a static field whose type matches the class itself.

Instance Fields via Find Instances

If there's no singleton, use Find Instances first, then expand a specific instance and add its fields. The resulting address table entry points to that specific object:

0x7f8a12340000 -> +0x18 (health)

In the Find Instances tree, Add this address to address list is always available for instance fields (no singleton needed). For one-hop fields it uses the absolute address, for deeper fields it creates a pointer chain from the instance base.

Tips and Tricks

Finding the Right Class

Unity games often have hundreds of classes. Use the search box to filter by name. Common patterns:

  • Player, Character, Actor: player-related classes
  • Enemy, NPC, AI: enemy behavior
  • Inventory, Item, Equipment: item systems
  • Manager, Controller, System: game state managers

Remember that the search only matches the Name column, not the Value column. So you can search for class or field names but not type names or offsets.

Understanding Field Types

  • Primitive types (System.Int32, System.Single, System.Boolean): Direct values you can read/write.
  • System.String: Managed strings with dynamic length.
  • Object references (any class type): Pointers to other managed objects. Drill into them or export as structures.
  • Arrays (T[]): Managed arrays with a length prefix. The actual elements start after the array header.
  • Value types (Vector3, Quaternion): Inline structs, not pointers.

When Find Instances Returns Nothing

If Find Instances returns no results:

  1. Make sure the game is actually running and has created instances of the class.
  2. Some classes might be abstract or only used as base classes.
  3. The class might use a custom allocator that doesn't follow the standard object layout.

If you see the error Could not resolve an instance marker for this class, it means the runtime doesn't expose the type information needed to identify instances.

Static Field Errors

If you try to add a static field to the address table and see Static field address is unavailable for this runtime, it means the collector couldn't resolve the field's address. This is more common with IL2CPP games where static fields are stored differently than in Mono and the static field offset may not be calibrated.

Combining with Native Code Analysis

The Dissector shows you the managed layer, but the actual game logic often lives in native code (especially in IL2CPP games where managed methods are compiled to native). Use Disassemble on methods to see the native implementation, then apply your normal reverse engineering techniques.

Clone this wiki locally