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:

  1. Immediate — a literal constant baked into the instruction. In Intel syntax it is just a number: the 5 in mov edi, 5 or the 100 in imul eax, edi, 100.
  2. Register — a register name: eax, rdi, r8d. The value is read from (or written to) the register directly.
  3. Memory — a value living in RAM, named by an address inside square brackets, like [rdi] or DWORD 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 rbp for 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], computing base + 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 of int-array indexing: base pointer plus index times element size, loaded in one instruction.
  • At -O0 the compiler computes the address across several instructions; at -O2 it 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.