Operands and Addressing Modes
Immediate, register, and memory operands, and the displacement[base + index*scale] addressing form.
What Goes After the Mnemonic
You now know several instructions and the rough shape of an operand. This lesson zooms in on the operands themselves: the three basic kinds, and the surprisingly powerful way x86-64 lets you describe a memory address. Addressing modes are where a lot of beginners stall, because the bracket notation looks cryptic. By the end of this lesson it will read like an arithmetic expression, which is exactly what it is.
The Three Kinds of Operand
Every operand you will meet is one of three things:
- Immediate — a literal constant baked into the instruction. In Intel syntax it is just a number: the
5inmov edi, 5or the100inimul eax, edi, 100. - Register — a register name:
eax,rdi,r8d. The value is read from (or written to) the register directly. - Memory — a value living in RAM, named by an address inside square brackets, like
[rdi]orDWORD PTR -4[rbp].
Immediate and register operands are simple: the value is right there. Memory operands are where the depth is, because the thing in the brackets is not a value but a recipe for computing an address. That recipe is the addressing mode.
The General Memory Operand
x86-64 can build a memory address out of as many as four pieces, all at once:
displacement[base + index*scale]
- base — a register holding a starting address (e.g. the start of an array, or
rbpfor the stack frame). - index — a register holding an element number or offset.
- scale — a constant multiplier on the index: 1, 2, 4, or 8. Nothing else is allowed.
- displacement — a constant offset added on (e.g.
-4).
The CPU computes base + index*scale + displacement and uses the result as the address to read or write. Every memory operand you have seen is just this general form with some pieces left out. [rdi] is base-only. -4[rbp] is base plus displacement. The full form appears when you index into an array.
Why these specific pieces, and why is scale limited to 1, 2, 4, or 8? Because those are the sizes of the common element types: 1 byte (char), 2 (short), 4 (int/float), 8 (long/double/pointer). The addressing mode is purpose-built to compute array + index * sizeof(element), which is exactly what indexing needs.
Watching All the Pieces Appear
Here is the canonical example, indexing into an array of int:
int elementAt(int* arr, int i)
{
return arr[i];
}
At -O2:
elementAt(int*, int):
movsx rsi, esi
mov eax, DWORD PTR [rdi+rsi*4]
ret
The pointer arr is in rdi, the index i in esi. The first instruction, movsx rsi, esi, sign-extends the 32-bit index up to a full 64-bit register so it can take part in address arithmetic (addresses are 64-bit). Then the payoff:
mov eax, DWORD PTR [rdi+rsi*4]
The memory operand [rdi+rsi*4] is the addressing mode doing real work. Read it as an expression: rdi + rsi * 4. That is arr + i * 4, and since an int is 4 bytes, it is the address of arr[i]. The *4 is the scale, automatically matching sizeof(int). The mov then loads the 4 bytes at that address into eax. The entire arr[i] indexing operation, address computation and load, happens in this one instruction.
This is the single most important addressing mode to recognise. Whenever you see [reg + reg*4] (or *8, *2, *1), think "array indexing," and the scale tells you the element size: *4 is an int array, *8 is a long/pointer/double array, and so on.
The Verbose Version
It is illuminating to see what the same indexing looks like at -O0, where the compiler spells the address calculation out step by step instead of folding it into one operand:
elementAt(int*, int):
push rbp
mov rbp, rsp
mov QWORD PTR -8[rbp], rdi
mov DWORD PTR -12[rbp], esi
mov eax, DWORD PTR -12[rbp]
cdqe
lea rdx, 0[0+rax*4]
mov rax, QWORD PTR -8[rbp]
add rax, rdx
mov eax, DWORD PTR [rax]
pop rbp
ret
Find the pieces of arr + i*4 scattered across several instructions. The index is loaded and widened (cdqe), then lea rdx, 0[0+rax*4] computes i * 4 into rdx (notice the scale *4 again), then mov rax, ... loads the base pointer arr, add rax, rdx forms arr + i*4, and finally mov eax, [rax] loads the element. The optimized version compressed all of that into the single operand [rdi+rsi*4]. The rich addressing mode is why the optimizer can be so terse: the hardware computes the whole address for free as part of the access.
You can also see displacement-only and base-only forms in this listing. QWORD PTR -8[rbp] and DWORD PTR -12[rbp] are base (rbp) plus displacement, the standard way local variables are named. [rax] is pure base. Same general form, different pieces present.
Reading Memory Operands as Expressions
The trick to never being confused by a memory operand is to mentally rewrite the Intel notation as ordinary arithmetic:
| Intel notation | Address computed | Typical meaning |
|---|---|---|
[rdi] |
rdi |
dereference a pointer |
-4[rbp] |
rbp - 4 |
a local variable |
[rdi+rsi] |
rdi + rsi |
base plus offset |
[rdi+rsi*4] |
rdi + rsi*4 |
int array element |
8[rdi+rsi*8] |
rdi + rsi*8 + 8 |
array element past a header |
Whatever appears in the brackets, just add up the pieces. The displacement (the number written before the bracket in GCC's output) is added; the base and scaled index are added. That sum is the address; the size keyword (DWORD PTR, QWORD PTR) tells you how many bytes are read or written there.
One caution worth repeating from Chapter 1: GCC writes the displacement before the bracket, as in -4[rbp], whereas you will often see the same thing written [rbp-4] elsewhere. They mean the identical address. Do not let the placement throw you.
Key Takeaways
- Operands come in three kinds: immediate (a literal), register, and memory (an address in brackets).
- A memory operand encodes an address recipe:
displacement[base + index*scale], computingbase + index*scale + displacement. - Scale must be 1, 2, 4, or 8, matching common element sizes; this makes the mode ideal for array indexing.
[rdi+rsi*4]is the signature ofint-array indexing: base pointer plus index times element size, loaded in one instruction.- At
-O0the compiler computes the address across several instructions; at-O2it folds the whole thing into one rich memory operand. - Read any bracketed operand by adding its pieces together; the size keyword says how many bytes are accessed.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Operands and Addressing Modes - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!