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.

References#