OOP#
Assembly has no classes, no inheritance, no methods. “Object
orientation” at the asm level is structs as layouts plus
vtables as arrays of function pointers plus dispatch via
indirect call. The same structure the C++ and Rust compilers
emit when the operator looks at their output under
objdump -d.
/types`. For function mechanics, see Functions.
Struct as offsets#
A struct in memory is a layout. The operator references fields by adding the field’s offset to the base pointer.
struct user {
uint64_t id; // offset 0
char name[16]; // offset 8
uint32_t age; // offset 24
uint32_t flags; // offset 28
};
; rdi = struct user *u
mov rax, qword [rdi + 0] ; rax = u->id
lea rsi, [rdi + 8] ; rsi = &u->name
mov eax, dword [rdi + 24] ; eax = u->age
NASM’s struc directive names the offsets so the disassembly
reads better.
struc User
.id resq 1 ; offset 0
.name resb 16 ; offset 8
.age resd 1 ; offset 24
.flags resd 1 ; offset 28
endstruc
mov rax, qword [rdi + User.id]
mov eax, dword [rdi + User.age]
Method as function with self first#
A method is a function that takes the object pointer as its
first argument. Under SysV the convention is the same as C:
self rides in rdi.
/* method-style C */
void user_set_age(struct user *self, uint32_t age) {
self->age = age;
}
user_set_age:
mov dword [rdi + User.age], esi ; self->age = age
ret
vtable#
A vtable is an array of function pointers, one per virtual
method, in a fixed order. The object stores a pointer to its
vtable as its first field; method calls fetch the function
pointer from the vtable and call through it.
The C representation.
struct shape_vt {
double (*area)(struct shape *self);
void (*free)(struct shape *self);
};
struct shape {
const struct shape_vt *vt; // offset 0
/* concrete subclasses add fields below */
};
/* call: */
double a = shape->vt->area(shape);
The disassembly.
; rdi = struct shape *shape
mov rax, qword [rdi + 0] ; rax = shape->vt
mov rax, qword [rax + 0] ; rax = vt->area (first slot)
call rax ; tail-style: rdi already holds self
For the second slot (free), [rax + 8]; for the third,
[rax + 16]; one qword per entry. The compiler picks
slot order; the operator reads it from the C++ / Rust type
declaration.
Inheritance via embedding#
The standard C / kernel-style inheritance pattern: embed the parent struct as the first field of the child.
struct circle {
struct shape base; // offset 0
double r; // offset 8 (sizeof shape)
};
A struct circle * and a struct shape * point at the
same address; reading shape->vt from a circle works because
vt lives at offset 0 of the embedded shape.
Casting up (child to parent) is a no-op at the assembly level; the pointer value is the same.
; rdi = struct circle *c
; treat rdi as struct shape *
mov rax, qword [rdi + 0] ; (struct shape *)c -> vt
Constructor and destructor#
The operator’s convention: a function named new allocates
and initialises; a function named free releases. The vtable
slot free is the destructor.
extern malloc
global circle_new
circle_new:
; double r in xmm0
push rbx
mov rdi, sizeof_circle ; sizeof(struct circle)
call malloc
mov rbx, rax
lea rax, [rel circle_vt]
mov qword [rbx + 0], rax ; base.vt = &circle_vt
movsd qword [rbx + 8], xmm0 ; r = arg
mov rax, rbx
pop rbx
ret
Dispatch forms the operator reads#
When the operator sees call qword [rax + 0x10] in a
disassembly, that is almost always a vtable dispatch:
mov rax, [rdi] ; load vtable pointer
call qword [rax + 0x10] ; call the method at vtable slot 2
This is the operator’s recognition pattern for “a virtual call
is happening here”. The slot index / 8 tells the operator
which method in declaration order is being called.
Static dispatch#
If the compiler can prove the concrete type, it emits a direct
call to the concrete method (no vtable, no indirection).
The operator notices the difference; call qword [rax + 0x10]
is virtual; call _ZN6circle4areaEv (a mangled name) is
direct.