-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial Reading and Reconstructing ISIL
When you decompile an IL2CPP Unity game, standard decompilers emit empty method bodies like throw null; or return default;. The C# code is gone because Unity compiled it down to C++ and then to native machine code.
However, Cpp2IL can lift that machine code back into an intermediate representation called ISIL (Instruction Set Intermediate Layer). ISIL sits halfway between assembly and C#: it uses assembly-style instructions (Compare, Move, Call), but all method names, type names, and string constants are resolved back to their real names.
This tutorial walks through how to take raw ISIL output, map its numeric memory offsets to C# field names, and rewrite the logic back into clean, compilable C# on both x86_64 (PC/Windows) and ARM64 (macOS Apple Silicon / Android / iOS).
- AssetRipper output: A decompiled Unity project folder containing your game's scripts and assets.
-
Cpp2IL ISIL export: Generated by running:
Cpp2IL.exe --game-path "C:\Path\To\Game" --output-as isil --out "C:\Path\To\Output"
- A text editor where you can open two files side-by-side (e.g. VS Code).
AssetRipper gives you class declarations with field names, but without offset numbers. In 64-bit IL2CPP, base object metadata takes up offsets 0x00 through 0x1F (0 to 31 bytes). Your class's first custom field starts at offset 32 (0x20).
Here is ButtonMouseClick.cs from AssetRipper with offsets mapped by field order and alignment:
public class ButtonMouseClick : MonoBehaviour
{
public bool interactable; // Offset 32 (0x20) - 1 byte
public UnityEvent eventClick; // Offset 40 (0x28) - 8 bytes pointer
public UnityEvent eventEnter; // Offset 64 (0x40) - 8 bytes pointer
public UnityEvent eventExit; // Offset 88 (0x58) - 8 bytes pointer
public bool firstStart; // Offset 153 (0x99) - 1 byte
public bool changeNow; // Offset 154 (0x9A) - 1 byte
public bool lockButton; // Offset 160 (0xA0) - 1 byte
}- Offset 32 (
0x20) ->interactable - Offset 64 (
0x40) ->eventEnter - Offset 153 (
0x99) ->firstStart - Offset 154 (
0x9A) ->changeNow - Offset 160 (
0xA0) ->lockButton
In 64-bit x86_64 builds, CPU register rcx holds this on entry and is saved into rbx. [rbx + offset] reads fields on this.
020 Compare [rbx+153], 0 ; Compare firstStart with 0 (false)
022 JumpIfNotEqual 025 ; If firstStart is true, jump past the call
024 Call ButtonMouseClick.Start
025 Compare [rbx+32], 0 ; Compare interactable with 0 (false)
027 JumpIfEqual 037 ; If interactable is false, jump to return
029 Compare [rbx+160], 0 ; Compare lockButton with 0 (false)
031 JumpIfNotEqual 037 ; If lockButton is true, jump to return
031 Move rcx, [rbx+64] ; Load eventEnter (UnityEvent pointer) into rcx
035 Call UnityEvent.Invoke ; Call eventEnter.Invoke()
036 Move [rbx+154], 1 ; Set changeNow = true
037 Return
If your binary comes from macOS (Apple Silicon M1/M2/M3), Android, or iOS, Cpp2IL lifts ARM64 machine code into ISIL.
In ARM64 calling conventions:
-
x0holdsthison entry and is saved into callee-saved registerx19. -
w8/w9are 32-bit register views used for booleans (1byte) and integers (4bytes). -
[x19, #0x99]accesses memory offset0x99(decimal 153) onthis. -
bl(Branch with Link) invokes target methods.
000 mov x19, x0 ; Save `this` pointer (x0) into x19
020 ldrb w8, [x19, #0x99] ; Load byte from offset 0x99 (firstStart) into w8
022 cbnz w8, #0x25 ; If w8 != 0 (firstStart is true), branch to 0x25
024 bl ButtonMouseClick.Start ; Call Start()
025 ldrb w8, [x19, #0x20] ; Load byte from offset 0x20 (interactable) into w8
027 cbz w8, #0x37 ; If w8 == 0 (interactable is false), branch to exit (0x37)
029 ldrb w8, [x19, #0xA0] ; Load byte from offset 0xA0 (lockButton) into w8
030 cbnz w8, #0x37 ; If w8 != 0 (lockButton is true), branch to exit (0x37)
031 ldr x0, [x19, #0x40] ; Load 64-bit pointer from offset 0x40 (eventEnter) into x0
035 bl UnityEvent.Invoke ; Call eventEnter.Invoke()
036 mov w8, #1 ; Load 1 into w8
037 strb w8, [x19, #0x9A] ; Store byte 1 into offset 0x9A (changeNow)
038 ret ; Return
Notice that despite register name differences (rcx/rbx vs x0/x19), the memory offset numbers (0x99, 0x20, 0xA0, 0x40, 0x9A) are identical across both CPU architectures.
Whether reading x86_64 or ARM64 ISIL, the line-by-line translation to C# is identical:
-
Lazy Init Guard (Offsets 020–024): Checking
firstStart(offset 153 / 0x99). If false, callStart():if (!firstStart) { Start(); }
-
Gate Checks (Offsets 025–030): Checking
interactable(offset 32 / 0x20) andlockButton(offset 160 / 0xA0):if (interactable && !lockButton) { ... }
-
Event Execution & Flag Update (Offsets 031–037): Loading
eventEnter(offset 64 / 0x40), calling.Invoke(), and settingchangeNow(offset 154 / 0x9A):if (eventEnter != null) { eventEnter.Invoke(); } changeNow = true;
using UnityEngine;
using UnityEngine.EventSystems;
public class ButtonMouseClick : MonoBehaviour, IPointerEnterHandler
{
public bool interactable = true;
public UnityEvent eventClick;
public UnityEvent eventEnter;
public UnityEvent eventExit;
public bool firstStart;
public bool changeNow;
public bool lockButton;
public void OnPointerEnter(PointerEventData eventData)
{
if (!firstStart)
{
Start();
}
if (interactable && !lockButton)
{
if (eventEnter != null)
{
eventEnter.Invoke();
}
changeNow = true;
}
}
}| Operation | x86_64 ISIL | ARM64 ISIL | Reconstructed C# |
|---|---|---|---|
| Null / false check | Compare [rbx+X], 0 |
ldrb w8, [x19, #X] + cbz w8
|
if (field == null) or if (!field)
|
| Instance method call |
Move rcx, [rbx+X] + Call
|
ldr x0, [x19, #X] + bl
|
field.Method() |
| Assign boolean true | Move [rbx+X], 1 |
mov w8, #1 + strb w8, [x19, #X]
|
myBool = true; |
| Assign null / zero | Move [rbx+X], 0 |
str xzr, [x19, #X] |
myField = null; |