Addressing Modes

An addressing mode is an expression that names a memory location. x86-64 offers one flexible formula instead of many separate modes — master that formula and arrays, structs, and stack frames all become the same pattern.

The Common Addressing Modes

Textbooks list seven x86 addressing modes. In daily use, five of them appear constantly, and they are all special cases of one formula you will meet in the next section.

Immediate — the value is in the instruction

    mov  rax, 42            ; load the constant 42 into rax
    add  rbx, 100           ; add the constant 100

Fast (nothing is fetched from memory) but limited: an immediate cannot be a destination, because a constant has no storage.

Register — the value is in a register

    mov  rax, rbx           ; copy one register into another
    add  rax, rbx           ; register-to-register arithmetic

The fastest of all operand forms. Most well-written assembly keeps tight loops entirely in registers and touches memory only at the boundaries.

Direct — the value is at a named address

section .data
    counter  dd  0

section .text
    mov  eax, [counter]     ; read the 4 bytes at the address of `counter`
    mov  [counter], 5       ; write 5 there (size needed: `mov dword [counter], 5`)

The label counter is an address. Square brackets turn that address into "the contents at that address".

Indirect — the address is in a register

    mov  rbx, buffer        ; rbx now holds the address of buffer
    mov  al,  [rbx]         ; load one byte FROM that address
    mov  [rbx], al           ; store one byte TO that address

This is how pointers work. The register holds an address; the brackets say "go there". Load a pointer into a register once, and you can walk a whole buffer with it.

Base + Index + Displacement — the workhorse

    ; array of 32-bit ints at `values`, read element i
    ;   address = values + i*4
    mov  eax, [values + rcx*4]
    ;   or, with the base in a register:
    mov  eax, [rbx + rcx*4]

This single form covers array indexing, struct field access, and stack-frame locals. Everything else in this lesson is a variation on it.

The Effective Address Formula

All of x86-64's memory syntax is one expression, evaluated by the CPU's address-generation unit:

    ;           base  +  index * scale  +  displacement
    mov  eax, [rbx   +  rcx   * 4      +  8         ]
    ;          ^         ^       ^         ^
    ;       any 64-bit  any     1,2,4,8   a constant (may be negative)
    ;       register   64-bit   only
    ;                   register (not rsp)

Every part is optional, and the CPU computes the sum for free as part of executing the instruction — no separate add is needed. The scale must be 1, 2, 4, or 8 because those are the sizes of byte, word, dword, and qword elements; no other multiplier is encodable.

Written asEffective addressTypical meaning
[rbx]rbxFirst byte of a buffer
[rbx + 8]rbx + 8Second qword of a struct
[rbx + rcx]rbx + rcxByte array indexed by rcx
[rbx + rcx*4]rbx + 4·rcxDword array (int[]) — the classic
[rbx + rcx*8 + 16]rbx + 8·rcx + 16Field 16 of struct in a qword array
[rbp - 4]rbp − 4Local variable on the stack frame

Note the brackets. lea rbx, [msg] computes the address without reading memory, while mov rax, [rbx] reads the eight bytes stored there. That single distinction causes more beginner confusion than any other part of assembly.

RIP-Relative Addressing

Modern executables are often position-independent: the loader may place the code anywhere in memory. An instruction that hard-codes an absolute address would then be wrong, so x86-64 offers a relative form: the address is computed as rip + displacement, and the displacement is fixed at assembly time.

    default rel                     ; make [label] mean [rel label] by default

section .rodata
    msg  db "Hello", 10

section .text
    lea  rdi, [rel msg]             ; position-independent: no absolute address
    mov  rsi, [rel some_qword]      ; same idea for data loads

With NASM, adding default rel at the top of a file and writing [rel label] is the habit that avoids the relocation R_X86_64_32S against `.rodata` link error. It costs nothing at run time and makes your code work under every loader.

Walking an Array in Practice

Every array traversal is the same addressing expression with a moving index. Here is the loop that prints an array of 32-bit values, one per line, using base + index*scale:

section .rodata
    values  dd  10, 20, 30, 40, 50
    COUNT   equ ($ - values) / 4        ; 5 elements (each is 4 bytes)

section .text
    mov     rbx, values                 ; rbx = base address  (rsi role: base)
    xor     rcx, rcx                    ; rcx = index i = 0

.loop:
    cmp     rcx, COUNT                  ; i == COUNT ?
    jge     .done                       ; yes -> finished

    mov     eax, [rbx + rcx*4]          ; eax = values[i]  (scale 4 = dword)
    ; ... use eax here (a real program would print it) ...

    inc     rcx                         ; ++i
    jmp     .loop                       ; continue

.done:
    ; exit
    mov     rax, 60
    xor     rdi, rdi
    syscall

Change the scale and you change the element size: *1 for bytes, *2 for words, *4 for dwords, and *8 for quadwords or pointer arrays. Nothing else in the loop changes, which is why the expression is worth memorising.

For an array of structs, keep a second register for the field offset instead of folding it into the scale, and you get the same indexing with a per-field displacement:

    ; struct Point { int x; int y; };  sizeof(Point) == 8
    ; points[i].y  =  [base + i*8 + 4]
    mov     eax, [rbx + rcx*8 + 4]      ; 8 = element size, 4 = offset of y

Summary

  • An address is written as [base + index*scale + disp]; every part is optional.
  • len means the address itself; [ ] means the value at that address.
  • Scale accepts only 1, 2, 4, or 8 — exactly the common element sizes.
  • Use RIP-relative addressing (default rel + [rel label]) for position-independent code.
  • Arrays are [base + index*elemsize]; struct fields add a constant displacement.

Next: Instructions — the full instruction set, grouped by what each instruction actually does.