-
Notifications
You must be signed in to change notification settings - Fork 185
Mono IL2CPP Dissector
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, the Dissector will let you know.
- 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 an error message.
The Dissector presents the game's managed types in a hierarchical tree:
Assembly-CSharp.dll
├── Game.Player
│ ├── Fields
│ │ ├── health: System.Int32 @ 0x18
│ │ ├── position: Vector3 @ 0x20
│ │ └── inventory: Inventory @ 0x30
│ └── Methods
│ ├── TakeDamage(System.Int32)
│ └── GetHealth() : System.Int32
└── Game.Inventory
├── Fields
└── Methods
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.
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.
Each class has two children: Fields and Methods.
-
Fields: shows all instance and static fields with their types and offsets. Static fields show their runtime address, while instance fields show the offset from the object's base address. Const fields (compile-time constants) are shown as
const {type}and have no runtime address. - Methods: lists all methods with their full signatures, including parameter types and return types.
The search box at the top filters the tree as you type. It matches against assembly names, class names, field names and method names. 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, like health (PlayerBase).
Right-clicking on different items in the tree reveals context-specific actions:
-
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).
For static fields:
-
Add to Address List: Adds the field to the address table with its resolved runtime address. -
Copy Address: Copies the field's address to the clipboard.
For instance fields:
-
Add to Address List: If the class has a singleton pattern (a static field of its own type, likePlayer.Instance), this adds the field as a pointer chain rooted at that singleton. -
Copy Offset: Copies the field's offset to the clipboard.
-
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). -
Dissect as Structure: If the class has a singleton instance, opens the Structure view directly on that live object.
When a field's type is another managed class (like inventory: Inventory), you can expand it to see the referenced object's fields. You can drill down through object hierarchies this way:
player: Player @ 0x100
└── inventory: Inventory @ 0x30 [Player]
├── items: Item[] @ 0x10 [Inventory]
└── gold: System.Int32 @ 0x18 [Inventory]
The type name in brackets shows the referenced class. 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.
Note: Drilling is limited to 16 levels deep to prevent infinite loops in circular references.
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.
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".
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.
- Right-click on a class in the tree and select
Find Instances. - The scanner runs and opens a new window showing all found instances.
- Each instance can be expanded to show its fields at their actual memory addresses.
- You can right-click on any field to add it to the address table or drill into nested objects.
Right-clicking on an instance in the Find Instances window gives you additional options:
-
Add to Address List: Adds the object's base address to the address table. -
Dissect as Structure: Opens the Structure view on this specific instance. -
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.
Note: 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.
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.
Right-click on a method and select Invoke. The dialog shows:
- The method's full signature
- An instance field (for instance methods): you can type an address or click
Find Instanceto pick from live objects - Input fields for each parameter, with appropriate editors based on the parameter type
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)).
After calling a method, the result is displayed at the bottom of the dialog:
- Primitive return values are shown directly.
- Struct returns are displayed as
field=value, field=value, ...if all fields are primitive or as hex bytes otherwise. - String returns show the managed string content.
- If the method throws an exception, the exception object's address is shown.
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. If the class has a singleton pattern (like Player.Instance), the dialog tries to use that automatically.
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.
- Right-click on a class and select
Export as Structure. - PINCE creates a structure with all instance fields, including inherited ones.
- Reference type fields become pointers to nested structures.
- Value type fields (structs like Vector3) are inlined if their fields are all primitives, otherwise they become byte arrays.
- 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.
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 apply this structure to an address in the address table, PINCE dynamically calculates the correct length for the string preview.
The quickest way to monitor or modify a managed field is to add it directly to the address table:
Static fields have a fixed runtime address. Right-click and select Add to Address List -> the field appears with its absolute address and correct type.
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.
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)
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
-
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.
If Find Instances returns no results:
- Make sure the game is actually running and has created instances of the class.
- Some classes might be abstract or only used as base classes.
- 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. This can happen with certain IL2CPP builds or stripped assemblies.
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.
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.