Skip to content

Tutorial Reading and Reconstructing ISIL

clericall edited this page Aug 13, 2026 · 1 revision

Tutorial: Reading and Reconstructing Raw 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).


What You Need Before Starting

  1. AssetRipper output: A decompiled Unity project folder containing your game's scripts and assets.
  2. Cpp2IL ISIL export: Generated by running:
    Cpp2IL.exe --game-path "C:\Path\To\Game" --output-as isil --out "C:\Path\To\Output"
  3. A text editor where you can open two files side-by-side (e.g. VS Code).

Field Offset Mapping (ButtonMouseClick Example)

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

Worked Example 1: x86_64 Disassembly Walkthrough

In 64-bit x86_64 builds, CPU register rcx holds this on entry and is saved into rbx. [rbx + offset] reads fields on this.

Raw ISIL (x86_64)

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

Worked Example 2: ARM64 Disassembly Walkthrough

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:

  • x0 holds this on entry and is saved into callee-saved register x19.
  • w8 / w9 are 32-bit register views used for booleans (1 byte) and integers (4 bytes).
  • [x19, #0x99] accesses memory offset 0x99 (decimal 153) on this.
  • bl (Branch with Link) invokes target methods.

Raw ISIL (ARM64)

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.


Step-by-Step C# Logic Reconstruction

Whether reading x86_64 or ARM64 ISIL, the line-by-line translation to C# is identical:

  1. Lazy Init Guard (Offsets 020–024): Checking firstStart (offset 153 / 0x99). If false, call Start():

    if (!firstStart)
    {
        Start();
    }
  2. Gate Checks (Offsets 025–030): Checking interactable (offset 32 / 0x20) and lockButton (offset 160 / 0xA0):

    if (interactable && !lockButton)
    {
        ...
    }
  3. Event Execution & Flag Update (Offsets 031–037): Loading eventEnter (offset 64 / 0x40), calling .Invoke(), and setting changeNow (offset 154 / 0x9A):

    if (eventEnter != null)
    {
        eventEnter.Invoke();
    }
    changeNow = true;

Reconstructed C# Component

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;
        }
    }
}

Quick Reference for Common ISIL Patterns

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;