How Assembly Describes a Running Game
Understand what assembly is, how the CPU executes it, and how registers, memory, flags, stacks, and calls fit together.
Cheatsheet for Lesson 2.3
A one-page summary of “How Assembly Describes a Running Game” you can keep beside your notes. Plain Markdown.
Lesson 2.3 — How Assembly Describes a Running Game: cheatsheet
- Remember: Read each instruction as a plain sentence: what is read, what is written, and how many bytes. Skip memorizing opcodes.
- Also useful: Nested returns unwind one call at a time.
retuses the top return address; releasing a stack slot does not erase its bytes.
Key terms
eip:eipis the instruction pointer.- instruction pointer:
eipis the instruction pointer. rcx: Without brackets,rcxis a number.eflags:eflagsis a register containing many one-bit results.eax: Ifeaxis compared, trace backward to the instruction that last wroteeax.- basic block: A straight sequence with one entry and one exit is often called a basic block.
Formulas and patterns
jb (unsigned): is 100 below 4,294,967,295? yes -> refuse the purchase;jl (signed): is 100 less than -1? no -> allow it, then subtract;-1, which adds a gold
Review
- Q: In
mov eax, [rcx+8], what do the brackets mean? A: Read the value stored in memory at address rcx plus 8 - Q: A
cmpinstruction compares a field witheax, and you want to know where thateaxvalue came from. What should you look for? A: The last instruction before it that wroteeax
For an unfamiliar operation, use the instruction reference: it explains operand forms, changed values and flags, with an adjustable worked trace for each instruction.
What assembly actually is
Section titled “What assembly actually is”A CPU does not run source code or even the words mov and sub. It runs machine code: bytes whose bit patterns mean operations for one CPU architecture. Continue the 100-to-75 gold purchase from Lesson 2.1: this first instruction loads its price, 25, into a register. Later instructions will compare and subtract it.
Assembly language is a human-readable spelling of those operations. For 32-bit x86, these five bytes:
B8 means “put the 32-bit number that follows into eax.” The four bytes after it are that number.can be shown as:
mov eax, 25The byte B8 selects one form of mov. The next four bytes hold 25 as a little-endian 32-bit number. The readable instruction and the bytes describe the same CPU action at different levels.
An assembler travels from assembly text to machine code. A disassembler travels from machine code to assembly text. A debugger includes a disassembler and also shows the live register and memory values while the instructions execute.
Assembly preserves machine effects, not the author’s original explanation. A release build usually does not retain friendly variable names, comments, source loops, or the exact class boundaries that produced it. Optimization may inline two functions into one, remove a variable completely, or replace multiplication with a different but equivalent instruction sequence.
Therefore, disassembly shows what the machine does; it is not recovered source code. A decompiler can suggest source-like structure, but you still verify its types, signs, widths, branches, and calling convention against the actual instructions.
Assembly depends on the CPU architecture
Section titled “Assembly depends on the CPU architecture”This book’s historical game builds are 32-bit x86 programs. Their main registers are named eax, ebx, ecx, edx, esi, edi, esp, ebp, and eip.
A 64-bit x86 program uses wider names such as rax and rip. ARM uses a different instruction set and different register names. Assembly is not one universal language; it is the readable face of a particular processor architecture.
The examples use Intel syntax, where the destination normally appears before the source:
mov eax, ebx ; copy ebx into eaxOther tools may display AT&T syntax with a different order and punctuation. Pick one syntax while learning so the notation does not hide the idea.
Read the whole disassembly row
Section titled “Read the whole disassembly row”A debugger may show:
007CCD91 29 42 04 sub dword ptr [edx+4], eaxRead it from left to right:
The parts of a disassembly row
| Part | Meaning |
|---|---|
007CCD91 |
address of the first instruction byte in this run |
29 42 04 |
the three machine-code bytes |
sub |
mnemonic: the short human name for the operation |
dword ptr [edx+4] |
destination: a four-byte value in memory |
eax |
source: the four-byte value to subtract |
In plain English: “Take the 32-bit number stored four bytes after the address in edx, subtract eax, and store the answer back in that same memory location.”
This one sentence is far more useful than memorizing that opcode 29 means a form of subtraction.
The CPU repeats a tiny cycle
Section titled “The CPU repeats a tiny cycle”At a simplified level, one thread repeatedly:
- uses
eipto find the next instruction; - fetches its bytes from executable memory;
- decodes what the bytes mean;
- reads the required registers or memory;
- performs the operation;
- writes the result;
- advances
eipor jumps somewhere else.
eip is the instruction pointer. It is not a normal game variable. It tells the CPU where this thread is executing. A jump, call, return, exception, or debugger action can change it.
On the ordinary path, eip advances by the length the decoder just found.
The five bytes 29 42 04 85 C0 decode from offset 0 as
sub dword ptr [edx+4], eax (3 bytes) and then test eax, eax (2 bytes), so
the next instruction starts at 0 + 3 + 2 = 5.
Predict first
Suppose eip points at offset 1 instead, one byte inside the sub. What does the CPU do?
Answer: It decodes different instructions from the same bytes. The CPU keeps no list of where instructions begin. At offset 1 the byte 42 is a complete instruction, inc edx, and 04 85 is add al, 85h: real instructions the program never meant.
1 eip = 0: 3 bytes, next start 0 + 3 = 3
Bytes 29 42 04 85 C0offset 0offset 429 42 04sub dword ptr [edx+4], eax85 C0not decoded yet29 42 04 is one instruction, so decoding continues at offset 3.
2 eip = 3: 2 bytes, next start 3 + 2 = 5
Bytes 29 42 04 85 C0offset 0offset 429 42 04sub dword ptr [edx+4], eax85 C0test eax, eax85 C0 is the next instruction. The one after it starts at offset 5.
eip moves past the bytes the instruction used: 0 + 3 = 3, then 3 + 2 = 5.
1 eip = 1: 42 is inc edx, next start 2
Bytes 29 42 04 85 C0offset 0offset 429skipped42inc edx04 85 C0not decoded yetThe byte 42 is a complete one-byte instruction.
2 eip = 2: 04 85 is add al, 85h, next start 4
Bytes 29 42 04 85 C0offset 0offset 429skipped42inc edx04 85add al, 85hC0not yet04 means “add the next byte to al”, so 85 becomes its number.
3 eip = 4: C0 swallows what follows
Bytes 29 42 04 85 C0offset 0offset 429skipped42inc edx04 85add al, 85hC0 …needs 2+ more bytesC0 starts a shift or rotate that needs at least two more bytes, so it eats the start of the next real instruction at offset 5.
The same bytes, decoded from offset 1. Each result is a real instruction; none is what the program meant.
Registers hold the CPU’s current working values
Section titled “Registers hold the CPU’s current working values”Registers are tiny, very fast storage locations built into the CPU. On 32-bit x86, each general-purpose register below holds 32 bits:
x86 registers at a glance
| Register | Common historical role | Important warning |
|---|---|---|
eax |
return value and arithmetic | a function may use it for anything temporary |
ebx |
preserved general value | “preserved” depends on the calling convention |
ecx |
counter or this pointer |
not every use is a counter or object |
edx |
arithmetic or extra argument | meaning comes from surrounding instructions |
esi / edi |
source/destination pointers | compilers also use them as ordinary storage |
esp |
top of the current thread’s stack | unbalanced changes usually crash on return |
ebp |
stack-frame reference in some builds | optimized code may use it as a normal register |
eip |
address of the next instruction | controlled by branches, calls, and returns |
The role column is a clue, not a promise. Ask what the current function actually reads and writes.
Bits do not carry their own meaning
Section titled “Bits do not carry their own meaning”The register value 0xFFFF_FFFF is only 32 one-bits. An instruction may interpret those bits as:
- unsigned integer
4,294,967,295; - signed integer
-1; - four bytes;
- part of a pointer;
- a bit mask where every switch is on.
u32: 4,294,967,295i32: −1Source-level types give those bits a declared meaning. In disassembly, only the instruction that uses the bits, and the code around it, tells you which meaning applies.
You do not need to memorize the whole x86 instruction set. Start with a handful of verbs, but always connect each verb to the CPU, registers, memory, and control flow described above.
The debugger changes the view, not the bytes
Section titled “The debugger changes the view, not the bytes”The same four bytes can be displayed as hexadecimal, a signed integer, an unsigned integer, a float, four characters, or an instruction. Changing the debugger’s display format does not convert the target’s memory. It changes only the question you ask about those bits.
Use the program’s operations to choose the view:
- a floating-point instruction supports a float interpretation;
- sign extension or a signed conditional jump supports a signed integer;
- a memory dereference supports an address interpretation;
- repeated one-byte reads ending at zero support a C-string interpretation;
- bytes that decode into valid-looking instructions are code only if control flow actually reaches them.
This last point matters because a code section can contain alignment bytes, constants, and jump tables. A disassembler can produce a plausible instruction from almost any byte sequence. What makes bytes code is that execution reaches them: a jump, call, or fall-through from other code leads there, or a debugger sees the CPU run them.
Moving data
Section titled “Moving data”mov destination, source copies data:
mov eax, 25 ; eax now contains 25mov ebx, eax ; ebx now contains 25Square brackets mean “use the memory at this address”:
mov eax, [rcx] ; read 4 bytes from the address in rcxmov [rcx], eax ; write 4 bytes to that addressWithout brackets, rcx is a number. With brackets, that number is treated as an address.
For the 32-bit course games, the same examples usually use ecx instead of rcx:
mov eax, [ecx] ; read four bytes from memorymov [ecx], eax ; write four bytes to memorymov eax, [ecx] copies these four bytes: eax = 100ecx holds 0x5000. mov eax, ecx copies the number 0x5000 itself. The brackets in mov eax, [ecx] go to that address and copy what is stored there.The instruction decides the size. byte ptr means one byte, word ptr means two, dword ptr means four, and qword ptr means eight.
Little endian explains “backward” bytes
Section titled “Little endian explains “backward” bytes”x86 stores a multi-byte integer with its least-significant byte first. The value 0x12345678 appears in memory as:
0x12345678The number is not reversed when the CPU uses it. This is only the byte order in memory. The conversion helpers u32::from_le_bytes and to_le_bytes make the chosen byte order explicit.
Doing math
Section titled “Doing math”add eax, 10 ; eax = eax + 10sub eax, 3 ; eax = eax - 3inc eax ; eax = eax + 1dec eax ; eax = eax - 1The source-level equivalent is easier to read:
let mut value = 25_i32;value += 10;value -= 3;value += 1;value -= 1;Starting at 25, those four operations leave value at 32: add 10, subtract
3, add 1, subtract 1. The assembly operations would do the same arithmetic
if eax started at 25. The debugger lets us check the actual register value
before and after each instruction instead of assuming that starting point.
Comparing and branching
Section titled “Comparing and branching”cmp performs a comparison and updates flags. A jump reads those flags:
cmp eax, 0je player_is_deadje means “jump if equal.” Common jumps include:
Jumps after a compare
| Instruction | Meaning after cmp left, right |
|---|---|
je / jz |
equal / zero |
jne / jnz |
not equal / not zero |
jg |
greater, signed |
jl |
less, signed |
ja |
above, unsigned |
jb |
below, unsigned |
jmp |
always jump |
Signed and unsigned matter because the same bits can be read in different ways. 0xFFFF_FFFF is 4,294,967,295 as a u32, but -1 as an i32.
That choice decides which branch actually runs. Imagine a price of
0xFFFFFFFF arriving at a gold check while the player holds 100 gold:
jb (unsigned): is 100 below 4,294,967,295? yes -> refuse the purchasejl (signed): is 100 less than -1? no -> allow it, then subtract -1, which adds a goldIdentical bits, identical cmp, opposite outcomes. When you are reading a
check that guards something valuable, the signedness of the jump is not a
footnote — it is very often the entire bug.
The type makes that choice visible:
let signed = -1_i32;let unsigned = u32::MAX;
assert_eq!(signed.to_ne_bytes(), unsigned.to_ne_bytes());The assertion passes because -1_i32 and u32::MAX have the same four-byte
pattern on this target. A signed jump treats that pattern as −1; an unsigned
jump treats it as 4,294,967,295. The branch instruction determines which
meaning controls the next step.
Flags are one-bit answers from the last operation
Section titled “Flags are one-bit answers from the last operation”eflags is a register containing many one-bit results. Common ones include:
| Flag | Plain-English question it helps answer |
|---|---|
| ZF, zero flag | Was the result zero? |
| CF, carry flag | Did unsigned arithmetic carry or borrow? |
| SF, sign flag | Is the result’s top bit set? |
| OF, overflow flag | Did signed arithmetic overflow its range? |
cmp eax, ebx behaves like a subtraction that throws away the numeric answer but keeps the flags. je, jg, ja, and the other conditional jumps interpret those flags in different combinations.
This is why signed and unsigned jumps differ even though the register bits are identical.
Here are the flags after the two comparisons in the gold check at the end of this lesson:
je and jb both fall through.jb jump. SF differs from OF, which makes jl jump as well.Lesson 1.10 develops bit masks, shifts, and rotations before we use their assembly forms. It is a useful optional detour when these operations are new to you.
Calls, returns, and the stack
Section titled “Calls, returns, and the stack”call jumps into a function and remembers where to return. ret goes back.
call calculate_damage; execution continues here after `ret`The stack is a last-in, first-out area used for return addresses, saved registers, and some local values:
push rbx; use rbxpop rbxretOn 32-bit x86, the stack grows toward lower addresses. push eax subtracts four from esp and stores eax there. pop eax loads four bytes and adds four back to esp.
esp after push eaxpush eax with eax = 25. The stack pointer moves down four bytes and the value lands there. pop eax reads it back and moves esp up again.call target is roughly:
push address_of_the_next_instructionjump targetret roughly pops that saved address into eip. If a hook adds an unmatched push, ret may treat some unrelated value as an address and jump into nonsense.
Follow the call calculate_damage above through its return. The saved address
belongs to the caller’s next instruction; it is separate from the address of
calculate_damage:
A listing shows a call to calculate_damage at 0x00401020. The address of the next instruction, 0x00401025, is pushed onto a stack that grows leftward, and esp moves from 0x0019FF30 to 0x0019FF2C. The function pushes and pops ebx, then ret moves the saved address into eip, esp returns to 0x0019FF30, and execution resumes after the call.
Read the steps
- The caller reaches call calculate_damage at 0x00401020. Execution must enter the function, and later continue at the instruction just after this call.
- call pushes the address of the next instruction, 0x00401020 + 5 = 0x00401025, onto the stack. esp drops by four, from 0x0019FF30 to 0x0019FF2C. That saved address is the route back.
- Execution jumps to calculate_damage at 0x00401100. The function pushes ebx and later pops it, so the return address is on top again when ret runs.
- ret takes the saved address from the stack and puts it in eip, then adds four back to esp. An unmatched push would leave some other value where ret expects the address.
- Execution continues at 0x00401025, immediately after the call.
The function’s entry address tells call where to go; the saved return address tells ret where to come back.
A nested call returns to its immediate caller
Section titled “A nested call returns to its immediate caller”An ordinary near ret does not search for “the main program.” It takes the
return address currently on top of the stack. If main calls add_bonus,
and add_bonus calls add_one, the first return goes to add_bonus; its
return goes to main. The last-in, first-out order keeps these two
continuations separate.
The labels below stand for addresses in a simplified 32-bit x86 example.
Each near call saves one four-byte return address; each plain ret removes
one. There are no stack arguments or saved local registers in this model.
Change the starting score and watch the two slots as well as ESP.
Two calls need two return addresses. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
main: mov eax, start
call add_bonus
after_bonus: mov [score], eax
jmp done
add_bonus: call add_one
after_one: ret
add_one: add eax, 1
ret
done: nopThe result is 5. Two calls returned in reverse order, and the stack pointer recovered its starting value.
Final values: start = 4; EIP = done; ESP = 0x1000; EAX = 5; score = 5; call depth = 0.
Read every step of the lesson’s example
Step 1, line 1. Start with EAX = 4 and ESP = 0x1000. Neither return slot is active yet.
start = 4; EIP = main; ESP = 0x1000; EAX = 4; score = not stored yet; call depth = 0.
Step 2, line 2. The first call saves after_bonus at 0x0FFC, subtracts four from ESP, and enters add_bonus.
start = 4; EIP = add_bonus; ESP = 0x0FFC; EAX = 4; score = not stored yet; call depth = 1.
Memory: 0x0FF8 = not written yet (outside active stack); 0x0FFC = after_bonus (top of active stack).
Step 3, line 5. The nested call saves after_one at 0x0FF8. It does not overwrite the outer return address.
start = 4; EIP = add_one; ESP = 0x0FF8; EAX = 4; score = not stored yet; call depth = 2.
Memory: 0x0FF8 = after_one (top of active stack); 0x0FFC = after_bonus (next return).
Step 4, line 7. add_one adds one: EAX becomes 5. Both saved return addresses remain in their slots.
start = 4; EIP = add_one; ESP = 0x0FF8; EAX = 5; score = not stored yet; call depth = 2.
Memory: 0x0FF8 = after_one (top of active stack); 0x0FFC = after_bonus (next return).
Step 5, line 8. The inner ret takes after_one from the top and adds four to ESP. Popping releases the slot; it does not erase its bytes.
start = 4; EIP = after_one; ESP = 0x0FFC; EAX = 5; score = not stored yet; call depth = 1.
Memory: 0x0FF8 = after_one (old bytes) (outside active stack); 0x0FFC = after_bonus (top of active stack).
Step 6, line 6. The outer ret takes after_bonus next. Execution returns to main, and ESP is back at 0x1000.
start = 4; EIP = after_bonus; ESP = 0x1000; EAX = 5; score = not stored yet; call depth = 0.
Memory: 0x0FF8 = after_one (old bytes) (outside active stack); 0x0FFC = after_bonus (outside active stack).
Step 7, line 3. main stores 5 to score. Returning did not undo the calculation.
start = 4; EIP = after_bonus; ESP = 0x1000; EAX = 5; score = 5; call depth = 0.
Memory: 0x0FF8 = after_one (old bytes) (outside active stack); 0x0FFC = after_bonus (outside active stack).
Step 8, line 9. The result is 5. Two calls returned in reverse order, and the stack pointer recovered its starting value.
start = 4; EIP = done; ESP = 0x1000; EAX = 5; score = 5; call depth = 0.
Memory: 0x0FF8 = after_one (old bytes) (outside active stack); 0x0FFC = after_bonus (outside active stack).
The returned-to instruction is the one after the matching call, not the instruction that made the call. Also, popping a slot moves the stack pointer; it does not clear the old bytes. Those bytes alone are no longer evidence that the return address is still part of the active stack.
Call/return cheatsheet: call records where execution should resume; nested calls add newer return slots; ret uses the newest active slot. A calling convention separately specifies arguments, preserved registers, and stack requirements.
Calling conventions are agreements between functions
Section titled “Calling conventions are agreements between functions”A calling convention answers questions such as:
- Where are arguments placed: stack or registers?
- Which registers must the called function preserve?
- Who removes stack arguments?
- Where is the return value placed?
The course’s 32-bit games commonly expose cdecl, stdcall, and thiscall boundaries. The three differ mainly in who removes the arguments from the stack after the call. cdecl leaves that job to the caller, which is also what lets a cdecl function such as printf accept a varying number of arguments—only the caller knows how many it pushed. stdcall makes the callee remove its own arguments before returning. In a typical 32-bit thiscall, ecx carries the object pointer—the value an object method treats as self or this—while other arguments use the stack, and the callee cleans up like stdcall.
Do not identify a calling convention from one register alone. Inspect how the caller prepares arguments and how the callee returns. A wrong convention can leave esp incorrect even when the function address is right.
On 64-bit Windows, ordinary integer and pointer arguments start in rcx, rdx,
r8, and r9; floating-point arguments use the matching XMM argument
registers. The caller also reserves stack space for the register arguments and
keeps the stack correctly aligned. There is one main Windows x64 convention,
so do not carry the old 32-bit cdecl versus stdcall cleanup distinction into
64-bit code.
A calling convention is more than an argument list. It is the complete border contract between caller and callee:
arguments + return location + preserved registers + stack shape + unwind rulesRead both sides of a call. Code before call explains what the caller offers;
the callee’s first uses explain what it expects; code after the return shows what
the caller believes survived.
Address calculations
Section titled “Address calculations”This pattern appears constantly:
mov eax, [rcx+30]Read it as: “Start at the address in rcx, move forward 0x30 bytes, then read a 32-bit value.” If rcx points to a player object, offset 0x30 might be a field such as health.
A typed struct expresses the idea by name:
#[repr(C)]struct PlayerSnapshot { id: u32, team: u32, health: u32,}#[repr(C)] asks the compiler for a predictable C-compatible field layout. We only rely on a layout after confirming its offsets; guessing a struct from one screenshot is not enough.
lea (“load effective address”) is the one common instruction where brackets
do not mean a memory access:
mov eax, [rcx+rdx*4] ; eax = the four-byte value stored at rcx + rdx*4lea rax, [rcx+rdx*4] ; rax = the address rcx + rdx*4 itself; nothing is readlea rax, [rcx+rdx*4] stores the address
rcx + rdx*4 itself and reads nothing; mov eax, [rcx+rdx*4] reads the
four-byte value stored there into the four-byte eax (the eight-byte rax
would take the next element too). A *4 scale usually steps through an array of
four-byte elements, so with rdx = 2 the address is rcx+8. Compilers also
use lea for plain arithmetic, so not every result is a pointer.
Lesson 5.4 traces lea beside mov with addresses you
can change.
rdx = 2. lea rax, [rcx+rdx*4] puts the address rcx+8 in rax. mov eax, [rcx+rdx*4] reads the number stored there, 30.A five-question reading method
Section titled “A five-question reading method”For each instruction, ask:
- What is read?
- What is written?
- How many bytes are involved?
- Is a number signed or unsigned?
- Where can execution go next?
Answer those five questions one instruction at a time. A long function becomes a trail of small facts.
Trace both data flow and control flow
Section titled “Trace both data flow and control flow”Reading only downward through the listing works for a tiny straight-line function. Larger functions need two connected maps.
Data flow asks where a value came from and where it goes next. If eax is
compared, trace backward to the instruction that last wrote eax. If [ecx+0x30]
is changed, trace how ecx obtained the suspected player address and who later
reads that field.
Control flow asks which instruction can run next. A straight sequence with one entry and one exit is often called a basic block. Conditional jumps connect blocks into a graph:
The graph explains the decision even when the compiler has rearranged the blocks.
Source-level if, match, loops, early returns, and error paths all become control-flow
edges of this kind.
Use both maps together. A branch tells you when a write occurs; data flow tells you what it changes. Guessing from only one of them is how a nearby animation counter gets mislabeled as health or a failure path gets mistaken for the normal path.
Trace a complete tiny function
Section titled “Trace a complete tiny function”Suppose ecx points to a player and gold is the four-byte field at offset 4:
mov eax, [esp+4] ; read the price argumentcmp dword ptr [ecx+4], eaxjb cannot_afford ; unsigned gold < pricesub dword ptr [ecx+4], eaxmov eax, 1 ; return trueret 4 ; return and remove one 4-byte stack argument
cannot_afford:xor eax, eax ; eax = 0, meaning falseret 4The same listing as a control-flow graph. Each box is a basic block, and the
only decision is the jb:
Three details in that listing puzzle almost everyone the first time.
Why [esp+4] and not [esp]? The call that reached this function pushed
a return address onto the stack, so that address is now sitting at [esp]. The
argument the caller pushed just before the call is one slot further in, at
[esp+4]. Whenever you see a function collecting its arguments from +4, +8,
+12, the saved return address is what those offsets are stepping over.
[esp+4][esp]esp points at the return address, so the argument the caller pushed just before the call is one slot further in.Why xor eax, eax instead of mov eax, 0? Both leave zero in eax. Any
value exclusive-ORed with itself is zero, and the xor form assembles to fewer
bytes, so compilers emit it constantly. Read xor register, register as “set
this register to zero” and keep going.
Why does ret have a number after it? ret 4 returns and discards four
bytes of arguments from the stack, so the function cleans up after itself. A
plain ret leaves that job to the caller. Which form a function uses is fixed
by its calling convention, and getting it wrong corrupts esp: execution
returns to the correct place, and from that point on the caller’s stack is off
by four bytes.
Trace it with gold = 100 and price = 25:
| Step | Important state |
|---|---|
| enter | ecx = player, [ecx+4] = 100, [esp+4] = 25 |
mov |
eax = 25 |
cmp |
flags describe 100 - 25; unsigned-below is false |
jb |
branch is not taken |
sub |
[ecx+4] = 75 |
mov |
eax = 1 return value |
ret 4 |
caller resumes and the argument is removed |
Now trace gold = 20. The jb branch skips the subtraction and returns zero. That is assembly comprehension: follow state changes and rebuild the higher-level decision.
What the debugger is showing
Section titled “What the debugger is showing”Assembly text is a readable rendering of machine bytes for one CPU
architecture; x86 and ARM bytes need different decoders. In the x86 trace,
eip locates the next instruction, esp marks the stack top, and eflags
holds one-bit results that branches can use. Brackets such as [edx+4] mean
read or write memory at the calculated address; edx+4 without brackets is
only an address calculation. A call saves a return address on the stack,
and ret uses it to resume the caller. The calling convention completes that
contract by fixing where arguments and results go and who removes arguments
from the stack. Breakpoints in the next lesson let you inspect those facts
while a real instruction is paused.
Read the instruction
In mov eax, [rcx+8], what do the brackets mean?
Brackets indicate a memory operand. The CPU calculates the address, then loads the bytes found there.
Finished Lesson 2.3?
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.