Skip to content
Lesson 2.6Following Instructions and Addresses

Tracing an Action Through Its Callers

Follow callers and dependencies from one gold-changing instruction to the menu decision that caused it.

Lesson 2.6
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.6Key ideas, reference and questions

A one-page summary of “Tracing an Action Through Its Callers” you can keep beside your notes. Plain Markdown.

Lesson 2.6 — Tracing an Action Through Its Callers: cheatsheet

  • Remember: The location of a pointer is not the location of its object. Ending a raw-pointer stack slot does not release its heap allocation. An owning local can release that allocation when it is dropped during return.
  • Also useful: Bubbling follows confirmed callers toward the action. Keep the player, arguments, checks, and result together so one shared helper is not mistaken for one feature.

Key terms

  • establishing context: Here, establishing context means connecting a stored change to the specific player, requested action, operands, and checks that caused it.
  • esi: esi is the one that matters most here: find where it was set and you have the object the field belongs to, which is the whole of Lesson 2.8.
  • cross-reference: Each one is a cross-reference: a place in the code that refers to this address.

Formulas and patterns

  • if candidate_action == 3: # Recruit in all three trials; check unknown_flag # the branch exists; its purpose is unknown; call create_unit_candidate # a unit appeared every time this ran; subtract gold

Review

  • Q: How does the call stack help trace a low-level gold write back toward a menu action? A: Return addresses show the chain of callers that led here
  • Q: When tracing how a game feature works, what is an anchor? A: A verified starting point that you trace outward from
Download cheatsheet (.md)

Finding one instruction is useful. Understanding the surrounding feature is better.

In an offline Wesnoth 1.14.9 match, recruiting a unit reduces the player’s four-byte gold value by that unit’s price. The instruction that writes the reduced gold is our anchor. Set a code breakpoint on the instruction you verified in the debugger, recruit one unit, and let the breakpoint fire.

A breakpoint on the gold subtraction

The breakpoint paused on the gold subtraction during a recruit.

The current function probably handles a small part of recruitment. To discover who asked for that work, look at the call stack or run until the function returns.

Running until the current function returns

Run until return: execution continues until the current function finishes.

Now you are back in the caller. Repeat this carefully to move from the low-level write toward the broader decision that requested it.

When x86 executes call, it places a return address on the stack before jumping to the callee. That address points to the instruction the caller expects to run next. A debugger’s call stack combines return addresses with known module ranges and function information to show the nested route that reached the breakpoint.

The call stack a debugger shows is a reconstruction, and it may be incomplete. Optimizations can inline functions, reuse stack space, or omit traditional frame pointers. Confirm an important caller by returning to it or placing a breakpoint at its call site and reproducing the action.

Distinguish the frame from the object it points to

Section titled “Distinguish the frame from the object it points to”

The stack is one place a running function keeps its bookkeeping and temporary values. It is not the home of every object the function uses:

Storage role Typical contents Lifetime to check
Thread stack Active call frames, saved return addresses, some local values The current frame ends when the function returns; the thread has its own stack.
Heap allocation Dynamically allocated objects or buffers The allocation lasts until its owner releases it, which may be later than this call.
Static or module data Globals, constants, and other long-lived data Data remains for the program or module lifetime.

These are roles in virtual memory, not three physical kinds of RAM. Windows describes thread stacks and heaps; Microsoft’s C++ storage-duration guide explains why a local static value can survive repeated calls. A compiler can also keep a local in a register or eliminate it entirely.

Three places, three lifetimesA pointer inside a stack frame can refer to a heap block with a different lifetime. Step through one call to watch each place start and end on its own. Positions are schematic.
  1. 1 The program is running

    Module data: lasts as long as the program

    MAX_UNITS
    6live

    Thread stack: lasts as long as each call

    Caller’s frame
    unit = not setlive
    recruit() frame
    —not created yet

    Heap: lasts until its owner frees it

    Unit block
    —not created yet

    Static data exists. The caller’s frame is on the stack. The heap holds no Unit yet, and its variable unit is not set.

  2. 2 Call recruit()

    Module data: lasts as long as the program

    MAX_UNITS
    6live

    Thread stack: lasts as long as each call

    Caller’s frame
    unit = not setlive
    recruit() frame
    count = 3, p = not setlive

    Heap: lasts until its owner frees it

    Unit block
    —not created yet

    The call creates a new frame on the stack, with room for count and the pointer p. p holds no address yet.

  3. 3 Allocate a Unit on the heap

    Module data: lasts as long as the program

    MAX_UNITS
    6live

    Thread stack: lasts as long as each call

    Caller’s frame
    unit = not setlive
    recruit() frame
    count = 3, p = 0x00A41F00live

    Heap: lasts until its owner frees it

    Unit block
    at 0x00A41F00: Unit { hp: 10 }live

    The allocator hands back a block at 0x00A41F00. That number is stored in p, so p, a slot inside the stack frame, now refers to an object that is not inside the frame.

  4. 4 recruit() returns the pointer

    Module data: lasts as long as the program

    MAX_UNITS
    6live

    Thread stack: lasts as long as each call

    Caller’s frame
    unit = 0x00A41F00live
    recruit() frame
    released; old bytes may lingerreleased

    Heap: lasts until its owner frees it

    Unit block
    at 0x00A41F00: Unit { hp: 10 }live

    Returning ends the frame, so count and p are released. The heap block is not part of the frame, so it stays. The caller received the number 0x00A41F00 and keeps it in unit.

  5. 5 The owner frees the block

    Module data: lasts as long as the program

    MAX_UNITS
    6live

    Thread stack: lasts as long as each call

    Caller’s frame
    unit = 0x00A41F00 (dangling)live
    recruit() frame
    released; old bytes may lingerreleased

    Heap: lasts until its owner frees it

    Unit block
    freedreleased

    Freeing ends the block’s life. The caller’s variable still holds 0x00A41F00, but that number now names memory the program no longer owns: a dangling pointer. Static data is untouched throughout.

A pointer inside a stack frame can refer to a heap block with a different lifetime. Step through one call to watch each place start and end on its own. Positions are schematic.

Here is an illustrative near call with no arguments, not an address from Wesnoth. A five-byte call at 0x00401020 must resume at 0x00401025. Start with esp = 0x0012FF90 and caller ebp = 0x0012FFC0:

Step Stack pointer What is saved or restored
call 0x0012FF8C Four-byte return address 0x00401025.
push ebp; mov ebp, esp 0x0012FF88 Old EBP is saved; new EBP marks this frame.
sub esp, 0x10 0x0012FF78 Sixteen bytes are available for locals below EBP.
mov esp, ebp; pop ebp 0x0012FF8C Local space is released and caller EBP is restored.
Plain ret 0x0012FF90 Saved address becomes EIP, so execution resumes after the call.

A conventional EBP frame makes the saved return address visible at EBP + 4. The values below are illustrative and the call has no arguments.

Follow one call through the stackA near call with no arguments at 0x00401020. Each step shows the three registers that move and the four-byte stack words, higher addresses at the top.

Frame at its fullest, while the function body runs.

The frame at its fullest, while the function body runsThe body uses the locals at [ebp-0x10] to [ebp-1]. ESP and EBP stay put. EIP moves on 10 bytes, from 0x00401106 to 0x00401110, where leave is.

ESP0012FF78changed
EBP0012FF88changed
EIP00401110changed
  1. 0012FF90caller datacaller’s last pushed word (ESP pointed here before the call) live
  2. 0012FF8C00401025return address live
  3. EBP0012FF880012FFC0saved EBP (the caller’s) live
  4. 0012FF84not initialisedlocal, [ebp-4] live
  5. 0012FF80not initialisedlocal, [ebp-8] live
  6. 0012FF7Cnot initialisedlocal, [ebp-0xC] live
  7. ESP0012FF78not initialisedlocal, [ebp-0x10] live
  8. 0012FF74—below the frame: not in use empty

The instructions

  1. 00401020E8 DB 00 00 00call 0x00401100
  2. 00401025(the next caller instruction)
  3. 0040110055push ebp
  4. 004011018B ECmov ebp, esp
  5. 0040110383 EC 10sub esp, 0x10
  6. 00401106(function body, 10 bytes, not shown)
  7. 00401110C9leave
  8. 00401111C3ret

EIP always holds the address of the next instruction to run, so the highlighted line is the one that runs next. The addresses are illustrative.

All 8 steps as text
  1. Step 1 before the call

    ESP is 0x0012FF90, the caller’s last pushed word. EBP is 0x0012FFC0, the caller’s frame base. EIP is 0x00401020, the call instruction.

  2. Step 2 call 0x00401100

    The return address is the next instruction: 0x00401020 + 5 = 0x00401025. call pushes it: ESP = 0x0012FF90 − 4 = 0x0012FF8C, and 0x00401025 is stored there. EIP becomes the call target 0x00401100 (displacement 0xDB from the end of the call).

  3. Step 3 push ebp

    ESP = 0x0012FF8C − 4 = 0x0012FF88. The caller’s EBP, 0x0012FFC0, is stored there so it can be restored later.

  4. Step 4 mov ebp, esp

    EBP = ESP = 0x0012FF88. From now on [ebp+4] = 0x0012FF8C is the return address and locals live below EBP.

  5. Step 5 sub esp, 0x10

    ESP = 0x0012FF88 − 0x10 = 0x0012FF78. The sixteen bytes from 0x0012FF78 to 0x0012FF87 are reserved for locals; nothing has initialised them.

  6. Step 6 function body runs

    The body uses the locals at [ebp-0x10] to [ebp-1]. ESP and EBP stay put. EIP moves on 10 bytes, from 0x00401106 to 0x00401110, where leave is.

  7. Step 7 leave

    leave is mov esp, ebp (ESP = 0x0012FF88) followed by pop ebp: EBP = the word at 0x0012FF88 = 0x0012FFC0, then ESP = 0x0012FF88 + 4 = 0x0012FF8C. The locals are released; their bytes are not erased.

  8. Step 8 ret

    ret pops the return address: EIP = the word at 0x0012FF8C = 0x00401025, then ESP = 0x0012FF8C + 4 = 0x0012FF90. Execution resumes right after the call.

Releasing stack space does not erase it: after leave and ret the old bytes are still in memory, but they are no longer part of any live frame. A compiler can also omit the EBP frame; this is the conventional layout.

What can a write past the end of a local buffer reach?

Suppose the function copies bytes into a 8-byte local buffer and never checks the length. A copy goes from lower to higher addresses, so past the end of the buffer lie, in order, the rest of the frame: saved EBP, the return address, then the caller’s own data. Choose how many bytes the copy writes.

  1. 0012FF94untouchedcaller’s data
  2. 0012FF90untouchedcaller’s data
  3. 0012FF8C4 of 4 bytes writtenreturn address
  4. 0012FF884 of 4 bytes writtensaved EBP
  5. 0012FF844 of 4 bytes writtenbuffer, bytes 5 to 8
  6. 0012FF804 of 4 bytes writtenbuffer, bytes 1 to 4
  7. 0012FF7Cuntouchedanother local
  8. 0012FF78untouchedanother local

8 bytes went past the buffer: the first 4 damaged the saved EBP and 4 bytes reached the return address. When the function runs ret, the CPU loads EIP from those bytes, so the function returns wherever the written data says. This is the classic stack buffer overflow bug.

The same write with and without a cookie, for 8, 12, 16 and 20 bytes
The same write, with and without a cookie. Byte n lands n − 1 bytes above the start of the buffer.
Bytes writtenWords damaged, no cookieWords damaged, with a cookie
8only the bufferonly the buffer
12saved EBPstack cookie; the program stops before ret
16saved EBP, return addressstack cookie, saved EBP; the program stops before ret
20saved EBP, return address, caller’s datastack cookie, saved EBP, return address; the program stops before ret
Why bounds checks, cookies, DEP and ASLR exist
Bounds check
Compare the number of bytes to copy with the buffer size before copying, or use calls that take the size. With the check, the write never leaves the buffer and nothing else is needed.
Stack cookie
The compiler places a secret value between the locals and the saved EBP and checks it before the function returns. A copy that runs upward through the frame must cross the cookie, so the check fails and the program stops before ret runs. (Visual C++ calls this /GS.)
DEP / NX
Data Execution Prevention marks stack and heap memory as not executable, so bytes that were only ever data cannot be run as code even if the CPU is pointed at them.
ASLR
Address space layout randomisation places stacks, heaps and modules at different addresses on each run or boot, so a real program’s return address is not a number anyone can know in advance. The values on this page are fixed only so you can follow the arithmetic.
Memory-safe code
Rust’s checked indexing and slice copies panic on a length that does not fit instead of writing past the end. The protection is gone inside unsafe code and in C-style APIs.

These are defences, not a guarantee: they make the bug harder to use, and the fix is still to remove it.

Optional exploration

Follow a cdecl call and its cleanup

A bounded 32-bit near call with one four-byte integer argument. The helper adds seven, returns its result in EAX, and the cdecl caller releases the argument. This is an illustrative calling convention, not executed code.

12 / 12: add esp, 4 ; caller cleanup

The cdecl caller releases its four-byte argument. ESP is back at 0x0012FF90; EAX remains 28.

argument21
ESP0x0012FF90
EBP0x0012FFC0
EAX result28
resume address0x00401025
0x0012FF8Ccaller argument21
0x0012FF88saved return address0x00401025
0x0012FF84saved EBP0x0012FFC0
0x0012FF80reserved local worduninitialized
0x0012FF7Creserved local worduninitialized
0x0012FF78reserved local worduninitialized
0x0012FF74reserved local worduninitialized

Outlined slots are live. Released slots show possible leftover bytes, not live objects. Reserved local values are uninitialized.

ESP
0x0012FF90
EBP
0x0012FFC0
EAX
28 (0x0000001C)
EIP after return
0x00401025
Complete default worked example and listing

This reference keeps the default values. The live picture above follows your input and selected step.

ActionWorked state
before the caller pushes its argumentESP 0x0012FF90; EBP 0x0012FFC0; EAX not set
push argumentESP 0x0012FF8C; EBP 0x0012FFC0; EAX not set
call helper ; five-byte call at 0x00401020ESP 0x0012FF88; EBP 0x0012FFC0; EAX not set
push ebpESP 0x0012FF84; EBP 0x0012FFC0; EAX not set
mov ebp, espESP 0x0012FF84; EBP 0x0012FF84; EAX not set
sub esp, 0x10ESP 0x0012FF74; EBP 0x0012FF84; EAX not set
mov eax, [ebp+8]ESP 0x0012FF74; EBP 0x0012FF84; EAX 21
add eax, 7ESP 0x0012FF74; EBP 0x0012FF84; EAX 28
mov esp, ebpESP 0x0012FF84; EBP 0x0012FF84; EAX 28
pop ebpESP 0x0012FF88; EBP 0x0012FFC0; EAX 28
retESP 0x0012FF8C; EBP 0x0012FFC0; EAX 28
add esp, 4 ; caller cleanupESP 0x0012FF90; EBP 0x0012FFC0; EAX 28
caller: push argument
        call helper  ; at 0x00401020, next EIP 0x00401025
        add esp, 4   ; cdecl caller releases argument
helper: push ebp
        mov ebp, esp
        sub esp, 0x10
        mov eax, [ebp+8]
        add eax, 7
        mov esp, ebp
        pop ebp
        ret

A plain RET pops the return address, leaving the argument for caller cleanup. Released stack bytes can remain readable without being live objects. Compilers can omit this conventional EBP frame.

Intel’s CALL and RET descriptions define the instruction behavior. The EBP prologue is a conventional frame layout, not something call creates by itself. Releasing or popping stack space need not erase its old bytes, so readable leftovers do not prove a local object is still alive. The compiler caveats above explain why a real optimized call may have a different layout.

Establish context by following the observed call chain

Section titled “Establish context by following the observed call chain”

Here, establishing context means connecting a stored change to the specific player, requested action, operands, and checks that caused it. At the gold write, knowing “subtract 25” is incomplete: is that an item price, a fine, or a different player’s payment?

The original course called the move from a small helper toward its callers bubbling. Treat that name as a method, not a promise that every caller has a recoverable source name. Record each confirmed level:

Context cheatsheet: keep these levels together when following a write toward its callers.

Level Question it can answer Evidence to keep
Gold write Which field changed, and by how much? Receiver address, amount, before/after values
Spending helper Which check allowed the subtraction? Taken comparison and its operands
Purchase handler What did the amount mean? Item price passed to the helper
Action dispatcher What requested this handler? Selected command and matching call site

Reproduce one action, confirm its write, inspect the caller, and repeat until you can explain that action. Compare a second action that shares the helper: it can reach the same write for a different reason. A breakpoint above the shared helper needs that caller context to distinguish them.

The same gold write can have different callers. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

dispatch(action, player, amount):
  if action == Buy:
    buy_item(player, amount)
  else: pay_fine(player, amount)
buy_item(player, price):
  spend_gold(player, price)
pay_fine(player, fine):
  spend_gold(player, fine)
spend_gold(player, amount):
  if player.gold >= amount:
    player.gold = player.gold - amount
  return

Context: Ada requested a purchase for 25; the check accepted it, and gold ended at 75. Inspection changed no fields.

Final values: phase = inspect recorded callers; action = Buy; player = Ada; amount = 25; gold = 75; focus = dispatch; accepted = true.

Read every step of the lesson’s example
  1. Step 1, line 1. Ada starts with 100 gold. The selected action is a purchase, and its amount is 25.

    phase = execute; action = Buy; player = Ada; amount = 25; gold = 100; focus = dispatch.

  2. Step 2, line 3. dispatch selects buy_item and passes this player and amount.

    phase = execute; action = Buy; player = Ada; amount = 25; gold = 100; focus = buy_item.

  3. Step 3, line 6. buy_item calls the shared spend_gold helper. The caller gives the amount its meaning.

    phase = execute; action = Buy; player = Ada; amount = 25; gold = 100; focus = spend_gold.

  4. Step 4, line 10. 100 >= 25 is true. The comparison controls whether the write can run.

    phase = execute; action = Buy; player = Ada; amount = 25; gold = 100; focus = spend_gold; accepted = true.

  5. Step 5, line 11. The anchor writes 100 - 25 = 75 to Ada's gold.

    phase = execute; action = Buy; player = Ada; amount = 25; gold = 75; focus = spend_gold; accepted = true.

  6. Step 6, line 9. Now inspect the recorded path. spend_gold identifies the affected player and subtraction, but the helper name alone does not identify the action.

    phase = inspect recorded callers; action = Buy; player = Ada; amount = 25; gold = 75; focus = spend_gold; accepted = true.

  7. Step 7, line 6. One level up, buy_item explains why 25 was supplied: an item price.

    phase = inspect recorded callers; action = Buy; player = Ada; amount = 25; gold = 75; focus = buy_item; accepted = true.

  8. Step 8, line 1. Context: Ada requested a purchase for 25; the check accepted it, and gold ended at 75. Inspection changed no fields.

    phase = inspect recorded callers; action = Buy; player = Ada; amount = 25; gold = 75; focus = dispatch; accepted = true.

The final steps inspect a recorded path; they do not rewind or repeat a purchase. In a live debugger, Run until return executes the rest of the current function. Step over executes a call and pauses after it returns; Step into follows its executed instructions. None undoes previous writes. Note the current values again after running, and distinguish an inferred call stack from a call site you reproduced. Inlining and tail calls can remove an ordinary call/return boundary.

Menus often use a chain of comparisons:

cmp eax, MENU_RECRUIT
je handle_recruit
cmp eax, MENU_DESCRIPTION
je show_description
cmp eax, MENU_CANCEL
je close_menu

The constants will not have friendly names in the debugger. You create those names from experiments.

Triggering one menu item at a time shows which branch each one takes. In one run of this book’s build:

Action: Recruit
Compared value: 3
Taken destination: 0x00CC_A140
Action: Terrain description
Compared value: 5
Taken destination: 0x00CC_AF20

Those destinations belong to one run of one build, and yours will not match. What carries over is the shape: two actions, two constants, two different branch targets, all from the same comparison chain.

A comparison chain choosing a menu action. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

cmp eax, 3  ; MENU_RECRUIT?
je  handle_recruit
cmp eax, 5  ; MENU_DESCRIPTION?
je  show_description
cmp eax, MENU_CANCEL  ; ...and so on down the chain
je  close_menu

This one run gave you a pairing: the number 3 leads to the Recruit handler. Naming it is your model of the code, not something the program says.

Final values: eax = 3; ZF = 1; next = 0x00CCA140 (handle_recruit); your_name = MenuAction::Recruit = 3.

Read every step of the lesson’s example
  1. Step 1, line 1. The menu code has put the chosen action's number, 3, in eax. The comparison chain starts.

    eax = 3.

  2. Step 2, line 1. cmp eax, 3 subtracts without storing and records the result in the flags. 3 - 3 is 0, so the zero flag becomes 1.

    eax = 3; ZF = 1.

  3. Step 3, line 2. je means jump if equal, which reads ZF = 1. It is 1, so the jump is taken and the next instruction run is at the Recruit handler.

    eax = 3; ZF = 1; next = 0x00CCA140 (handle_recruit).

  4. Step 4, line 2. This one run gave you a pairing: the number 3 leads to the Recruit handler. Naming it is your model of the code, not something the program says.

    eax = 3; ZF = 1; next = 0x00CCA140 (handle_recruit); your_name = MenuAction::Recruit = 3.

Once each constant is matched to an action, the anonymous numbers become an enum:

#[repr(u32)]
enum MenuAction {
Recruit = 3,
TerrainDescription = 5,
}

The enum is your model, not a claim about the original source type.

The comparison value alone is not enough. Where it came from matters: a register, a stack argument, or a field inside a menu object, and whether another branch changed it before the dispatcher. Those details distinguish “menu command ID” from a temporary loop counter that merely happened to equal 3.

The gold instruction tells you where a change happens. The menu dispatcher tells you why execution reached it.

That distinction matters:

  • patching the subtraction changes the stored cost result;
  • changing a branch may select a different feature;
  • hooking the dispatcher lets you observe every menu action.

Do not make all three changes at once. Test one layer at a time.

This is also a lesson about abstraction levels. The subtraction is a low-level side effect. The recruitment function groups validation and side effects into one operation. The dispatcher chooses among operations. Moving upward explains more behavior but also adds more unrelated branches, so stop when the original question is answered.

Follow dependencies, not just nearby instructions

Section titled “Follow dependencies, not just nearby instructions”

“Backward” does not simply mean scrolling upward in the disassembly. It means following the facts that had to be true for the anchor instruction to run.

  • A data dependency asks where an operand came from. If eax is the price, find the instruction or argument that produced eax.
  • A control dependency asks which comparison allowed this block to run. If a branch skipped the subtraction, determine what value controlled that branch.
  • A call dependency asks which caller requested this operation and what arguments it supplied.

Compilers can move calculations away from the source statement that inspired them. The instruction immediately above the anchor may be unrelated, while the important value was loaded twenty instructions earlier or passed by the caller. Track one value and one decision at a time.

A practical stopping condition is: stop tracing callers when you can name the input, the decision, and the observable result in plain English. Going farther may reveal the whole menu framework, but it no longer answers the focused gold question.

Put the anchor from Lesson 2.5 back on screen and ask all three of them about it. Writing sub dword ptr [esi+0x30], eax in the middle, each question pulls in a different direction:

call dependency who asked for this work?
| (the caller, and what it passed)
v
data --> sub [esi+0x30], eax <-- control
where did which comparison let
esi and eax this line run at all?
come from?

They are genuinely different searches, and the answers usually live in different places. eax may have been loaded two instructions above, or handed in by the caller and untouched since. esi is the one that matters most here: find where it was set and you have the object the field belongs to, which is the whole of Lesson 2.8. The comparison that guarded the block may sit immediately above, or in the caller, having already decided this purchase was affordable before the call was made.

Answering them one at a time matters. The common way to get lost is to chase eax upward, notice an interesting branch on the way, follow that instead, and end up unable to say which question you were answering.

Code branching between menu operations

A branch in the code that chooses between menu operations.

The call stack lists the one route that reached your breakpoint this time. The code itself can list every route. A direct call is the byte E8 followed by a four-byte distance, stored lowest byte first, which the CPU adds to the address of the next instruction. Work out that target for every call in the module, keep the ones that land on the first byte of the function you are in, and you have its callers. Each one is a cross-reference: a place in the code that refers to this address. In x64dbg, right-click the function’s first instruction and choose Find references to, then Selected Address(es).

Take an illustrative helper that starts at 0x00402000. A call at 0x00401020 stores DB 0F 00 00, which read lowest byte first is 0x00000FDB. The next instruction starts at 0x00401020 + 5 = 0x00401025, and 0x00401025 + 0xFDB = 0x00402000, so this call is a caller. A distance whose top bit is set counts as a negative number, which lets a call reach backward; the calculator’s second example works one out.

The two lists answer different questions. A cross-reference shows that an instruction can call the helper, and the call stack shows which caller did. An indirect call such as call eax takes its target from a register while the game runs, so it never appears in the cross-reference list.

Predict first

During one recruit the call stack showed a single caller, at 0x00401020. The cross-reference list also has a call at 0x00403100 whose target is 0x00402000.

What does the second call site tell you?

Answer: Some other action can reach the same helper, and a breakpoint on 0x00403100 shows which. Both calls land on 0x00402000, so the helper serves at least two callers. Breaking on the second call site and trying other actions that spend gold names the one that uses it.

Where does this call land?A direct call stores a signed distance, lowest byte first. The CPU adds it to the address of the next instruction. A call that lands on the helper’s first byte is one of its callers.

From the call bytes to its target

Five bytes at 0x00401020

The call instruction
0x004010200x00401024
E8call
DBlowest
0F
00
00highest

Work out the target

The four bytes, read lowest byte first
0x00000FDB = +4059top bit clear, so positive
Next instruction = call address + 5
0x00401020 + 5 = 0x00401025
Target = next instruction + distance
0x004020000x00401025 + 0xFDB

Compared with the helper

Verdict
Lands exactly on the helper at 0x00402000: a caller

0x00401025 + 0xFDB = 0x00402000, the helper’s first byte, so this call is one of its callers.

Use x64dbg’s restore command after every temporary patch.

Restoring the original selection

Restoring the original selection after a temporary patch.

Then verify:

  1. original bytes are back;
  2. the normal menu behavior works;
  3. no stale breakpoint remains.

The recovered behavior fits in a few lines of pseudocode:

when the player chooses a menu item:
if it is Recruit:
check the tile
check the price
create the unit
subtract gold
otherwise:
run the matching menu action

Pseudocode separates the idea from register names and addresses, so the same sketch describes the function in any build even though every address changes. Parts whose purpose is not known yet can keep neutral names instead of invented ones:

if candidate_action == 3: # Recruit in all three trials
check unknown_flag # the branch exists; its purpose is unknown
call create_unit_candidate # a unit appeared every time this ran
subtract gold

The gold write supplied a known starting instruction. Its return address and call stack led to the caller; recruiting and opening Terrain Description supplied contrasting cases for the menu branch. The small pseudocode model names only behavior those cases supported. That route from an observed write to a caller, branch, and named action also works when the compiler has removed the original source names.

That explanation identifies the action and its checks, but the address of its instruction still belongs to one run. The next question is how to find the same game state after a restart: why addresses move, and which relationships between objects can survive the move. Once those are clear, we can return to changing control flow itself.

Knowledge check

Climb toward the cause

How does the call stack help trace a low-level gold write back toward a menu action?

Finished Lesson 2.6?

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.