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

Preparing read-aloud controls…

Lesson 3.8 Listening edition

How C++ Objects Appear in Memory

Turn repeated reads, member-function calls, and virtual dispatch into a tested object layout, then inspect it through typed snapshots.

About 35 minutes

Section: The source code is gone, but the layout remains.

Imagine that a game originally contained this C++ class:

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

The compiler does not normally copy that neat declaration into the finished .exe. Names such as Player, position, and update may disappear. The running program still needs a repeatable arrangement of bytes, though. It must know where each player’s position begins, where the name lives, and which function to call.

Reversing a class means rebuilding a description of that arrangement from what the running program does. It does not restore the original source; it produces a layout that matches the machine code and the program’s behavior. 🧠

This lesson uses AssaultCube 1.2.0.2 32-bit Windows in a local bot match. Offsets are version-specific. A different build may have a different layout.

Section: From changing bytes to a class.

Recovering a class works upward through several levels, and each level is shown by a different technique:

Lesson table
Lesson diagram

Each level rests on the ones below it. If +hexadecimal 4 0 turns out not to be yaw, the object and class levels built on that field have to be checked again, which is why a jump from “bytes changed” straight to “this is a Player class” is so easy to get wrong.

Section: Separate five object-layout terms.

Lesson table

Suppose one player object begins at address shown here, and its yaw is at offset hexadecimal 4 0. The yaw address is:

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

An offset is a distance, not an address by itself. Many player objects can use the same yaw offset while living at different base addresses.

The yaw field is the same distance from every player’s base.

In brief: An object has one base address, and each field sits a fixed distance from it. Many objects can share the same offsets.

Section: What an object looks like in memory.

For the course build of AssaultCube, our recovered player model contains these useful landmarks:

Lesson table
Labelled memory cells

Notice the gaps. We have not claimed that every byte between hexadecimal 0 C and hexadecimal 4 0 is useless. Those bytes may contain velocity, dimensions, pointers, flags, or padding. Unknown means unproven, not empty.

Padding is especially easy to miss. CPUs and compilers often place a field at an aligned address — usually a multiple of that field’s own size — leaving unused bytes in between. Three fields declared in this order produce a layout with a hole in it:

Labelled memory cells

The three bytes at +hexadecimal 0 1, +hexadecimal 0 2, and +hexadecimal 0 3 hold nothing meaningful. They exist only so that health begins at a multiple of four. If you assume fields sit back to back and put health at +hexadecimal 0 1, then every offset after it is wrong by three bytes, and each read returns a value stitched together from pieces of two neighboring fields. ❌

Alternative: Think of parking bays marked in sets of four: a bigger car skips ahead to the next set, leaving empty bays. Padding is those empty bays inside an object.

Section: Find the object base from one known field.

Start with behavior you can control:

Find the local player’s yaw with a float scan while turning left and right.

Put a hardware access breakpoint on that address.

Turn again and inspect the instruction that uses it.

Look for an address calculation shaped like [register + constant].

You may find an instruction shaped like this:

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

Read it as: “copy the 32-bit float located hexadecimal 4 0 bytes after the object in e c x into x m m 0.” If the stopped yaw address is this value, then e c x should be this value.

Verify the arithmetic in the debugger:

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

Labelled memory cells

One matching subtraction is a clue, not proof. Repeat it while turning, respawning, and starting a new match.

Section: The hidden this pointer.

A C++ call such as player.update() looks as if it passes no player argument. A non-static member function still needs to know which player it should update. The compiler supplies a hidden argument called this.

Section: Shared method code still needs a particular receiver.

A field offset describes a position within one layout; it is not an absolute field address. In the invented layout below, both objects store health at base + 8. Ada’s base is hexadecimal 1 0 0 0, so her health is at hexadecimal 1 0 0 8. Bo’s base is hexadecimal 2 0 0 0, so his health is at hexadecimal 2 0 0 8.

Choose the receiver and follow the same method body. The example uses the x86 Microsoft __thiscall convention’s e c x receiver and limits damage so this small subtraction cannot make health negative. It illustrates address selection; it does not define a universal C++ layout or model the whole A B I, application binary interface. Microsoft documents this receiver convention in its __thiscall reference.

Identical code and identical field offsets can modify different objects. To explain one write, recover both the layout and the receiver used on that call. A pointer to the code is not a pointer to the instance’s fields.

In brief: Field address equals receiver base + field offset. Shared method code does not imply shared instance storage.

Receiver cheatsheet: shared method code is not shared object storage; the receiver supplies the instance address; base plus field offset selects that instance’s field.

Section: The call’s frame and the object’s lifetime.

That argument belongs to one call’s working state. Each thread has a stack region for return addresses and temporary call data. On 32-bit x86, e s p marks its current top. Calling another function claims a new stack frame; returning releases that frame. The object that this points to can live much longer on the heap. The pointer in a call frame and the object it names have different lifetimes:

Labelled memory cells

This explains why a debugger’s call stack can lead you to the caller but a saved address of one of its temporary values cannot be assumed valid after the call returns. Lesson 3.10 will trace the longer lifetimes of heap objects.

For the 32-bit Microsoft C++ convention commonly called __thiscall, this usually arrives in e c x:

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

Inside the method, reads often use that same base register:

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

On 64-bit Windows, the first integer or pointer argument normally arrives in r c x, so this is commonly in r c x. This is a calling-convention rule, not a promise that the compiler keeps the value in that register for the whole method. It may copy the pointer into r b x, r s i, or a stack slot.

The practical question is: which value stays constant while several member offsets are read from it? That value is a strong object-base candidate.

Section: Virtual methods leave a useful clue.

A class can declare a method as virtual, allowing different object types to choose different implementations. Many C++ compilers implement this with:

a vtable: a table containing function addresses;

a vptr: a hidden pointer in each object that points to its vtable.

The purpose is to let a single call site reach different code depending on what the object actually is. The compiler cannot know at build time whether a given entity will be a human player or a bot, so rather than writing a fixed destination it writes “call whatever address is in slot 3 of this object’s table.” Each class gets its own table, and every object carries a pointer to the right one.

A 32-bit virtual call often has this general shape:

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

That is two lookups before any game code runs. Followed with real numbers:

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

Labelled memory cells

The offset hexadecimal 0 C is 12, and a pointer in a 32-bit process is four bytes, so 12 / 4 equals 3: this is the fourth entry counting from zero, which is why it is called slot 3. Two objects of different classes executing this identical instruction end up in different functions, because the second step read a different table.

Narration: This is how a game loop can call update on every entity, human or bot, without checking each one. The object carries the pointer to the right code.

Lesson diagram

Do not label every first pointer a vptr. Test it:

The object field should point into a readable table, usually in a module’s read-only data.

Several table entries should look like valid pointers.

Those entries should lead into executable sections.

Objects believed to share a class should usually share the same table pointer.

Breaking on one table entry should show a sensible method receiving the object as this.

This pattern is common, but the C++ language does not require one universal object layout. Link-time optimization, devirtualization, multiple inheritance, and custom engines can all change it.

Section: Inheritance makes the map less tidy.

With single inheritance, the base-class part is often placed at the beginning of the derived object. A BotPlayer may therefore begin with all fields needed by Player, followed by bot-specific state.

Multiple inheritance can place several base parts, each with its own vptr, inside one allocation. A pointer typed as the second base then points a few bytes into the object, and a thunk, a tiny compiler-generated stub, may adjust this before jumping to the real method. So when two candidate bases always differ by the same small constant, they are usually two views of one object. RTTI (run-time type information), when a release build keeps it, can expose a class name near a virtual table; the name says nothing about where any field sits.

One object can therefore answer three questions differently: which block was allocated, which base view a particular pointer addresses, and its dynamic type, the most-derived implementation virtual dispatch chooses. Lesson 3.9 separates those identities with a multiple-inheritance layout and the thunk that adjusts this.

A constructor and a destructor bracket that object’s life. A constructor runs right after an allocation, calls base constructors first, and switches the vptr from the base table to the derived table; an ordinary reset function may fill the same defaults but does none of those three things. A destructor marks the point after which every cached address to the object is stale, which matters more to a memory tool than the original class name. Lesson 3.10 follows that timeline from allocation to free.

Section: Recover a structure step by step.

One memory window is not enough to recover a structure. A repeatable sequence is:

Anchor one field. Find a value with an obvious controlled behavior, such as yaw, health, or ammunition.

Find code that uses it. Break on access. The instruction’s base register and displacement give the field’s offset: [e c x+40h] means the field sits hexadecimal 4 0 bytes (4 times 16 equals 64) past the address in e c x.

Propose an object base. Subtract the displacement from the known field address: a yaw at this value read through [e c x+40h] gives a base of this value minus hexadecimal 4 0 equals this value.

Collect nearby candidates. Watch adjacent aligned values as position, view, health, and state change.

Compare instances. Inspect the local player and several bots at the same offsets.

Assign types carefully. Check byte width, signedness, endianness, value range, and how instructions consume the field.

Trace methods. Look for repeated this + offset accesses and virtual calls.

Test lifetimes. Join, die, respawn, leave, and start a new match. The moments when pointers become null or objects are replaced mark where cached addresses stop being valid.

A field nobody has managed to change on purpose stays unnamed, as field_48 or plain padding, until a controlled change or a traced read shows what it holds.

Section: Do not turn a remote address into a reference.

This is tempting but incorrect for an external tool:

Code example. The code’s comments explain: ❌ `remote_address` belongs to another process, not this tool process.

A reference promises that valid, correctly aligned local memory exists for a lifetime. A number copied from another process cannot keep those promises.

Copy each field through ReadProcessMemory instead:

Code example. The code’s comments explain: ✅ The process boundary and the fallible read remain visible.

Reading fields separately is also friendlier to an incomplete layout. You do not need to invent hundreds of padding bytes just to inspect three confirmed values.

The optional probe reference applies this rule to the confirmed AssaultCube fields with checked offsets and plausibility checks.

Section: Common false conclusions.

“Two nearby floats must be X and Y.” They could be velocity, color, or a timer. Change one behavior at a time.

“The first pointer proves a vtable.” Follow several entries and verify they point to executable methods.

“The debugger showed e c x, so it must always be this.” Confirm the calling convention and watch how the method uses the value.

“The structure worked once, so it is finished.” Compare instances and repeat after object destruction, respawn, and clean launches.

“A successful read proves the address is right.” Windows can copy readable but unrelated bytes. Validate meaning, not only access.

Section: The layout is a versioned model.

An object base is the start of one instance, and each field offset is a byte distance from that base. The hidden this argument selects the instance for a member call; a vptr commonly leads to its virtual-method table. Padding and inheritance can add gaps or make one object reachable through several base views. Those observations form a layout model for the tested game build, not a promise for every build. A remote object address remains an address in the game process, never a valid local Rust reference.

The same technique appears in debuggers, crash analyzers, compatibility tools, profilers, and digital-forensics programs. Each one recovers structure from raw memory without the original source code.

Section: Optional build reference.

The read-only probe below applies this lesson’s layout to a live AssaultCube match. The reconstruction above does not depend on building or running it.

Additional detail: Optional build reference: a read-only object-layout probe.

The lab implementation is windows-labs/src/bin/object_layout_probe.rs. It opens ac_client.exe with read access only, follows the verified local-player and entity-list pointers, then copies the confirmed fields into local snapshots.

Here is the path a pointer takes through the probe before anything is printed:

Lesson diagram

The most important types are deliberately small:

Code example. The code’s comments explain: ✅ Once constructed, an ObjectAddress can never contain null.

ObjectAddress and FieldOffset are both numbers underneath, but they mean different things. The wrappers make it harder to accidentally treat a distance as a complete address. NonZeroUsize also moves the null check into construction. After ObjectAddress::new succeeds, every later function can rely on that invariant instead of asking “is this zero?” again. This is what a strong type does: it stores both a value and a rule about what that value is allowed to mean.

The field reader performs bounded copies and plausibility checks:

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

Why validate values after a successful Windows read? Because “four bytes were copied” does not mean “our class model is correct.” A stale pointer can still lead to readable memory. Rejecting impossible coordinates and flags catches some wrong-object guesses early.

Additional detail: Optional build reference: run the probe.

Use 32-bit AssaultCube 1.2.0.2 on Windows and start a private local match with bots. From the repository root:

Terminal window

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

The output is a set of local snapshots, not live references:

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

Addresses and values will differ. The important checks are structural:

every printed object has a different plausible base address;

the same offsets produce the same kind of value for each object;

names are bounded and readable;

coordinates react to movement;

view angles react to turning;

the probe refuses a player count above 32;

stopping the match produces a useful null-pointer error rather than a crash.