Skip to content
Lesson 2.9Following Instructions and Addresses

How Detours Change Control Flow

Understand how a small detour can run extra instructions and still return the game to its original path.

Lesson 2.9
Read or listenPrint chapter or book
About 14 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.9Key ideas, reference and questions

A one-page summary of “How Detours Change Control Flow” you can keep beside your notes. Plain Markdown.

Lesson 2.9 — How Detours Change Control Flow: cheatsheet

  • Remember: A detour has five parts: a hook site, saved bytes, a cave, the extra logic, and a return address. Get one wrong and the game may crash.
  • Also useful: Think of cutting a sentence to paste in a sticker: cut between words, never in the middle of one, or the leftover letters read as nonsense.

Key terms

  • code cave: A code cave is a spare executable area used for extra code.
  • extra_logic: extra_logic is a placeholder, not a ready-to-install hook.
  • lifecycle code versus boundary code: The key distinction is lifecycle code versus boundary code.

Review

  • Q: Where should a code-cave detour return after replaying displaced behavior? A: The first complete original instruction after the overwritten region
  • Q: A jump needs five bytes, but the fifth byte lands in the middle of the next CPU instruction. What should the patcher do? A: Cover enough whole instructions, save them, and replay their meaning before jumping back
Download cheatsheet (.md)

The pointer path gives us a repeatable way to find data, but it cannot add behavior. Replacing the gold-changing instruction with nop was useful for one quick test, but it could only remove behavior. What if we want to observe values, add a check, or run several instructions and then resume the game?

A code cave is a spare executable area used for extra code. A detour redirects execution there and then returns.

The road-diversion image behind the name is worth taking literally, because every part of it maps onto something you have to get right. You put a sign at one junction sending traffic down a side road that rejoins the main road further along. You have not demolished the main road, so traffic can be restored by removing the sign. The side road must rejoin — a diversion that just stops leaves everyone stranded, which is what a detour with no return jump does. And the sign has to replace a whole junction, not half of one, which is the instruction-boundary problem below.

original code runs
up to the hook site
hook site: a jump now sits
where whole instructions were
cave: save registers and flags
cave: run the extra logic
cave: restore registers and flags
cave: replay the replaced instructions
jump back to the return address
original code continues

A responsible detour has:

  1. hook site — the instructions being replaced;
  2. saved bytes — an exact copy of those original instructions;
  3. cave — memory where the new code can execute;
  4. extra logic — the observation or change;
  5. return address — the first untouched instruction after the hook.

If any one is wrong, the game may crash or behave strangely.

On x86, one instruction may be 1 byte and another may be 7 bytes. A typical near jump needs 5 bytes. You must replace whole instructions until you have enough room.

The reason is that the CPU keeps no map of where instructions begin. It starts decoding at whatever address it is handed and reads forward, letting each opcode announce how many bytes it consumes. Start it one byte late and every instruction from that point on is decoded from the wrong position.

Take the six-byte span this lesson hooks:

0x10008B
01
0x10028D
74
26
00
mov eax, [ecx]
lea esi, [esi]
Two whole instructions, six bytes.

A five-byte jump written over it covers 8B 01 8D 74 26 and strands the final 00. When the cave later returns to 0x1005, the CPU does not see “the leftover tail of a lea.” It sees a fresh byte stream beginning with 00, which starts a completely different instruction that swallows whatever follows as its operands. Execution continues confidently into nonsense, and the crash usually surfaces somewhere unrelated a few instructions later — which is what makes this mistake so hard to diagnose after the fact.

0x1000E9
??
??
??
??
0x100500
jmp to the cave
stranded
Bad: exactly five bytes copied. The cave returns to 0x1005, and the CPU starts a new instruction at the orphaned 00.
0x1000E9
??
??
??
??
90
jmp to the cave
nop
Good: both whole instructions covered, the spare byte filled with nop. The cave returns to 0x1006, the first untouched byte.

So you round up to the next instruction boundary: cover all six bytes and fill the spare sixth byte with a nop. The return address is the hook address plus the total number of whole bytes replaced.

Make room without cutting an instruction. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

sizes = [2, 4]
covered = 0
take the next whole instruction
repeat until covered >= jump_size
padding = covered - jump_size
resume = hook_start + covered

Result: cover 6 bytes and resume at 0x1006, a complete instruction boundary.

Final values: jump = 5; covered = 6; hook = 0x1000; padding = 1; resume = 0x1006.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.

    jump = 5.

  2. Step 2, line 2. Begin at illustrative hook address 0x1000.

    jump = 5; covered = 0; hook = 0x1000.

  3. Step 3, line 3. Cover the complete first instruction, which is two bytes.

    jump = 5; covered = 2; hook = 0x1000.

    Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.

  4. Step 4, line 4. The first instruction is too short, so also cover the entire four-byte second instruction.

    jump = 5; covered = 6; hook = 0x1000.

    Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.

  5. Step 5, line 5. The replacement fits, leaving 1 byte(s) of padding.

    jump = 5; covered = 6; hook = 0x1000; padding = 1.

    Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.

  6. Step 6, line 6. Result: cover 6 bytes and resume at 0x1006, a complete instruction boundary.

    jump = 5; covered = 6; hook = 0x1000; padding = 1; resume = 0x1006.

    Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.

For the pinned Wesnoth 1.14.9 build, the Terrain Description menu reaches a six-byte span at 0x00CC_AF8A: 8B 01 8D 74 26 00. The first two bytes encode mov eax,[ecx]; the other four encode an alignment lea. A jump needs five bytes, so it must cover both whole instructions and resume at 0x00CC_AF90, the first untouched byte.

This short x86 sketch puts code beside the flow diagram above. The two instructions near the end replay the behavior removed from the hook site; the final jump lands after all six covered bytes:

pushfd ; save the interrupted flags
pushad ; save the 32-bit general registers
call extra_logic ; observe or change something
popad
popfd
mov eax, [ecx] ; replay the first covered instruction
lea esi, [esi] ; replay the second covered instruction
jmp 0x00CCAF90 ; first untouched Wesnoth instruction
Where eip goes during one trip through the detourThe six bytes at 0x00CCAF8A now hold a jump and a nop. The two instructions they held run in the cave instead, exactly once.
  1. 1 At 0x00CCAF8A: jump to the cave

    Hook site, 6 bytes
    0x00CCAF8A0x00CCAF8F
    E9 rel32jmp cave
    90nop

    The game reaches the hook site. Its six bytes are now a five-byte jump and a nop.

  2. 2 In the cave: extra work, then the saved bytes

    Cave: the added work
    save
    extra logic
    restore
    Cave: the saved bytes
    8B 01mov eax,[ecx]
    8D 74 26 00lea esi,[esi]
    jmp back

    The extra logic runs between save and restore. Then the two whole instructions the jump covered run here, once.

  3. 3 Back at 0x00CCAF90

    Game code again
    jmp backfrom the cave
    0x00CCAF90first untouched instruction

    jmp back lands on 0x00CCAF8A + 6 = 0x00CCAF90, the first byte the patch did not touch.

The six bytes at 0x00CCAF8A now hold a jump and a nop. The two instructions they held run in the cave instead, exactly once.

extra_logic is a placeholder, not a ready-to-install hook. The exact implementation also has to verify the original bytes, encode a reachable relative jump, and restore the patch when disabled.

Lesson 2.10 uses x64dbg to trace that route with breakpoints before adding behavior. Lesson 8.3 then builds the full DLL version: checked gold pointer path, naked x86 cave, relative jump bytes, saved registers and flags, injector commands, F1 toggle, and restoration. The exact code and stack diagram live beside that runnable implementation.

A detour begins and ends at machine-code boundaries. The surrounding program can:

  • allocate and own buffers;
  • calculate and verify addresses;
  • decode instructions with a library;
  • build a patch plan;
  • restore original bytes automatically;
  • expose a small safe API.

The final jump bytes and an in-process hook still require unsafe code and a small amount of assembly. Typed code manages the larger, error-prone lifecycle:

struct Patch {
address: usize,
original: Vec<u8>,
replacement: Vec<u8>,
}
impl Patch {
fn is_compatible_with(&self, found: &[u8]) -> bool {
found == self.original
}
}

Before writing, compare the bytes in memory with original. If they differ, stop. The target may be a different version or another tool may already have changed it.

A patch is not complete until it has a cleanup path. Plan restoration before applying the detour:

  • store the exact original bytes;
  • restore them when the feature is disabled;
  • restore them when the tool exits normally;
  • make repeated enable/disable operations safe.

This habit will shape the wrappers we build later.

The key distinction is lifecycle code versus boundary code. The manager owns the patch, validates the target, reports failures, and restores state. Assembly describes the few CPU instructions that must run at the exact hijacked boundary.

Knowledge check

Return safely

Where should a code-cave detour return after replaying displaced behavior?

Finished Lesson 2.9?

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.