Addressing Modes
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 as | Effective address | Typical meaning |
|---|---|---|
[rbx] | rbx | First byte of a buffer |
[rbx + 8] | rbx + 8 | Second qword of a struct |
[rbx + rcx] | rbx + rcx | Byte array indexed by rcx |
[rbx + rcx*4] | rbx + 4·rcx | Dword array (int[]) — the classic |
[rbx + rcx*8 + 16] | rbx + 8·rcx + 16 | Field 16 of struct in a qword array |
[rbp - 4] | rbp − 4 | Local 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. lenmeans 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.