---
title: Addressing Modes
module: Instruction Set Architecture
moduleNumber: 2
lessonNumber: 3
order: 203
summary: >
  Once an operand field exists, it needs a rule for turning its bits into the data
  it names. That rule is the addressing mode. We walk the standard set — immediate,
  register, direct, register-indirect, displacement, scaled-indexed, and PC-relative
  — fixing the effective-address computation for each, run every mode against one
  concrete machine state, and price out what Y86-64 loses by keeping only base plus
  displacement.
topics: [Instruction Set Architecture]
sources:
  - book: Bryant & O'Hallaron
    ref: "CS:APP — §4.1 The Y86-64 Instruction Set Architecture"
  - book: Bistriceanu
    ref: "Computer Architecture Notes — §4 Addressing Modes"
---

An operand field is just a handful of bits. The **addressing mode** is the rule the
hardware applies to those bits to find the actual operand: sometimes the bits _are_
the value, sometimes they name a register holding it, and sometimes they describe a
calculation that produces a memory address. That calculation's result, the address
the instruction ultimately reaches, is the **effective address**. Addressing modes
exist because the shapes of data in real programs are varied: a loop constant, a
local variable, a struct field, an array element, a jump target. Each mode is the
cheap encoding of one of those shapes.

> **Definition (Effective address).** The memory address an operand finally refers
> to, after the addressing mode's computation is applied to the operand's bits and
> any registers it names.

We use AT&T/x86 syntax throughout, since it names every mode compactly and the
machine-level module already uses it.

## Modes that need no memory

The two simplest modes never touch memory at all.

**Immediate.** The operand bits _are_ the value. A constant is baked directly into
the instruction. In AT&T syntax a `$` marks it: `$0x100` is the literal `0x100`. No
address is computed; the value travels inside the instruction word. Its use is any
literal constant: loop bounds, initial values, masks.

**Register.** The operand names a register, and the value is that register's
contents. `%rax` _is_ the operand. Again no memory access: the value is already in
the datapath. This is the mode nearly every operand uses, since register machines
keep hot values in registers.

$$
% caption: Immediate and register modes need no memory. An immediate operand carries
% caption: the value inside the instruction; a register operand names a slot in the
% caption: register file whose contents are the value. Neither computes an address.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  box/.style={draw, minimum width=18mm, minimum height=8mm, align=center}]
  \definecolor{acc}{HTML}{2348F2}
  % immediate
  \node[box, fill=acc!8, text=acc, thick] (imm) at (0,0) {\texttt{{\char36}0x100}};
  \node[font=\scriptsize] at (0,0.95) {immediate};
  \node[anchor=west] at (1.4,0) {value $= \mathtt{0x100}$};
  % register
  \node[box] (reg) at (0,-2.0) {\texttt{\%rax}};
  \node[font=\scriptsize] at (0,-1.05) {register};
  \node[box] (rf) at (3.6,-2.0) {register\\f\/ile};
  \draw[->] (reg.east) -- (rf.west) node[midway, above, font=\scriptsize] {read};
  \node[anchor=west] at (5.0,-2.0) {value $=$ contents of \texttt{\%rax}};
\end{tikzpicture}
$$

## Modes that reach into memory

The remaining modes all compute an effective address and then read or write the
bytes there. They differ only in how the address is built.

**Direct (absolute).** The operand carries a full address outright. The effective
address is the constant in the instruction; the operand is the bytes at that fixed
location. This names a single, statically known object — a global variable at a
link-time address. It is encodable but rare, because a full address is a wide field
and most accesses are relative to something.

**Register-indirect.** A register holds the address. Written `(%rax)`, the
effective address is $\text{EA} = R[\texttt{rax}]$, and the operand is the bytes
there. This is pointer dereference: load the pointer into a register, then
dereference with `(%reg)`. The address is computed, but trivially — just read the
register.

$$
% caption: Register-indirect mode. The register holds an address; the effective
% caption: address is its contents, and the operand is the memory at that address.
% caption: This is pointer dereference — (%rax) reaches the bytes %rax points to.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  box/.style={draw, minimum width=18mm, minimum height=8mm, align=center}]
  \definecolor{acc}{HTML}{2348F2}
  \node[box] (reg) at (0,0) {\texttt{\%rax}};
  \node[font=\scriptsize] at (0,0.95) {register};
  \node[box, minimum width=14mm] (addr) at (3.0,0) {$\mathtt{0x2040}$};
  \node[box, fill=acc!8, text=acc, thick, minimum width=22mm] (cell) at (6.4,0)
        {operand bytes};
  \node[font=\footnotesize] at (6.4,0.95) {memory @ \texttt{0x2040}};
  \draw[->] (reg.east) -- (addr.west) node[midway, above, font=\scriptsize] {holds};
  \draw[->] (addr.east) -- (cell.west)
        node[midway, above, font=\scriptsize, text=acc] {EA};
\end{tikzpicture}
$$

**Displacement (base + offset).** A register plus a constant. Written `D(%rb)`, the
effective address is $\text{EA} = D + R[\texttt{rb}]$. This is the single most
useful memory mode, because it names "a fixed offset from a moving base": a field
inside a struct (`8(%rax)` for the field 8 bytes into the object `%rax` points to),
or a local variable at a known offset from the stack or frame pointer
(`-16(%rbp)`). Register-indirect is just the $D = 0$ case.

**Scaled-indexed (base + index × scale).** The full form, `D(%rb, %ri, s)`, with
effective address

$$
\text{EA} = D + R[\texttt{rb}] + R[\texttt{ri}] \cdot s, \qquad s \in \{1,2,4,8\}.
$$

This mode exists for **array indexing**. To reach `array[i]` with 8-byte elements,
put the base in `%rb`, the index `i` in `%ri`, and use scale 8: one instruction,
`base(%rb, %ri, 8)`, computes `base + i*8`. The scale set $\{1,2,4,8\}$ matches
the sizes of `char`, `short`, `int`, and `long`.

**PC-relative.** The operand is an offset added to the **program counter**. The
effective address is $\text{EA} = \text{PC} + D$, where the PC holds the address of
the _next_ instruction. Jumps and calls use it: a branch encodes "go this many bytes
forward or back from here," not an absolute target. In real x86-64 bytes, the
conditional branch `je` with encoding `74 0e` sitting at address `0x100` occupies
two bytes, so the next instruction is at `0x102`, and the branch target is
$\mathtt{0x102} + \mathtt{0x0e} = \mathtt{0x110}$. The payoff is **position
independence**: because every internal reference is relative to the code itself,
the whole block can be loaded at any address and every branch still lands
correctly. Modern executables and shared libraries depend on this.

$$
% caption: PC-relative addressing in real bytes. The two-byte branch je (74 0e) at
% caption: address 0x100 ends at 0x102, and the target is next-PC plus the offset:
% caption: 0x102 + 0x0e = 0x110. Move the whole block anywhere in memory and the
% caption: branch still lands correctly, which is what makes code position-independent.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  by/.style={draw, minimum width=11mm, minimum height=8mm, inner sep=0pt}]
  \definecolor{acc}{HTML}{2348F2}
  \node[by, draw=acc, thick, text=acc] (b0) at (0,0)   {$\mathtt{74}$};
  \node[by, draw=acc, thick, text=acc] (b1) at (1.1,0) {$\mathtt{0e}$};
  \node[by] (b2) at (2.2,0) {};
  \node[by] (b3) at (3.3,0) {...};
  \node[by] (b4) at (4.4,0) {};
  \node[by, fill=acc!8] (tg) at (5.5,0) {};
  \node[anchor=south, font=\scriptsize] at (0,0.5)   {$\mathtt{0x100}$};
  \node[anchor=south, font=\scriptsize] at (2.2,0.5) {$\mathtt{0x102}$};
  \node[anchor=south, font=\scriptsize, text=acc] at (5.5,0.5) {$\mathtt{0x110}$};
  \node[anchor=north, font=\scriptsize, text=acc] at (0.55,-0.5)
        {\texttt{je} $+\mathtt{0x0e}$};
  \node[anchor=north, font=\scriptsize] at (2.2,-0.5) {next PC};
  \node[anchor=north, font=\scriptsize, text=acc] at (5.5,-0.5) {target};
  \draw[->, acc] (2.2,0.85) .. controls (3.2,1.7) and (4.5,1.7) .. (5.5,0.85)
        node[midway, above, font=\scriptsize] {$+\ \mathtt{0x0e}$};
\end{tikzpicture}
$$

## One machine state, every mode

To fix the arithmetic, run every mode against the same
state. Suppose an array of 8-byte elements starts at address `0x2000`, with
`a[0]` = `0x11`, `a[3]` = `0x44`, and `a[7]` = `0x88`; the registers hold
`%rbx` = `0x2000` (the base) and `%rsi` = `3` (an index). Element `a[i]` lives at
$\mathtt{0x2000} + 8i$, so `a[3]` is at `0x2018` and `a[7]` at `0x2038`. Now read
each row and check the arithmetic.

| Instruction | Mode | Effective address | `%rax` gets |
| --- | --- | --- | --- |
| `movq $0x2038, %rax` | immediate | — | `0x2038` |
| `movq %rbx, %rax` | register | — | `0x2000` |
| `movq 0x2038, %rax` | direct | `0x2038` | `a[7]` = `0x88` |
| `movq (%rbx), %rax` | register-indirect | `0x2000` | `a[0]` = `0x11` |
| `movq 0x18(%rbx), %rax` | displacement | `0x2018` | `a[3]` = `0x44` |
| `movq (%rbx,%rsi,8), %rax` | scaled-indexed | `0x2018` | `a[3]` = `0x44` |
| `movq 0x10(%rbx,%rsi,8), %rax` | scaled-indexed | `0x2038` | `a[7]` = `0x88` |

Two rows deserve a second look. The displacement row and the first scaled-indexed
row reach the _same_ cell two different ways: `0x18(%rbx)` bakes the offset
$3 \times 8 = \mathtt{0x18}$ into the instruction as a constant, which works only
because the index is known at compile time, while `(%rbx,%rsi,8)` computes it from
`%rsi` at run time and keeps working when `i` changes. That is the whole reason
scaled-indexed exists: it is displacement mode with the offset promoted from a
constant to a variable. The last row exercises every field at once.

$$
% caption: The full effective-address computation for 0x10(%rbx,%rsi,8) with
% caption: %rbx = 0x2000 and %rsi = 3. The index is scaled (3 x 8 = 0x18), added to
% caption: the base and displacement (0x10 + 0x2000 + 0x18 = 0x2038), and the sum
% caption: is the address sent to memory, which returns a[7] = 0x88.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  t/.style={draw, minimum width=17mm, minimum height=8mm, align=center},
  opn/.style={draw, circle, inner sep=1.5pt}]
  \definecolor{acc}{HTML}{2348F2}
  \node[t] (d) at (0,1.6)  {$\mathtt{0x10}$};
  \node[t] (b) at (0,0.35)   {$\mathtt{0x2000}$};
  \node[t] (i) at (0,-1.2) {$\mathtt{3}$};
  \node[t] (s) at (0,-2.45) {$\mathtt{8}$};
  \node[anchor=east, font=\scriptsize] at (-1.0,1.6)  {disp};
  \node[anchor=east, font=\footnotesize] at (-1.0,0.35)   {base \texttt{\%rbx}};
  \node[anchor=east, font=\footnotesize] at (-1.0,-1.2) {index \texttt{\%rsi}};
  \node[anchor=east, font=\scriptsize] at (-1.0,-2.45) {scale};
  \node[opn] (mul) at (2.6,-1.85) {x};
  \node[opn] (add) at (4.6,0.35) {$+$};
  \node[t, draw=acc, thick, text=acc] (ea) at (6.8,0.35) {$\mathtt{0x2038}$};
  \node[t, fill=acc!8, minimum width=26mm] (mem) at (10.2,0.35) {$a[7] = \mathtt{0x88}$};
  \node[anchor=south, font=\scriptsize, text=acc] at (6.8,0.85) {EA};
  \node[anchor=south, font=\scriptsize] at (10.2,0.85) {memory};
  \draw[->] (i.east) -- (mul);
  \draw[->] (s.east) -- (mul);
  \draw[->] (mul) -- (add) node[midway, right=1mm, font=\scriptsize] {$\mathtt{0x18}$};
  \draw[->] (d.east) -- (add);
  \draw[->] (b.east) -- (add);
  \draw[->] (ea) -- (mem);
  \draw[->] (add) -- (ea);
\end{tikzpicture}
$$

## A mode per data shape: one struct, walked

Each mode matches a data shape, which shows when a compiler picks one per access
in ordinary C. Take an array of structs:

```c [record.c]
struct rec { long id; long score; };
struct rec table[100];

long lookup(long i) {
  return table[i].score;
}
```

Reaching `table[i].score` is one effective-address computation using the richest
mode. Each `struct rec` is 16 bytes; `score` is the second field, at offset 8. So

$$
\text{EA} = \underbrace{\texttt{\&table}}_{R[\texttt{rbx}]} +
            \underbrace{i}_{R[\texttt{rsi}]} \cdot \underbrace{16}_{\text{stride}} +
            \underbrace{8}_{\text{field offset}},
$$

which is `8(%rbx, %rsi, 16)` with the base in `%rbx` and the index `i` in `%rsi`.
One instruction, `movq 8(%rbx,%rsi,16), %rax`, loads the field. Each field of the
mode does a named job: scale = struct stride, index = subscript, displacement =
field offset. This is why the general form has all four components — real data is
exactly this shape, an array of fixed-size records with fields at known offsets.

$$
% caption: table[i].score compiled to one scaled-indexed access. The struct stride
% caption: (16 bytes) is the scale, the subscript i is the index, and the field
% caption: offset (8 bytes, score is the second long) is the displacement, so
% caption: 8(%rbx,%rsi,16) with %rbx = &table, %rsi = i reaches base + 16i + 8.
\begin{tikzpicture}[font=\footnotesize, >=stealth,
  slot/.style={draw, minimum width=20mm, minimum height=7mm, inner sep=1pt,
               align=center, font=\scriptsize}]
  \definecolor{acc}{HTML}{2348F2}
  % three consecutive records
  \foreach \k/\x in {0/0, 1/2.3, 2/4.6} {
    \node[slot] (id\k) at (\x,0)      {id};
    \node[slot] (sc\k) at (\x,-0.75)  {score};
    \node[anchor=south, font=\scriptsize, text=black] at (\x,0.4)
          {\texttt{table[\k]}};
  }
  % highlight the accessed field: table[i], i = 1 say
  \node[slot, fill=acc!8, draw=acc, thick, text=acc] at (2.3,-0.75) {score};
  % offset annotations
  \draw[->, acc] (2.3,-1.9) -- (2.3,-1.15)
        node[midway, right, font=\scriptsize] {$+8$ (f\/ield o\/f\/fset)};
  \draw[<->, black] (-1.15,0) -- (-1.15,-0.75)
        node[midway, left, font=\scriptsize, align=right] {16 B\\stride};
  \node[anchor=west, text=acc, font=\scriptsize] at (0.0,-2.6)
        {EA $=$ \texttt{\&table} $+ 16i + 8$};
\end{tikzpicture}
$$

The richer the mode, the more the single instruction carries. In Y86-64, which has
only displacement, that same `table[i].score` load cannot be one instruction — the
`i*16` has to be built by hand first, then the field offset folded into the
displacement:

```asm [record.ys]
rrmovq %rsi, %rax       # i
addq %rax, %rax         # 2i
addq %rax, %rax         # 4i
addq %rax, %rax         # 8i
addq %rax, %rax         # 16i
addq %rbx, %rax         # &table + 16i  (base of table[i])
mrmovq 8(%rax), %rdx    # load .score: displacement 8 does the field offset
```

Y86-64's single memory mode still helps on the last line: the
field offset `+8` is absorbed into the displacement, because that part _is_ a
constant. It is only the variable `i*16` that costs the four doublings and an add.
The pattern generalizes to the rule from the previous lesson — constant offsets are
cheap in any displacement mode, but a runtime-scaled index needs the
scaled-indexed mode that Y86-64 lacks.

## The set at a glance

The modes form a tidy progression: from operands that need no address, through one
register, to a register and a constant, to the full base-plus-scaled-index, with
PC-relative as the special case anchored on the program counter.

| Mode | Syntax | Effective address | Used for |
| --- | --- | --- | --- |
| Immediate | `$Imm` | — (value is `Imm`) | literal constants |
| Register | `%ra` | — (value is `R[ra]`) | values in registers |
| Direct | `Imm` | `Imm` | a fixed global |
| Register-indirect | `(%rb)` | `R[rb]` | pointer dereference |
| Displacement | `D(%rb)` | `D + R[rb]` | struct field, local var |
| Scaled-indexed | `D(%rb,%ri,s)` | `D + R[rb] + R[ri]·s` | array element |
| PC-relative | label | `PC + D` | jumps, calls, PIC |

## What Y86-64 drops, and the bill

The Y86-64 ISA of the
[next lesson](/computer-architecture/instruction-set-architecture/the-y86-64-instruction-set)
keeps immediate, register, and displacement — and nothing else. Its only memory
form is `D(%rb)`, with register-indirect falling out free as the $D = 0$ case.
Direct mode is gone (a global's address must first be loaded into a register with
`irmovq`), scaled-indexed is gone, and even jumps give up PC-relative: a Y86-64
`jXX` carries its full 8-byte absolute destination, so a conditional branch costs
9 bytes where x86-64's `je` above cost 2, and Y86-64 code is not
position-independent.

The scaled-index cut costs the most. Reaching `a[%rcx]` from a base in `%rdx` is one
instruction in x86-64, but Y86-64 must build $8i$ by hand — and since its
arithmetic family has no multiply and no shift, the only way to scale is repeated
doubling.

```asm [indexing.s]
movq (%rdx,%rcx,8), %rax    # x86-64: a[i] in one instruction
```

```asm [indexing.ys]
rrmovq %rcx, %rax           # y86-64: i
addq %rax, %rax             # 2i
addq %rax, %rax             # 4i
addq %rax, %rax             # 8i
addq %rdx, %rax             # base + 8i
mrmovq (%rax), %rax         # load a[i]
```

Six instructions and a scratch register against one instruction. In a loop the
honest fix is not to compute `8i` at all: keep a pointer to the current element
and add 8 each iteration, so every access is plain `(%reg)`. Compilers call this
**strength reduction**, and the
[array-sum program](/computer-architecture/instruction-set-architecture/y86-64-programming)
in this module's last lesson is built around exactly that pattern. The trade is
the same one this module keeps meeting: x86-64 spends encoder richness so common
patterns cost one instruction; Y86-64 spends instructions to keep its formats few
enough to implement by hand.

## lea, RIP-relative, and the RISC minimum

CS:APP introduces addressing modes on the way to x86-64 machine code. Three points
about how they play out in modern practice round out the picture.

**`lea` turns the address unit into an adder.** x86-64's `leaq` (load effective
address) computes the effective address but does _not_ dereference it — it returns
the address itself. Since the scaled-indexed mode computes $D + R[\texttt{rb}] +
R[\texttt{ri}] \cdot s$ in one instruction, compilers use `leaq` as a three-input
arithmetic instruction that never touches memory:

$$
\texttt{leaq (\%rdi,\%rsi,4), \%rax} \;\Rightarrow\; R[\texttt{rax}] = R[\texttt{rdi}] + 4\,R[\texttt{rsi}],
$$
$$
\texttt{leaq 7(\%rdi,\%rdi,2), \%rax} \;\Rightarrow\; R[\texttt{rax}] = 3\,R[\texttt{rdi}] + 7.
$$

The address-generation hardware, built for pointer math, is reused for integer math
the ISA has no cheaper way to express — an ISA feature's designed use and its actual
use diverging.

**RIP-relative addressing made PC-relative general.** Classic x86 used PC-relative
only for branches. x86-64 added **RIP-relative** data addressing —
`movq sym(%rip), %rax` — so that a global variable is reached as an offset from the
instruction pointer rather than an absolute address.[^ripc] The payoff is the
position-independence this lesson framed for branches, now extended to _data_: a
shared library's code and its global references can be loaded at any address, which
is what makes modern **position-independent executables** (the default hardening on
current systems) possible. The mode this lesson called a special case for jumps
turned out to be the foundation of how every shared library is addressed.

**The RISC minimum.** Where x86 accumulated modes, RISC ISAs deliberately keep the
smallest set that still compiles well: essentially register, immediate, and
base-plus-displacement — the same three Y86-64 keeps. ARM64 and RISC-V have _no_
memory-to-memory anything and no direct absolute mode; RISC-V's only memory form is
`disp(reg)`, exactly Y86-64's, and it too builds array indices with explicit
arithmetic or a walking pointer.[^rvaddr] The lesson's account of what Y86-64 gives
up is not a teaching simplification that real machines avoid — it is the mainstream
RISC design, and the strength-reduced walking pointer is what those compilers emit in
practice. Y86-64's addressing austerity is closer to ARM64 than x86-64 is.

> **Takeaway.** An addressing mode is the rule turning an operand's bits into its
> data. Immediate and register modes need no memory; the memory modes build an
> **effective address** of increasing richness — direct (a constant), register-
> indirect (a register), displacement (register + constant, for fields and locals),
> and scaled-indexed (`D + base + index·scale`, for arrays) — while PC-relative
> anchors the address on the program counter to make code position-independent.
> Y86-64 keeps only displacement and pays for arrays in extra instructions, a cost
> loops dodge by strength-reducing the index to a walking pointer — the same austere
> set ARM64 and RISC-V ship in production.

[^ripc]: **RIP-relative addressing** is x86-64's `disp(%rip)` mode, in which a data
    reference is computed as an offset from the address of the next instruction; it
    is the mechanism behind position-independent code and executables. See Intel,
    _Intel 64 and IA-32 Architectures Software Developer's Manual_, Vol. 1, §3.7.5.1,
    and the System V x86-64 ABI on PIC. The general idea — PC-relative addressing for
    position independence — is CS:APP §3.6/§7.
[^rvaddr]: **A. Waterman and K. Asanović, eds.**, _The RISC-V Instruction Set Manual,
    Volume I_. RISC-V's load/store instructions use a single base-plus-12-bit-
    displacement addressing mode; there is no scaled-index or absolute mode, so array
    indexing is done with explicit `slli`/`add` or a pointer walked across the array —
    the strength-reduction pattern this module builds its array-sum program around.
