Skip to content
Lesson 6.2Windows Processes and Callable Interfaces

Game Runtimes and Object Lifetimes

Native code, Mono, IL2CPP, .NET, and Java expose different evidence about objects and their lifetimes; classify a build before trusting an offset.

Lesson 6.2
Read or listenPrint chapter or book
About 26 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 6.2Key ideas, reference and questions

A one-page summary of “Game Runtimes and Object Lifetimes” you can keep beside your notes. Plain Markdown.

Lesson 6.2 — Game Runtimes and Object Lifetimes: cheatsheet

  • Remember: An offset describes one layout; it cannot keep the object alive or prevent its storage from moving.
  • Also useful: The installation folder is a proposed inventory; the loaded-module list is the running process’s actual inventory. Compare both before classifying the runtime.

Key terms

  • health: You are reading a field called health and the method called TakeDamage that writes it.
  • TakeDamage: You are reading a field called health and the method called TakeDamage that writes it.

Review

  • Q: Two games are both described as 'managed and garbage collected': one is Unity on the Mono backend, the other runs on .NET CoreCLR. Which statement is accurate? A: Unity's non-compacting collector leaves survivors in place; CoreCLR's compacting one can relocate them
  • Q: A Unity game's data folder holds its gameplay code as a managed assembly of intermediate language. Which backend does that suggest? A: Mono, which runs C# as managed intermediate language
Download cheatsheet (.md)

An executable has become a running process, but its address space does not yet tell us what runs the game’s logic. Before reading a memory map or applying a native-code method to an unfamiliar game, identify its engine and runtime. Wesnoth, AssaultCube, and Urban Terror let us practice with compiled native code. A game using a managed runtime or script host may give us names and types that those labs had to infer, and its objects may have different lifetimes and may move in memory.

For example, searching for a field at player base + 0x138 assumes that we have the right kind of player object, that the field has that layout in this build, and that the object is still alive when we read it. Discover the runtime first; then choose what evidence can answer the health question.

native code
Unity, Mono backend
Unity, IL2CPP backend
CoreCLR or JVM
Unfamiliar game
What runs the logic?
inspect this build's
object layout and lifetime
managed assemblies can expose names;
Unity's GC is non-compacting
C# compiled to native code;
inspect available metadata
collector may relocate objects;
raw address is not identity

The branches are not different difficulty levels. They are different questions. In any branch, an offset describes a layout in a particular build; it does not keep an object alive. The runtime changes what evidence is available for recovering that layout and whether the object can move during its lifetime.

The same address arithmetic, three outcomes0x5000 + 0x138 = 0x5138 every time. What sits there depends on the object’s lifetime and the build’s layout.
  1. 1 Matching build, live player

    Read player base + offset

    Address arithmetic
    0x5000player base
    + 0x138health in this build
    = 0x5138address read

    At 0x5000 right now

    Object, then field
    Player, alive
    health = 100

    The layout is right and the object is alive, so 0x5138 holds health.

  2. 2 The player was destroyed; its storage reused

    Read player base + offset

    Address arithmetic
    0x5000player base
    + 0x138health in this build
    = 0x5138address read

    At 0x5000 right now

    Object, then field
    Projectile
    a projectile field, not health

    Same address, different object: 0x5138 now belongs to a projectile.

  3. 3 A new build moved health to +0x140

    Read player base + offset

    Address arithmetic
    0x5000player base
    + 0x138the old build’s offset
    = 0x5138address read

    At 0x5000 right now

    Object, then field
    Player, alive
    armor, not health

    The object is right but the layout is not: in this build +0x138 holds another field.

0x5000 + 0x138 = 0x5138 every time. What sits there depends on the object’s lifetime and the build’s layout.

Read the folder before you attach anything

Section titled “Read the folder before you attach anything”

The quickest answer is on disk and costs nothing:

What you see beside the executable Very likely
<Game>_Data/Managed/Assembly-CSharp.dll Unity, Mono backend
GameAssembly.dll and il2cpp_data/Metadata/global-metadata.dat Unity, IL2CPP backend
<Game>/Binaries/Win64/<Game>-Win64-Shipping.exe, Engine/ Unreal Engine
A large .pck beside a small executable Godot
coreclr.dll or a .runtimeconfig.json .NET
jvm.dll, .jar files Java
Only an EXE and ordinary DLLs Possibly native code; inspect the loaded modules before deciding

Confirm it against the process’s loaded-module list, because a folder can lie and a launcher can start something other than the executable you inspected. The folder shows what is installed; the module list shows what the running process actually mapped. Lesson 12.1 later shows a tool for collecting that list.

Native: reuse the earlier method, with its checks

Section titled “Native: reuse the earlier method, with its checks”

For a native build, the debugger and PE techniques from Chapters 2, 3, and 5 remain useful. An object layout can have fixed field offsets within that build. The object base can still change after allocation, collection growth, destruction, or a restart, as Lessons 1.8 and 3.10 explained. Reconfirm the module, object identity, and lifetime before reusing an address.

Unity with the Mono backend: the code is readable

Section titled “Unity with the Mono backend: the code is readable”

Assembly-CSharp.dll is not a native DLL. It holds .NET intermediate language, which keeps class names, method names, and field names in its metadata. A decompiler shows something close to the original C#, including the field you are looking for and the method that changes it.

This changes the shape of the work completely. You are no longer inferring that offset 0x138 behaves like health across controlled tests. You are reading a field called health and the method called TakeDamage that writes it. The recovery skills from Chapter 3 still matter, but they become confirmation rather than discovery.

One useful nuance: Unity documents its managed garbage collector as non-compacting. It does not move surviving managed objects merely to close gaps in the heap. That removes one reason an address might change, but it does not make a pointer chain a permanent identity: the game can destroy and recreate an object, reuse storage, or change the layout in another build.

Unity with IL2CPP: native code with some metadata

Section titled “Unity with IL2CPP: native code with some metadata”

IL2CPP converts the same C# ahead of time into C++ and compiles it, so the logic ships as native code inside GameAssembly.dll. There is no Assembly-CSharp.dll to decompile, and at first glance you are back to reading disassembly.

The runtime also needs metadata for the managed features that remain in the build. A typical Unity IL2CPP layout has global-metadata.dat; it can reveal useful names and type relationships. It is not a promise that every original class, method, or field name survived: build stripping and packaging affect what remains. Correlate the metadata you can actually inspect with the native code and with a controlled observation in that exact build.

CoreCLR and the JVM: addresses that can move

Section titled “CoreCLR and the JVM: addresses that can move”

A .NET game running on CoreCLR is the case where the earlier assumption really does break. CoreCLR’s garbage collector is generational and compacting: it relocates surviving objects to remove gaps. An object’s address is therefore valid only until the next collection that moves it.

t0object at 0x5000
t1, after GCobject at 0x5000
With Unity's non-compacting collector, this surviving object stays at the same address across a collection. Destruction and recreation can still change its address later.
t0object at 0x5000
t1, after GCobject moved to 0x5A00
CoreCLR or a moving JVM collector such as G1 can relocate a surviving object; after this example collection, its old raw address is no longer valid.

That differs from Unity’s non-compacting collector. Both are “managed” and “garbage collected”; those words alone do not tell you whether a particular object may move. Check the runtime and collector, as well as the object’s lifetime.

Where objects can move, a raw address is not a durable identity. Use a runtime-supported reference, a validated handle, or a fresh lookup when the runtime exposes one. Lesson 3.10 built slot-and-generation handles for a similar identity problem: storage can be reused even when nothing moves.

Many JVM collectors, including G1, also move surviving objects. The collector and configuration matter; do not infer a permanent address from the word “Java” alone.

A layout does not keep an object at an address. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

object begins at address 0x1000
remember address and field offset 8
apply the selected lifetime event
resolve the current object identity
compare the remembered address
read only through a still-valid identity

Result: use the newly resolved field at 0x2008. The unchanged offset never guaranteed lifetime or location.

Final values: collector = moving; event = collect; remembered_base = 0x1000; remembered_field = 0x1008; alive = true; current_base = 0x2000; current_field = 0x2008; remembered_valid = false.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.

    collector = moving; event = collect.

  2. Step 2, line 2. The remembered field address is0x1008.

    collector = moving; event = collect; remembered_base = 0x1000; remembered_field = 0x1008.

  3. Step 3, line 3. This collection moves the object to0x2000.

    collector = moving; event = collect; remembered_base = 0x1000; remembered_field = 0x1008; alive = true; current_base = 0x2000.

  4. Step 4, line 4. Resolve identity again instead of treating the old readable number as ownership.

    collector = moving; event = collect; remembered_base = 0x1000; remembered_field = 0x1008; alive = true; current_base = 0x2000; current_field = 0x2008.

  5. Step 5, line 5. The stored pointer is valid only if it still identifies the live allocation.

    collector = moving; event = collect; remembered_base = 0x1000; remembered_field = 0x1008; alive = true; current_base = 0x2000; current_field = 0x2008; remembered_valid = false.

  6. Step 6, line 6. Result: use the newly resolved field at 0x2008. The unchanged offset never guaranteed lifetime or location.

    collector = moving; event = collect; remembered_base = 0x1000; remembered_field = 0x1008; alive = true; current_base = 0x2000; current_field = 0x2008; remembered_valid = false.

Unreal combines native C++ code with an engine object system. Its reflected UObject types carry class and property information, while ordinary native types and unreflected fields can still require the earlier layout method. That makes the engine’s object and name facilities useful orientation points, not a guarantee that every gameplay value is listed there. Inspect the exact game build before choosing a search target.

Section titled “Other engines: classify the build, not its logo”

A .pck file may suggest Godot, but a game can add native extensions or use a different scripting arrangement. A launcher can also start a different executable from the one beside the package you inspected. Treat the folder clues as a starting hypothesis and verify it with the loaded-module inventory and one observed behavior.

A runtime classification goes stale the same way an offset does. Unity lets a studio switch a project’s scripting backend between Mono and IL2CPP, so an early-access release and the finished game can land on different branches of the flowchart above. The classification holds for the exact build you inspected; a new build means reading the folder and the loaded modules again.

Apply the method to a second folder. Suppose you see GameAssembly.dll and il2cpp_data/Metadata/global-metadata.dat, but no Assembly-CSharp.dll. Unity IL2CPP is a useful first hypothesis, so inspect the loaded modules and available metadata. It would be a mistake to conclude that every old field name survived, or that a pointer found today will still identify the same object after a scene reload. If the next build instead loads coreclr.dll, revisit the collector and reference model before reusing any raw object address. Verify runtime, build, and object lifetime separately before reusing an address.

References: Unity scripting back ends, Unity garbage-collection modes, .NET garbage collection, Oracle G1 garbage collection, and Unreal’s reflection system.

Knowledge check

Which collector moves objects?

Two games are both described as 'managed and garbage collected': one is Unity on the Mono backend, the other runs on .NET CoreCLR. Which statement is accurate?

Finished Lesson 6.2?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 6: 11 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.