Skip to content
Lesson 2.8Following Instructions and Addresses

How Pointer Paths Survive Restarts

Trace the player object back to a verified starting point and prove the pointer path across restarts.

Lesson 2.8
Read or listenPrint chapter or book
About 20 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.8Key ideas, reference and questions

A one-page summary of “How Pointer Paths Survive Restarts” you can keep beside your notes. Plain Markdown.

Lesson 2.8 — How Pointer Paths Survive Restarts: cheatsheet

  • Remember: A pointer path is a route through objects. Check every step, not only the final value, and note the conditions it depends on.
  • Also useful: Read the path like treasure-map directions: go to the first spot, read the note there, follow it, then take a few steps along.

Key terms

  • eax: At entry, eax is zero; after stepping over the call, it holds the side-record pointer.

Review

  • Q: What is the strongest evidence that a pointer path is useful? A: The pinned build's starting address and dereference steps resolve the intended object across clean restarts
  • Q: At a breakpoint, an instruction reads add dword ptr [eax+4], edx. Which description of the registers fits? A: eax points at a record, and edx is the amount added to its field at +4
Download cheatsheet (.md)

Use offline, 32-bit Wesnoth 1.14.9. Find the current player’s gold again and set a write breakpoint on its four bytes. Instead of recruiting, end a turn so income changes that value.

Wesnoth’s income display

Wesnoth's income display at the end of a turn.

When the breakpoint fires, inspect the instruction that reached the gold field. In the 32-bit Wesnoth 1.14.9 build used by this book, income reaches:

A breakpoint triggered by income

The breakpoint fires on the instruction that adds income.
009B4D00 add dword ptr [eax+4], edx

Read that line like a sentence: “add the income in edx to the 32-bit number four bytes after the object in eax.” eax points at the current side record, eax + 4 is its gold field, and edx contains the income.

These addresses are exact only for Wesnoth 1.14.9 32-bit. If your instruction differs, use your build’s values instead of forcing these numbers onto it.

Set a breakpoint on the call above the income write. At entry, eax is zero; after stepping over the call, it holds the side-record pointer. Step into that call on the next turn. The useful path begins with:

mov eax, dword ptr [ecx+60]
; later, follow that pointer and add 0xA90
; the gold field is another 4 bytes into the side record

At 0x009AE7F7, read ecx. Restart Wesnoth, recreate the same local match, and break there again. In this book’s build, ecx remains 0x017EECB8. The complete path is:

[[0x017EECB8 + 0x60] + 0xA90] + 0x4

Because 0x017EECB8 + 0x60 is 0x017EED18, later labs use the shorter form:

[[0x017EED18] + 0xA90] + 0x4

Square brackets mean “read the pointer stored here.” A plus sign means “move this many bytes from that address.”

A pointer path is a route through an object graph

Section titled “A pointer path is a route through an object graph”

Each dereference moves from one object to another. The observed root 0x017EED18 is an absolute address for the pinned course build, not an RVA. Confirm that it still points into the expected module in your run. A portable profile would store the root’s RVA and first discover the module’s current base, as Lesson 2.7 explained. The heap objects reached afterward may move between runs, which is why the path reads their current addresses rather than saving yesterday’s final address.

A path is stable only under stated conditions. It may depend on:

  • the exact executable build;
  • a local match being active;
  • the current player or side object existing;
  • the same layout and pointer width;
  • each intermediate page being readable at the moment of use.

Validate every edge, not only the final gold. A non-null pointer can still be stale or point to the wrong object. Check module ranges, page protections, alignment, and a few field invariants before trusting the final value.

In Cheat Engine, add a pointer entry by hand using the module and the offsets you observed.

Adding a pointer path manually

Adding a pointer entry by hand: the module, then each offset.

Be careful about order. A displayed list of offsets may be applied from the module outward or shown in reverse depending on the tool. For this exact build, enter base address 0x017EED18, first offset 0xA90, and final offset 0x4. The resolved value should equal the gold shown in the game.

The completed pointer entry

The finished pointer entry, resolving to the gold value.

One successful run proves very little. Test:

  1. close Wesnoth completely;
  2. start it again;
  3. begin a new local match;
  4. confirm the pointer entry resolves;
  5. repeat at least three times.

Also test a state where no match is active. A good tool should report “player object unavailable,” not crash or read address zero.

The code follows this plain algorithm:

read the 32-bit pointer stored at 0x017EED18
if it is null, stop with a useful error
add 0xA90 and read the next 32-bit pointer
if it is null, stop with a useful error
add the final gold-field offset
read the 32-bit gold value

We can model failures precisely:

#[derive(Debug)]
enum ResolveError {
ModuleNotFound,
NullPointer { step: usize },
AddressOverflow,
ReadFailed { address: usize },
}

Here is the resolver without any Windows API noise. read_u32 can use ReadProcessMemory in an external tool or direct reads inside an injected DLL:

const PLAYER_ROOT: usize = 0x017E_ED18;
const GAME_OFFSET: usize = 0xA90;
const GOLD_OFFSET: usize = 0x4;
fn wesnoth_gold_address(
mut read_u32: impl FnMut(usize) -> Result<u32, ResolveError>,
) -> Result<usize, ResolveError> {
let player = read_u32(PLAYER_ROOT)? as usize;
if player == 0 {
return Err(ResolveError::NullPointer { step: 0 });
}
let game_slot = player.checked_add(GAME_OFFSET)
.ok_or(ResolveError::AddressOverflow)?;
let side = read_u32(game_slot)? as usize;
if side == 0 {
return Err(ResolveError::NullPointer { step: 1 });
}
side.checked_add(GOLD_OFFSET)
.ok_or(ResolveError::AddressOverflow)
}

The Result type makes failure data we must handle, not a surprise we hope never happens.

This interactive lab needs JavaScript. The lesson text around it still explains the same idea.

The path is stable only for the target version and states you tested. The absolute root in this example also depends on the module loading where it did in the verified course run; a relocated module requires base-plus-RVA resolution. Game updates can change layouts and offsets. Document:

  • game version and architecture;
  • module name;
  • offsets in traversal order;
  • the expected final type;
  • how many restarts passed;
  • known states where the chain is unavailable.

Chapter 3 turns this verified path into an external program. First, the final two lessons in this chapter ask a different question: what must be preserved when execution takes a temporary detour through new instructions?

Knowledge check

Prove stability

What is the strongest evidence that a pointer path is useful?

Finished Lesson 2.8?

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.