Tracing an Action Through Its Callers
Follow callers and dependencies from one gold-changing instruction to the menu decision that caused it.
Cheatsheet for Lesson 2.6
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:esiis 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
Finding one instruction is useful. Understanding the surrounding feature is better.
Start from a known anchor
Section titled “Start from a known anchor”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.

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.

Now you are back in the caller. Repeat this carefully to move from the low-level write toward the broader decision that requested it.
Why the caller is visible
Section titled “Why the caller is visible”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.
1 The program is running
Module data: lasts as long as the program
MAX_UNITS6liveThread stack: lasts as long as each call
Caller’s frameunit = not setliverecruit() frame—not created yetHeap: lasts until its owner frees it
Unit block—not created yetStatic data exists. The caller’s frame is on the stack. The heap holds no Unit yet, and its variable unit is not set.
2 Call recruit()
Module data: lasts as long as the program
MAX_UNITS6liveThread stack: lasts as long as each call
Caller’s frameunit = not setliverecruit() framecount = 3, p = not setliveHeap: lasts until its owner frees it
Unit block—not created yetThe call creates a new frame on the stack, with room for count and the pointer p. p holds no address yet.
3 Allocate a Unit on the heap
Module data: lasts as long as the program
MAX_UNITS6liveThread stack: lasts as long as each call
Caller’s frameunit = not setliverecruit() framecount = 3, p = 0x00A41F00liveHeap: lasts until its owner frees it
p points here
Unit blockat 0x00A41F00: Unit { hp: 10 }liveThe 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 recruit() returns the pointer
Module data: lasts as long as the program
MAX_UNITS6liveThread stack: lasts as long as each call
Caller’s frameunit = 0x00A41F00liverecruit() framereleased; old bytes may lingerreleasedHeap: lasts until its owner frees it
unit points here
Unit blockat 0x00A41F00: Unit { hp: 10 }liveReturning 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 The owner frees the block
Module data: lasts as long as the program
MAX_UNITS6liveThread stack: lasts as long as each call
Caller’s frameunit = 0x00A41F00 (dangling)liverecruit() framereleased; old bytes may lingerreleasedHeap: lasts until its owner frees it
Unit blockfreedreleasedFreeing 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.
Read one concrete 32-bit call frame
Section titled “Read one concrete 32-bit call frame”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.
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.
0012FF78changed0012FF88changed00401110changed0012FF90caller datacaller’s last pushed word (ESP pointed here before the call) live0012FF8C00401025return address live- EBP
0012FF880012FFC0saved EBP (the caller’s) live 0012FF84not initialisedlocal, [ebp-4] live0012FF80not initialisedlocal, [ebp-8] live0012FF7Cnot initialisedlocal, [ebp-0xC] live- ESP
0012FF78not initialisedlocal, [ebp-0x10] live 0012FF74—below the frame: not in use empty
The instructions
00401020E8 DB 00 00 00call 0x0040110000401025(the next caller instruction)0040110055push ebp004011018B ECmov ebp, esp0040110383 EC 10sub esp, 0x1000401106(function body, 10 bytes, not shown)00401110C9leave00401111C3ret
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
Step 1
before the callESP is 0x0012FF90, the caller’s last pushed word. EBP is 0x0012FFC0, the caller’s frame base. EIP is 0x00401020, the call instruction.
Step 2
call 0x00401100The 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).
Step 3
push ebpESP = 0x0012FF8C − 4 = 0x0012FF88. The caller’s EBP, 0x0012FFC0, is stored there so it can be restored later.
Step 4
mov ebp, espEBP = ESP = 0x0012FF88. From now on [ebp+4] = 0x0012FF8C is the return address and locals live below EBP.
Step 5
sub esp, 0x10ESP = 0x0012FF88 − 0x10 = 0x0012FF78. The sixteen bytes from 0x0012FF78 to 0x0012FF87 are reserved for locals; nothing has initialised them.
Step 6
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.
Step 7
leaveleave 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.
Step 8
retret 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.
0012FF94untouchedcaller’s data0012FF90untouchedcaller’s data0012FF8C4 of 4 bytes writtenreturn address0012FF884 of 4 bytes writtensaved EBP0012FF844 of 4 bytes writtenbuffer, bytes 5 to 80012FF804 of 4 bytes writtenbuffer, bytes 1 to 40012FF7Cuntouchedanother local0012FF78untouchedanother 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
| Bytes written | Words damaged, no cookie | Words damaged, with a cookie |
|---|---|---|
| 8 | only the buffer | only the buffer |
| 12 | saved EBP | stack cookie; the program stops before ret |
| 16 | saved EBP, return address | stack cookie, saved EBP; the program stops before ret |
| 20 | saved EBP, return address, caller’s data | stack 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
unsafecode 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.
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.
Play starts only when chosen. Step works one action at a time.
12 / 12: add esp, 4 ; caller cleanup
The cdecl caller releases its four-byte argument. ESP is back at 0x0012FF90; EAX remains 28.
0x0012FF8Ccaller argument210x0012FF88saved return address0x004010250x0012FF84saved EBP0x0012FFC00x0012FF80reserved local worduninitialized0x0012FF7Creserved local worduninitialized0x0012FF78reserved local worduninitialized0x0012FF74reserved local worduninitializedOutlined 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.
| Action | Worked state |
|---|---|
before the caller pushes its argument | ESP 0x0012FF90; EBP 0x0012FFC0; EAX not set |
push argument | ESP 0x0012FF8C; EBP 0x0012FFC0; EAX not set |
call helper ; five-byte call at 0x00401020 | ESP 0x0012FF88; EBP 0x0012FFC0; EAX not set |
push ebp | ESP 0x0012FF84; EBP 0x0012FFC0; EAX not set |
mov ebp, esp | ESP 0x0012FF84; EBP 0x0012FF84; EAX not set |
sub esp, 0x10 | ESP 0x0012FF74; EBP 0x0012FF84; EAX not set |
mov eax, [ebp+8] | ESP 0x0012FF74; EBP 0x0012FF84; EAX 21 |
add eax, 7 | ESP 0x0012FF74; EBP 0x0012FF84; EAX 28 |
mov esp, ebp | ESP 0x0012FF84; EBP 0x0012FF84; EAX 28 |
pop ebp | ESP 0x0012FF88; EBP 0x0012FFC0; EAX 28 |
ret | ESP 0x0012FF8C; EBP 0x0012FFC0; EAX 28 |
add esp, 4 ; caller cleanup | ESP 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
retA 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
returnContext: 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
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.
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.
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.
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.
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.
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.
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.
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.
Recognize dispatcher code
Section titled “Recognize dispatcher code”Menus often use a chain of comparisons:
cmp eax, MENU_RECRUITje handle_recruitcmp eax, MENU_DESCRIPTIONje show_descriptioncmp eax, MENU_CANCELje close_menuThe 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: RecruitCompared value: 3Taken destination: 0x00CC_A140
Action: Terrain descriptionCompared value: 5Taken destination: 0x00CC_AF20Those 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.
Read every step of the lesson’s example
Step 1, line 1. The menu code has put the chosen action's number, 3, in eax. The comparison chain starts.
eax = 3.
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.
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).
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.
Separate “where” from “why”
Section titled “Separate “where” from “why””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
eaxis the price, find the instruction or argument that producedeax. - 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.
The three questions, asked once
Section titled “The three questions, asked once”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.

Find every caller without running it
Section titled “Find every caller without running it”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.
From the call bytes to its target
Five bytes at 0x00401020
Work out the target
the distance is measured from here
Next instruction = call address + 5add
Target = next instruction + distanceCompared with the helper
0x00401025 + 0xFDB = 0x00402000, the helper’s first byte, so this call is one of its callers.
Restore before changing direction
Section titled “Restore before changing direction”Use x64dbg’s restore command after every temporary patch.

Then verify:
- original bytes are back;
- the normal menu behavior works;
- no stale breakpoint remains.
A plain-English function sketch
Section titled “A plain-English function sketch”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 actionPseudocode 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 goldFrom a write to a menu action
Section titled “From a write to a menu action”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.
Climb toward the cause
How does the call stack help trace a low-level gold write back toward a menu action?
Stack frames and return addresses describe active function calls. Walking upward moves from the immediate write toward the higher-level decision that requested it.
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.
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.