Skip to content
Lesson 2.2Following Instructions and Addresses

How Source Becomes a Running Process

Compare native compilation, bytecode interpretation, REPLs, and type checking; then follow a native program through linking and loading.

Lesson 2.2
Read or listenPrint chapter or book
About 28 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 2.2Key ideas, reference and questions

A one-page summary of “How Source Becomes a Running Process” you can keep beside your notes. Plain Markdown.

Lesson 2.2 — How Source Becomes a Running Process: cheatsheet

  • Remember: A compiled game keeps distances, not names. A missing name usually means the program never needed it to run.
  • Also useful: Picture a street and a house number: the street can be built at a different place each day, but the house stays the same number along it.

Key terms

  • Source code: Source code is the text a programmer edits.
  • virtual machine: A virtual machine is a program that understands its own instruction set.
  • bytecode: Its bytecode is instructions for that virtual machine, not the x86 instructions a debugger shows for the CPU.
  • REPL: A REPL is a way to interact with a language tool: read a line, evaluate it, print the answer, then loop for another line.
  • statically typed: Rust is statically typed: every value has a type known to the compiler, often inferred without an explicit annotation.
  • dynamically typed: Lua is dynamically typed: values have types, and a variable can refer to a number at one moment and text at another.
  • EXE: A Windows EXE is an executable file for a program.
  • module: Once loaded, either one is a module with a starting address and a range of code and data in the process.
  • padding: Those gaps are padding, explored in the later object-layout lesson.

Review

  • Q: A release game has no debug information for its health field. Why can the code still read that field without its source name? A: The compiler fixed the layout, so the code uses the field's offset from the object base
  • Q: How does a language that compiles ahead of time to native code reach the CPU? A: It is translated into machine instructions for one target before it runs
Download cheatsheet (.md)

The debugger shows instructions and addresses, while the programmer wrote functions, fields, and variable names. Why are those two views different? And why can an address found by a memory scan move after a restart even though the game still checks affordability and subtracts the same cost?

These differences come from the stages that turn source into a running process. First, distinguish several ways a language implementation can run code. Then we can follow the native executable used in the Windows memory labs and see which facts survive a restart, which require the exact build, and which must be rediscovered after an update.

Source code can take different routes to execution

Section titled “Source code can take different routes to execution”

Source code is the text a programmer edits. A CPU eventually executes machine instructions, but the source does not always become a standalone machine-code file first. Two routes used in this book are:

Rust application: source becomes native code before the program runs.

Rust source
compile to native code
EXE on disk
CPU runs the program

Lua script: source becomes instructions for the Lua virtual machine when a game loads the script.

Lua source
compile to VM bytecode
Lua VM interprets bytecode
CPU runs the Lua VM

Compilation translates code from one representation to another. The Rust route above compiles ahead of time to native instructions for a particular target. A virtual machine is a program that understands its own instruction set. Its bytecode is instructions for that virtual machine, not the x86 instructions a debugger shows for the CPU. Lua also compiles when it loads a source chunk; its virtual machine then interprets the resulting bytecode by carrying out those instructions. This is why “compiled” and “interpreted” are not mutually exclusive labels. The Lua reference manual describes this load-and-execute sequence. Lesson 11.2 shows what changes when a game embeds a Lua interpreter.

A REPL is a way to interact with a language tool: read a line, evaluate it, print the answer, then loop for another line. For instance, an interactive Lua session can evaluate 2 + 3 and show 5 immediately. A file can contain the same calculation and be run later. REPL describes the interaction, not whether the tool uses bytecode, native compilation, or dynamic typing. The Lua interpreter manual describes both interactive and file-based use.

Type checking answers a different question

Section titled “Type checking answers a different question”

The route to execution is separate from when a type mismatch is detected. The same job, giving one variable a number and then text, goes differently in each language:

Rust: statically typed

Rust is statically typed: every value has a type known to the compiler, often inferred without an explicit annotation. This fragment fails to compile, because one variable cannot hold both a u32 number and a string:

let mut score: u32 = 7;
score = "seven"; // rejected by the compiler

Lua: dynamically typed

Lua is dynamically typed: values have types, and a variable can refer to a number at one moment and text at another. This code can make both assignments, but its final arithmetic operation fails when run:

score = 7
score = "seven"
print(score + 1) -- error: text cannot be used as this number

The difference is about where that particular mistake is caught. Rust can still have run-time errors, and Lua catches some mistakes, such as malformed syntax, while loading code. Neither typing style alone tells you whether code is compiled to native instructions, compiled to bytecode, or entered through a REPL. The Rust data-types chapter and the Lua reference manual give the language-specific rules.

The rest of this lesson follows the native executable route used for Rust tools and the Windows game builds in the early memory labs. Lua’s virtual-machine route returns when we study scripting.

Four stages, each deciding something different

Section titled “Four stages, each deciding something different”

A Windows EXE is an executable file for a program. A DLL (dynamic-link library) is compiled code that a running program can load and call. Once loaded, either one is a module with a starting address and a range of code and data in the process.

A file address becomes a live address through its RVA. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

preferred_base = 0x400000
image_gold = 0x4A3010
gold_rva = image_gold - preferred_base
Windows chooses the selected live base
live_gold = live_base + gold_rva
report relocation and unchanged field offset

Result: live gold at 0xAF3010. Relocation moves the image; it does not redefine the player’s field layout.

Final values: base = 10813440; image_gold = 0x4A3010; gold_rva = 0xA3010; live_base = 0xA50000; live_gold = 0xAF3010; relocation = 0x650000; field_offset = 0x30.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.

    base = 10813440.

  2. Step 2, line 2. The linked image records an address relative to its preferred base.

    base = 10813440; image_gold = 0x4A3010.

  3. Step 3, line 3. Subtract the preferred base to recover the RVA.

    base = 10813440; image_gold = 0x4A3010; gold_rva = 0xA3010.

  4. Step 4, line 4. A load address can change between runs.

    base = 10813440; image_gold = 0x4A3010; gold_rva = 0xA3010; live_base = 0xA50000.

  5. Step 5, line 5. Add the same RVA to the actual loaded base.

    base = 10813440; image_gold = 0x4A3010; gold_rva = 0xA3010; live_base = 0xA50000; live_gold = 0xAF3010; relocation = 0x650000; field_offset = 0x30.

  6. Step 6, line 6. Result: live gold at 0xAF3010. Relocation moves the image; it does not redefine the player’s field layout.

    base = 10813440; image_gold = 0x4A3010; gold_rva = 0xA3010; live_base = 0xA50000; live_gold = 0xAF3010; relocation = 0x650000; field_offset = 0x30.

Watch it happenFrom source files to a running process

One program changes form in six stages. The source line player.health -= damage becomes an instruction using the distance 0x30; the global gold gets the address 0x004A3010 in the linked image; the file records a preferred base of 0x00400000; Windows loads it at 0x00A50000 and shifts that address by 0x00650000 to 0x00AF3010; then threads run the instructions at those live addresses.

Read the steps
  1. Source files describe the program for its author: names, types, comments. player.health and gold are names a person chose.
  2. Compile: the compiler turns each source file into an object file and fixes the layout. The name health is gone; only its distance from the object base, 0x30, is left. gold still has no address.
  3. Link: the linker resolves names and lays out the image. It gives gold an address inside it: the preferred base 0x00400000 plus 0x0A3010 is 0x004A3010.
  4. File on disk: the EXE records its preferred base and the imports it will need. Every address inside is written as if the image will sit at that base.
  5. Load: Windows maps the image at 0x00A50000 instead. It adds the difference, 0x00A50000 - 0x00400000 = 0x00650000, to every stored address, so gold moves from 0x004A3010 to 0x00AF3010.
  6. Run: threads execute the instructions from the mapped image, at the live addresses the loader chose.

Follow one compiled route: source becomes object files, a linked image, and finally a process whose live addresses the loader chose.

Each stage keeps what the next stage needs. A release build may omit information that helped the programmer but is unnecessary for execution. Debug information can preserve some names separately; we will distinguish that case below.

The compiler decides layout, and stops needing your names

Section titled “The compiler decides layout, and stops needing your names”

The compiler translates source into object code, often stored in .obj files by the Microsoft toolchain used for these Windows C++ examples. Two of its decisions matter throughout this book.

First, it chooses field order, padding, and alignment under the language’s layout requirements. For an example player structure, it may leave unused bytes between fields so each field starts at an address suited to its type. Those gaps are padding, explored in the later object-layout lesson.

Second, the machine instruction no longer needs the word health. The name helped the programmer refer to the field; the instruction uses its distance from the object base. If health begins 48 bytes into the object, a debugger may show the read as [ecx+0x30]: the address held in the ecx register, plus hexadecimal 0x30, which is decimal 48. Lesson 2.3 explains registers and this notation. A normal release executable often omits local and field names, though debug information can preserve them. A missing name is not evidence that it was encrypted or hidden.

Function names survive slightly longer, because the linker still has work to do with them.

The linker resolves names, then it does not need them either

Section titled “The linker resolves names, then it does not need them either”

Your game is many object files plus libraries, and one file’s call to a function defined in another may still be an unresolved reference. The linker matches references to definitions it has, lays the sections out into the final image, and records the references that a loader must finish later. A call to an internal game function can be resolved here; a call into a DLL still needs the DLL’s address at run time.

After that, many internal names have served their purpose. Debug information can be kept — the Microsoft toolchain often puts it in a separate .pdb file — but the EXE does not need it to run. Imports and exports can still carry names in the executable; the next section shows why an imported function name may remain.

This explains why a debugger may show an imported Windows function’s name but only a numeric address for an internal game function. A binary without debug symbols is not necessarily obfuscated; it may simply lack source names that execution does not require.

Two things the linker deliberately leaves unfinished

Section titled “Two things the linker deliberately leaves unfinished”

Not everything can be decided at build time, and the two exceptions are exactly the ones later chapters spend time on.

Where the module will be. The linker does not know which address Windows will choose, so it records a preferred base and includes a table of every location that would need adjusting if a different base is used. An offset measures a distance from the beginning of that module. The later address-movement lesson uses the distinction: for the same build, that distance can stay fixed while the loader chooses a different starting address.

Where the DLL functions will be. The linker knows the game calls MessageBoxW in user32.dll, but not the address, which will not exist until the DLL is mapped. So it writes down the requirement — which DLL, which function — and leaves a table of slots for the loader to fill with real addresses. That table is the Import Address Table:

on diska reference to the name MessageBoxW in user32.dll
in memorythe address of MessageBoxW, written by the loader
One Import Address Table slot. The game's code calls through the slot, so the call works only once the loader has written the real address there.

Lesson 8.4 hooks a program’s own copy of this table — a technique that works precisely because the linker left the address for someone else to fill in.

The loader maps the sections into memory with the right permissions, applies those relocations if it picked a different base, fills in the import table, prepares thread-local storage (data kept separately for each thread), and runs the initialisation code. The later loader-boundary lessons follow these details. They explain why a DLL’s DllMain initialisation function cannot safely perform every task a normal function can.

The executable-file chapter will compare the two layouts: content arranged for storage on disk and content arranged for execution in memory. The loader is the connection between them; file positions do not automatically equal positions in the running process.

ExampleOne location across two runs

Suppose the linker placed an instruction 0x1350 bytes from the beginning of the game’s module. In one run, Windows loads that module at 0x00400000; in another, it loads it at 0x00600000. The instruction did not move within the module, even though its absolute address changed:

Run Module base Instruction offset Instruction address
first 0x00400000 0x1350 0x00401350
second 0x00600000 0x1350 0x00601350

The same distinction applies inside an object, but the starting point is different. If the compiler put health at +0x30 within a player object, and that object begins at 0x01A2B000 in the first run, its health field is at 0x01A2B030. A newly created player at 0x0208C000 puts the same field at 0x0208C030. The module offset measures from the loaded module; the field offset measures from that particular object. Neither offset alone is a complete address. Later pointer work uses this distinction every time it adds an offset to a base address.

Predict first

The same build runs a third time, and Windows loads the game's module at 0x00A10000.

Where is the instruction from the table now?

Answer: 0x00A11350. The instruction is still 0x1350 bytes into the module, so add that offset to this run’s base: 0x00A10000 + 0x00001350 = 0x00A11350.

Base + offset = this run’s addressThe offset is fixed by the build. Change the base the loader chose and only the absolute address moves.

Add the offset to this run’s base

This run

Base (find it every run)
0x00400000
Offset (fixed by the build)
+ 0x00001350
Address to read this run
0x00401350

What to write down

Address − base
0x00401350 − 0x00400000 = 0x1350the offset; the address changes with the base

0x00400000 + 0x00001350 = 0x00401350. Next run, find the new base and add the same 0x1350: the offset is what you keep.

The practical payoff is knowing which kinds of location survive a new game version.

What you found Safe to reuse? Why
A raw address from one run No reliable reuse after a restart The module or object may have a different base
An offset from the module base Usually after a restart of the same build The linker fixed its place within that image
A field offset like +0x30 Usually for objects of that type in the same build The compiler chose the layout
A byte pattern Verify it in each build Compilation or linking can change its bytes
“Health is 48 bytes in” Recheck after a rebuild A new field earlier in the structure may shift it

That last row is the one Lesson 14.1 turns into a migration procedure. In this fixed-order example, inserting a field moves the later fields and leaves the earlier ones in place. A different field-order or alignment requirement can change more, so measure the new build rather than assuming the same shift everywhere:

+0x00id
+0x04health
+0x08max_health
+0x0Cgold
The player structure in one build.
+0x00id
+0x04armor
+0x08health
+0x0Cmax_health
+0x10gold
each moved 4 bytes later
The next build adds armor. id keeps its offset; every field after the new one moves.

Optimization is why the code does not match the source

Section titled “Optimization is why the code does not match the source”

One more consequence, and it explains a lot of confusing disassembly. An optimising build is allowed to produce anything that behaves the same. It can paste a small function into each of its callers so no call remains, keep a variable only in a register so it never touches memory, drop a variable whose result is never used, reorder work, and merge two identical functions.

So when a function you expected is missing, the usual explanation is not that it was hidden. It is that the compiler decided the fastest way to do that work did not involve a separate function. This is also why a debug build and a release build of the same source can be so different to read, and why an address or offset belongs to the exact build it came from.

The compiler chooses field layouts and may remove or transform source functions. The linker fixes positions within a native image and records DLL imports; the loader chooses a run-time base and fills in external addresses. A module-relative offset can therefore survive a restart of the same build while a raw address changes. Rebuilding after a field insertion can shift later offsets, so old offsets need rechecking. Release binaries may omit source names even while import names or separate debug information remain.

We now know why a debugger can show addresses and transformed operations instead of the source’s original names. How Assembly Describes a Running Game gives those operations a readable vocabulary and follows a gold purchase through its comparison, branch, and subtraction.

Knowledge check

Follow the names

A release game has no debug information for its health field. Why can the code still read that field without its source name?

Finished Lesson 2.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 2: 10 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.