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.
Cheatsheet for Lesson 6.2
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 calledhealthand the method calledTakeDamagethat writes it.TakeDamage: You are reading a field calledhealthand the method calledTakeDamagethat 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
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.
The runtime decides what an address means
Section titled “The runtime decides what an address means”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.
1 Matching build, live player
Read player base + offset
Address arithmetic0x5000player base+ 0x138health in this build= 0x5138address readAt 0x5000 right now
Object, then fieldPlayer, alivehealth = 100The layout is right and the object is alive, so 0x5138 holds health.
2 The player was destroyed; its storage reused
Read player base + offset
Address arithmetic0x5000player base+ 0x138health in this build= 0x5138address readAt 0x5000 right now
Object, then fieldProjectilea projectile field, not healthSame address, different object: 0x5138 now belongs to a projectile.
3 A new build moved health to +0x140
Read player base + offset
Address arithmetic0x5000player base+ 0x138the old build’s offset= 0x5138address readAt 0x5000 right now
Object, then fieldPlayer, alivearmor, not healthThe 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.
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 identityResult: 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
Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.
collector = moving; event = collect.
Step 2, line 2. The remembered field address is0x1008.
collector = moving; event = collect; remembered_base = 0x1000; remembered_field = 0x1008.
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.
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.
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.
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: native, with its own directory
Section titled “Unreal: native, with its own directory”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.
Other engines: classify the build, not its logo
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.
The answer belongs to one build
Section titled “The answer belongs to one build”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.
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?
'Managed' does not decide whether an address is durable; the specific collector does. Unity's Mono does not move survivors, so fixed addresses behave much as in native games. CoreCLR relocates survivors, so there you need a stable reference such as a handle or a re-lookup.
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.
Plain Markdown, saved in this browser.
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.