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.
Cheatsheet for Lesson 3.9
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 asActoris 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
Source patterns do not survive as labels
Section titled “Source patterns do not survive as labels”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 pattern name communicates intent
Section titled “A pattern name communicates intent”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. |
Procedural: follow the steps
Section titled “Procedural: follow the steps”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.
Functional: make the result visible
Section titled “Functional: make the result visible”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); // 75That 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 pointercall qword ptr [rax+20h] ; call virtual slot 4What 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.
0x20 selects 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.
Five patterns and the clues they leave
Section titled “Five patterns and the clues they leave”Singleton or global manager
Section titled “Singleton or global manager”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, raxje not_readymov ecx, [rax+30h]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.
Factory
Section titled “Factory”A factory chooses which concrete object to create. A common compiled shape is:
- inspect a type ID;
- choose an allocation size;
- call an allocator;
- call one of several constructors;
- return a common base pointer.
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 or strategy
Section titled “State or strategy”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.
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.
Observer
Section titled “Observer”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.
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.
Component or entity-component system
Section titled “Component or entity-component system”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.
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 callResult: 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
Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.
object = A.
Step 2, line 1. The selected object has its own base address.
object = A; receiver = 0x1000; calls_A = 0; calls_B = 0.
Step 3, line 2. Both objects point to this illustrative shared table.
object = A; receiver = 0x1000; calls_A = 0; calls_B = 0; table = 0x3000.
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.
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.
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.
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.
Constructors are layout witnesses
Section titled “Constructors are layout witnesses”A constructor often writes the vptr and then initializes fields:
mov [rcx], rax ; vptr at +0x00mov dword ptr [rcx+8], 64h ; likely integer default 100mov qword ptr [rcx+10h], 0 ; likely pointer or 64-bit fieldmov [rcx], rax: 8 bytesdword 100qword 0+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:
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:
- Allocation identity: which complete block of memory was created and must eventually be released?
- Subobject view: which base-class-shaped part does this particular pointer address?
- 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.
A repeatable reconstruction method
Section titled “A repeatable reconstruction method”- Collect several live object addresses from the exact local build.
- Group objects by their first pointer and validate that it leads to executable function entries.
- Find code that writes that first pointer; it may be a constructor.
- Find code that allocates the object and read the requested size.
- Label every repeated field access as
field_08,field_10, and so on. - Watch which actions change each field.
- Find virtual call slots and trace their arguments and side effects.
- Name a pattern only when several of these shapes agree.
- 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.
Common mistakes
Section titled “Common mistakes”- ❌ 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.
Recognize a vptr
Which observation most strongly supports a suspected C++ vptr?
A pointer from the object to a read-only table whose entries lead to executable methods matches how compilers commonly implement virtual dispatch. It shows the object has virtual methods; it does not recover the class name.
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.
Plain Markdown, saved in this browser.
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.