Skip to content
Lesson 3.9Typed Memory, Ownership, and Object Layouts

Object-Oriented Clues in Machine Code

Recognize the shapes that factories, states, observers, singletons, and component systems leave in compiled code: repeated pointers, virtual calls, constructors, and update loops.

Lesson 3.9
Read or listenPrint chapter or book
About 42 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 3.9Key ideas, reference and questions

A one-page summary of “Object-Oriented Clues in Machine Code” you can keep beside your notes. Plain Markdown.

Lesson 3.9 — Object-Oriented Clues in Machine Code: cheatsheet

  • Remember: Objects sharing a first pointer to a table of function addresses is a strong sign of virtual methods, but it never gives you the class name.
  • Also useful: Think of a group chat: one message goes out and every subscriber gets the same text. An update loop is more like a teacher visiting each desk in turn.

Key terms

  • programming paradigm: A programming paradigm is a style for organizing instructions and responsibilities.
  • Actor: A name such as Actor is a label chosen from the object's behavior; nothing in the binary says what the original class was called.

Review

  • Q: Which observation most strongly supports a suspected C++ vptr? A: The object starts with a pointer to a read-only table whose entries lead to executable methods
  • Q: Compiled code has no names for design patterns such as a factory. What survives instead? A: Recognizable shapes in memory and code, like rows of function pointers
Download cheatsheet (.md)

A C++ programmer may write a class named EnemyFactory, a State interface, and an Observer list. The processor never sees those friendly names. After compilation it sees addresses, loads, stores, comparisons, and calls.

That sounds discouraging, but design patterns still leave shapes in memory and code. A vtable is a row of function pointers. A factory allocates an object and initializes it. An observer loop walks a collection and calls similar functions. One shape alone is common in unrelated code, but several that fit together usually point to one pattern. 🧩

When debug symbols exist for a build, they give the real names and layouts, so they can check a layout recovered from the version-pinned executable.

A design pattern names recurring roles and expected changes, not just dependency removal. Similar machine-code shapes can have different intent.

For example, both State and Strategy may delegate through a function table. State usually represents behavior that changes as an object moves through a lifecycle. Strategy usually selects one replaceable calculation or action, such as how a weapon chooses its aim point. Observed transitions and call sites matter more than the table shape.

In your own tool, use a pattern after repeated change exposes a real problem: duplicated lifecycle code, tangled ownership, or features that cannot be tested separately. Do not add a factory, observer, or trait hierarchy only because the name sounds professional. A smaller function or enum is often the clearer design.

A pattern name is a likely role, not a recovered source name or proof of intent.

Programming paradigms: ways to organize a purchase

Section titled “Programming paradigms: ways to organize a purchase”

A programming paradigm is a style for organizing instructions and responsibilities. It does not change what a purchase means. Ada still starts with 100 gold, pays 25 if she can, and ends with 75. The styles answer a different question: where should the affordability check and subtraction live, and how should other code use it?

The examples below revisit the guarded purchase from Lesson 1.4. For each isolated fragment, the caller starts with mutable gold of 100 and cost of 25. A minimal player record and the shared calculation are:

struct Player {
gold: u32,
}
fn spend_if_possible(gold: u32, cost: u32) -> u32 {
if gold >= cost {
return gold - cost;
}
return gold;
}

This compares source organization. It does not imply that a Rust record has the C++ virtual-table layout examined later in this lesson.

Style Main idea Our purchase example
Procedural or imperative Write the steps that change state Check gold, then subtract cost.
Object-oriented Put related data and behavior together A Player has gold and a buy method.
Functional Calculate an output from inputs without changing hidden state spend_if_possible(100, 25) returns 75.
Event-driven Run a handler when something happens A Buy click calls the purchase function.

The direct-update version is procedural. It says how to update the variable, in order:

if gold >= cost {
gold = gold - cost;
}

That is often the clearest way to begin. It becomes awkward only if every screen and bot copies the same purchase steps, because a later change to the affordability check would have to be repeated everywhere. Our named function solves that duplication.

Object-oriented: keep behavior with its data

Section titled “Object-oriented: keep behavior with its data”

A Player record already keeps related data together. A method can put the purchase behavior beside that record, so ada.buy(25) reads as an action on Ada. The Rust Primer shows the same organization with impl Runner and explains why a method receives editable access to its runner. Rust methods do not require class inheritance; Lesson 3.8 later examines how a compiled C++ game’s classes and methods appear in memory.

Our spend_if_possible function illustrates a functional style. Given 100 and 25, it returns 75. It does not secretly reach out and change Ada. The caller decides whether to store the answer:

let next_gold = spend_if_possible(100, 25); // 75

That makes the calculation easy to check with different inputs. A larger program can mix this style with state-changing methods; the two are useful for different jobs.

Event-driven: respond when something happens

Section titled “Event-driven: respond when something happens”

A real game stays open instead of running main once and closing. A Buy click can produce an event, and the interface or game engine can call a registered handler in response:

fn on_buy_click(player: &mut Player) {
player.gold = spend_if_possible(player.gold, 25);
}

This shows the handler’s job, not a complete window setup. A game also updates and draws while no one clicks. Lesson 6.9 explains Windows messages and game loops; Lesson 4.9 compares events with checking for changes on a schedule. For now, notice that the event-driven style decides when the purchase calculation runs.

One game can use all four styles: an event calls a handler, which asks a player method to update state using a small calculation. A paradigm is a way to organize work, not a team you must join forever.

Start from what the code does, not class names

Section titled “Start from what the code does, not class names”

Suppose three heap objects all begin with the same pointer. That pointer leads into a read-only section containing executable addresses. Several call sites do this:

mov rcx, rbx ; rcx is probably the current object (`this`)
mov rax, [rcx] ; read the first field, possibly a vtable pointer
call qword ptr [rax+20h] ; call virtual slot 4
slot 4, at +0x20
object A
+0x00: table pointer
one read-only table
of function addresses
object B
+0x00: table pointer
object C
+0x00: table pointer
the function called,
with the object in rcx

What this code shows is small:

Objects at these addresses share a function table, and slot four receives the object address.

Slot four is at +0x20 because each slot holds one 8-byte function address on 64-bit Windows, and 0x20 is 2 × 16 = 32 bytes, which is 32 ÷ 8 = 4 slots past slot zero.

Predict first

A 32-bit build of the same game makes the call as call dword ptr [eax+20h]. A 32-bit process stores each function address in 4 bytes.

Which slot of the table does this call use?

Answer: Slot 8. 0x20 is still 32 bytes, but each slot is now 4 bytes wide, so 32 ÷ 4 = 8 slots past slot zero.

Which table slot does the call use?call [table + displacement] reads one function address from the table. Each slot holds one pointer, so the slot number is the displacement divided by the pointer size.

0x20 selects slot 4

Table, 8 bytes per slot
slot 0+0x00
slot 1+0x08
slot 2+0x10
slot 3+0x18
slot 4+0x20
slot 5+0x28
Displacement ÷ slot size
0x20 = 32
÷ 8
slot 4

0x20 is 32 bytes, and 32 ÷ 8 = 4, so the call reads slot 4. Slot 0 is the first entry, at +0x00.

The code does not show the source class name, the inheritance tree, or the method name. Two facts can be checked directly: offset 0x00 points into read-only module memory, and the entries of that table point to executable module pages. Together they match how C++ compilers implement a vptr, so offset 0x00 is very likely one. A name such as Actor is a label chosen from the object’s behavior; nothing in the binary says what the original class was called.

A singleton is one shared object. Look for many functions loading the same global pointer before accessing fields:

mov rax, [rip+player_manager]
test rax, rax
je not_ready
mov ecx, [rax+30h]
recruit code
one global pointer
save code
interface code
the one manager object

One global pointer alone does not make a singleton; it might be a cache or the current scene. A singleton is allocated once during initialization, reused by many unrelated call sites, and released at shutdown.

A factory chooses which concrete object to create. A common compiled shape is:

  1. inspect a type ID;
  2. choose an allocation size;
  3. call an allocator;
  4. call one of several constructors;
  5. return a common base pointer.
1
2
3
type ID
switch
allocate, then call
the first constructor
allocate a different size,
call the second constructor
allocate, then call
the third constructor
return a pointer
to the common base

A switch followed by several constructor calls is more useful than the word “factory.” Follow each constructor and compare its first vptr write and field initialization.

State and strategy patterns both replace one behavior with another. Memory may contain a pointer to a small behavior object or a function table. The owner delegates work to it each update.

sees the player
in range
target moves away
loses the player
Idle
Chase
Attack

Each arrow is a moment when the owner’s behavior pointer changes. If it changes when an enemy moves from idle to chase to attack, “state” is a useful name. If it changes when a weapon changes its aim-point calculation, “strategy” may fit better. The bytes do not care which textbook word you choose; behavior over time is the deciding clue.

An observer system keeps a collection of listeners and calls them when an event occurs. Look for one loop that:

  • walks a vector, list, or handle table;
  • passes the same event record to each element;
  • makes a direct or virtual call;
  • tolerates an empty collection.
event: a unit died
notify loop:
same event record for each
score panel
sound system
achievement tracker

Do not confuse this with an ordinary entity update loop. An observer loop usually starts from an event or notification function and passes similar event data to many unlike objects.

Composition stores behavior in separate components rather than one deep inheritance tree. Useful clues include:

  • an entity ID used to index several pools;
  • type IDs mapped to separate arrays;
  • bit masks describing which components an entity owns;
  • tightly packed arrays of positions or health values;
  • systems that loop over one component type at a time.
pos 0
pos 1
pos 2
pos 3
pos 4
one component type, packed together
A movement system walks this array and touches nothing else.

If position records are contiguous while unrelated entity fields live elsewhere, do not force them into one giant Player structure. The game may be data-oriented.

A table slot dispatches to one object. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

receiver = selected_object
table = receiver.vptr
slot_address = table + 4 * 8
function = read_pointer(slot_address)
call function(receiver)
record which object received the call

Result: object A received the call. The table gives a dispatch mechanism, not the original source class name.

Final values: object = A; receiver = 0x1000; calls_A = 1; calls_B = 0; table = 0x3000; slot_address = 0x3020; function = 0x4000.

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.

    object = A.

  2. Step 2, line 1. The selected object has its own base address.

    object = A; receiver = 0x1000; calls_A = 0; calls_B = 0.

  3. Step 3, line 2. Both objects point to this illustrative shared table.

    object = A; receiver = 0x1000; calls_A = 0; calls_B = 0; table = 0x3000.

  4. Step 4, line 3. Slot four lies four eight-byte entries after slot zero.

    object = A; receiver = 0x1000; calls_A = 0; calls_B = 0; table = 0x3000; slot_address = 0x3020.

  5. Step 5, line 4. Read the function pointer from that slot.

    object = A; receiver = 0x1000; calls_A = 0; calls_B = 0; table = 0x3000; slot_address = 0x3020; function = 0x4000.

  6. Step 6, line 5. Pass the selected object as the receiver.

    object = A; receiver = 0x1000; calls_A = 1; calls_B = 0; table = 0x3000; slot_address = 0x3020; function = 0x4000.

  7. Step 7, line 6. Result: object A received the call. The table gives a dispatch mechanism, not the original source class name.

    object = A; receiver = 0x1000; calls_A = 1; calls_B = 0; table = 0x3000; slot_address = 0x3020; function = 0x4000.

A constructor often writes the vptr and then initializes fields:

mov [rcx], rax ; vptr at +0x00
mov dword ptr [rcx+8], 64h ; likely integer default 100
mov qword ptr [rcx+10h], 0 ; likely pointer or 64-bit field
+0x00vptr
+0x04vptr
+0x08100
+0x0C?
+0x100
+0x140
mov [rcx], rax: 8 bytes
dword 100
not written
qword 0
Each box is four bytes. The three stores reach +0x18; the bytes at +0x0C are never written, so they stay unknown.

The last store starts at +0x10 and writes a qword—eight bytes—so it touches offsets +0x10 through +0x17; +0x18 is the first byte after the store. That gives a minimum object size of at least 0x18 bytes, but alignment may make the allocation larger. Compare the allocator’s requested size, constructor stores, destructor reads, and ordinary methods. No single function tells the whole story.

One object can have several useful identities

Section titled “One object can have several useful identities”

Lesson 3.8 placed a single-inheritance base part at the start of the derived object. Multiple inheritance makes the map less tidy:

+0x00vptr for base A
A's fields
+0x10vptr for base B
B's fields
derived fields
one allocation, one complete object
what a pointer typed as B points at: 0x10 bytes in
Multiple inheritance, with an example offset. A method of B receives the object as a pointer to the B part, not to the start.

Multiple inheritance can add more than one base subobject and more than one vptr. A method may even adjust this before using it.

When inspecting object-oriented code, separate three questions:

  1. Allocation identity: which complete block of memory was created and must eventually be released?
  2. Subobject view: which base-class-shaped part does this particular pointer address?
  3. Dynamic type: which most-derived implementation will virtual dispatch choose at this moment?

These answers can differ. A pointer passed to a base-class method may point a few bytes into the complete allocation. A thunk — a tiny compiler-generated stub that fixes something up and then jumps to the real function — may adjust this before jumping to the real method. Subtracting every observed field from that adjusted pointer can produce negative offsets and a broken layout even though the pointer is valid for its particular base view.

When two candidate bases always differ by the same small constant, they are usually two views of one object. The thunk or call site that adds the constant shows where the adjustment happens, and the constructor, which writes both vptrs, shows how the two views sit inside one allocation.

RTTI stands for run-time type information: extra data the compiler emits so that code can ask what class an object really is while the program is running. It is what makes dynamic_cast possible.

When it is present it can expose type-descriptor clues near virtual tables, sometimes including a readable class name. That name tells you what the class was called and nothing more: release builds can strip RTTI entirely, and a name does not tell you where any of the class’s fields sit.

  1. Collect several live object addresses from the exact local build.
  2. Group objects by their first pointer and validate that it leads to executable function entries.
  3. Find code that writes that first pointer; it may be a constructor.
  4. Find code that allocates the object and read the requested size.
  5. Label every repeated field access as field_08, field_10, and so on.
  6. Watch which actions change each field.
  7. Find virtual call slots and trace their arguments and side effects.
  8. Name a pattern only when several of these shapes agree.
  9. Compare with source or debug symbols as a final answer key.

The goal is not to make the class diagram look impressive. The goal is to build a model that correctly predicts what the program will do next.

  • ❌ Naming a class from one interesting string.
  • ✅ Naming fields provisionally, such as field_08, until their behavior is clear.
  • ❌ Treating every first pointer as a vptr.
  • ✅ Checking page permissions and several table entries.
  • ❌ Assuming inheritance because two objects share fields.
  • ✅ Checking vtables, constructors, conversions, and call behavior.
  • ❌ Forcing an entity-component design into one C-style struct.
  • ✅ Letting access patterns reveal where each component really lives.

Next, we will add ownership and lifetime clues so the recovered model explains not just what an object is, but who keeps it alive.

Knowledge check

Recognize a vptr

Which observation most strongly supports a suspected C++ vptr?

Finished Lesson 3.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 3: 11 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.