Skip to lesson
Game Hacking Academy
Preparing lesson text…
Exit reader mode
Voice and speed

Preparing read-aloud controls…

Lesson 8.10 Listening edition

Hooks as Live Control-Flow Changes

A hook changes live control flow under threads, ABI rules, and versioned bytes; safe installation and removal preserve those contracts.

About 52 minutes

The tool now has an owner for its commands, features, and cleanup. A hook adds a harder lifetime problem: it changes a path that the game itself can enter while the tool is starting or stopping.

A detour is installed into code that other threads may already be executing. Its correctness depends on whole x86-64 instructions, the calling convention, executable-page protections, a valid trampoline, and ownership that can undo the change. We will treat those as one live contract.

Section: A hook changes a live control-flow graph.

Lesson 2.9 replaced a verified span of instructions with a jump to a code cave, then resumed the original path. Writing that jump is the easy part.

The hard part is that you are editing a program while it runs. Another thread may be executing the very bytes you are half-way through rewriting. A call may already be inside your replacement at the moment you decide to remove it. The game may throw away the object you hooked and build a fresh one.

In the vocabulary of Chapter 2, a detour replaces one edge in the game’s control-flow graph — it changes where a single instruction leads. Every difficulty in this lesson comes from doing that to a graph other threads are walking at the same time.

The installation invariant is:

The hook is either completely absent or completely working. There is never a moment when a call can find half of it.

“Completely working” means four things are true at once, for every call: the instructions it lands on are whole, the registers and stack are as the caller expects, everything the hook touches is still alive, and there is a correct way back.

Model installation and removal explicitly:

Lesson diagram
A live call must finish before its hook can be released

There should be no HalfPatched state visible to executing threads.

Resources stay alive until the active callbacks reach zero.

Section: Preserve five contracts.

Lesson table

A successful single-threaded test proves almost none of these under load.

Section: Restoring state and replaying instructions are separate work.

A detour can replace the bytes of an operation that the original path still needs to perform. Its trampoline must execute the displaced work with its original meaning, then continue at the first whole instruction after the replaced span. Saving and restoring a register does not perform that work.

Consider a toy path that loads gold, adds a bonus, and stores the result. The detour replaces the addition with a jump. Its observer temporarily uses e a x, so preserving the incoming value prevents that scratch calculation from changing the bonus’s input. Replay means executing the displaced addition exactly once before continuation. Try the three deliberate mistakes: no replay, two replays, or leaving the temporary register change in place.

The successful case preserves one register and replays one addition. It is only a computation model. A real trampoline also has to preserve live flags and required machine state, relocate address-dependent operands, satisfy the calling convention, and survive concurrent entry. The five contracts above remain necessary even if the toy’s final number matches.

There is a third operation at removal: restore the original patch-site bytes after callers can no longer enter or remain in the detour. That puts the original code path back. Replaying displaced work during a call, preserving machine state during observer work, and restoring bytes during removal happen at different times and solve different problems.

In brief: Preserve the observer’s live inputs, replay displaced work once per call, and restore original patch bytes during safe removal. None substitutes for the other two.

Restoration cheatsheet: preserve live register/flag inputs; replay displaced work exactly once; restore original patch bytes only during safe removal. These are three separate jobs.

Section: Decode before deciding patch length.

x86-64 instructions have variable length. Decode complete instructions until their total size can hold the chosen branch encoding. Every displaced instruction has to be kept, because the trampoline runs it later, and any operand that depends on the instruction’s own address has to be identified:

r i p-relative memory operands;

relative calls and jumps;

conditional branches;

instructions whose behavior depends on the original address.

Copying such bytes to a trampoline without relocation changes their target.

You can watch it happen with a r i p-relative load. At the original site, the instruction means “read the global sitting hexadecimal 2 F 1 A bytes past the end of this instruction”:

Lesson diagram

Copy those seven bytes into a trampoline at a different address. The displacement is unchanged, but the address it is measured from is not:

Lesson diagram

The bytes are byte-for-byte identical and the instruction is entirely valid. It just reads somewhere else now. Relative calls and conditional branches fail in exactly the same way, and the symptom is rarely a crash at the hook — it is a wrong value, or a jump into unrelated code, surfacing much later. The trampoline builder must either rewrite the operand correctly or reject the site. “Unsupported instruction” is a valid, reliable outcome.

Section: Make installation a transaction.

Prepare everything before publishing the detour:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

ExecutableBlock stands for the owning wrapper around the trampoline’s executable allocation; its implementation depends on the allocation strategy used by the detour tool. This sketch describes the state that must stay alive, rather than supplying another complete hook library. The saved original bytes belong to the same owner, so restoration cannot outlive its evidence.

The preparation stage can verify:

exact target build and module identity;

current bytes equal expected;

all displaced instructions decode completely;

relative operands can be relocated;

trampoline and return address are reachable;

callback A B I, application binary interface, matches the target function;

a dry-run decode of the final patch succeeds.

If a precondition fails, publish nothing: release unused prepared resources and report the failed check. Only a verified preparation may reach the owner that publishes the change. If verification fails after that owner has changed the bytes, roll back its change under the same thread-safety mechanism used for installation. As a flow:

Lesson diagram

Section: Publication must have a real thread-safety mechanism.

A multi-byte detour is not magically atomic. Another thread must not fetch the first half of the new jump while the second half still contains old bytes. Pick one publication mechanism and name it in the design:

Lesson table

With peer threads unable to execute the patch span, the publisher changes the page protection, writes the complete detour, calls Windows FlushInstructionCache for the modified range, restores the original page protection, rereads the bytes, and only then releases the rendezvous. The same sequence applies in reverse during removal. VirtualProtect only changes page permissions; it does not make a five-byte jump atomic or synchronize other cores.

Do not claim that an ordinary byte copy is atomic. A single aligned store is a valid shortcut only when the architecture and exact patch encoding explicitly guarantee atomicity for that width. If those facts are not proven, use a real rendezvous or a transaction-capable hook library.

Section: Re-entry is part of the contract.

A callback can indirectly call the hooked function again. That is re-entry. Choose and document a policy:

Lesson table

A thread-local depth counter can implement “bypass nested” in a local fixture. It is not a substitute for process-wide lifetime tracking during removal.

Section: Keep callback work bounded.

The hooked thread may be the render thread or simulation thread. A callback should capture a small owned event and return. Do not perform blocking I/O, wait for another thread that may need the current thread, hold a global lock through the original function, or let a panic cross the callback boundary.

A bounded queue makes overload visible. Count dropped events rather than silently allocating without limit.

Section: Drain before removing.

Removal needs two counters or states:

whether new callback work is accepted;

how many callbacks are currently active.

Set the state to Draining, stop publishing new work, wait for the active count to reach zero using a bounded policy, restore the exact original bytes, verify them, then release trampoline and callback state. If draining times out, keep the still-valid resources and report the incomplete shutdown; freeing them is not recovery.

Section: Test more than the happy path.

Use a local fixture function and cover:

expected bytes match and installation succeeds;

one expected byte differs and nothing is written;

a displaced r i p-relative instruction is relocated or cleanly rejected;

nested callback entry follows the documented policy;

16 or more threads call the fixture while events are captured;

the event queue fills and reports drops;

removal begins while callbacks are active;

install/remove repeated hundreds of times leaves original bytes intact;

a callback failure returns a defined result without unwinding across the A B I.

Section: Glossary terms introduced here.

Detour: a deliberate replacement of one control-flow edge.

Trampoline: relocated displaced instructions followed by a return edge.

Relocation: adjusting address-dependent instructions for a new location.

Re-entry: the hooked path is entered again before an earlier call returns.

Draining: refusing new work while existing users finish.

Publish: make prepared state visible to other threads.

A hook works only while its instruction, A B I, identity, concurrency, and lifetime contracts all hold. Installation prepares and validates before publication; removal drains active calls before freeing anything. Rollback and the stress cases above test the transitions where those contracts are easiest to break.

With the hook’s lifetime established, we can use it to observe rendering. The next chapter connects the game’s coordinate frames to the draw calls and pixels that a rendering tool works with.