This is a Deep Dive child of 5.1.3 Addressing modes . The parent gave you the E A formulas. Here we exhaust them — every mode, every degenerate case, every trap an exam can set. If a scenario exists, it lives in the matrix below and gets a fully worked example.
Before anything, five plain-word reminders (we will not use a symbol we have not re-earned):
Definition Notation we reuse
A ::: the address / displacement field — the raw number carried inside the instruction word.
R , ( R ) ::: a register name R , and the number currently stored in it ( R ) . The parentheses mean "contents of".
M [ x ] ::: the number stored in memory at address x . Think of memory as a long numbered street; M [ x ] is what's inside house number x .
E A ::: the effective address — the final house number the operand lives at, after the mode's rule runs.
P C ::: the program counter — the address the CPU is currently reading instructions from.
A "memory access" = one trip down the street to open one house. We count these because each trip costs time. See Memory Hierarchy for why trips are expensive.
Definition Assembly punctuation we use on this page
#value ::: the hash sign marks an immediate — the number after # is the operand, baked into the instruction word. No address, no memory trip.
(R) in an operand ::: "go to the address held in register R " (register-indirect).
A(R) ::: displacement — "compute A + ( R ) , then go there".
* before a target ::: the target is measured relative to the current P C , i.e. the assembler turns *+A into E A = P C + A .
Every worked example below is tagged with the cell it covers. The goal: no reader ever meets a case we skipped.
Cell
What makes it tricky
Covered by
C1 — zero memory access
operand never touches memory (immediate / register)
Ex 1
C2 — the "how many trips?" axis
direct (1) vs indirect (2) vs register-indirect (1)
Ex 2
C3 — sign of the displacement
A can be negative (backward offset)
Ex 3
C4 — degenerate / zero inputs
A = 0 , or ( R ) = 0 — do the rules still hold?
Ex 4
C5 — scaled indexing (arrays of wide elements)
element size = 1 byte
Ex 5
C6 — PC-relative, both directions
forward branch and backward loop
Ex 6
C7 — auto-increment / decrement ordering
when does R change relative to the access?
Ex 7
C8 — the stack (real-world word problem)
push/pop as auto-dec/auto-inc
Ex 8
C9 — exam twist: same bits, four modes
one instruction word decoded four ways
Ex 9
C10 — limiting behaviour / wrap-around
E A exceeds the address space
Ex 10
Shared machine state for all examples (memorise this street):
Address
Contents
Register
Contents
100
500
R1
400
200
108
R2
3
400
700
R3
408
403
900
Rz
0
404
111
PC
200
408
222
SP
600
500
800
Rb
596
596
42
600
55
608
333
MOVE #25, R3 (immediate) and ADD R1, R2 (register)
Recall from the punctuation box above: #25 means the number 25 is baked straight into the instruction word.
Forecast: how many memory trips does each make to get its operand? Guess before reading.
Immediate #25. Operand = 25 . Why this step? The # (defined above) says "the number is inside the instruction word itself." There is no E A at all — nothing to look up. Trips = 0 .
Register R1, R2. Operand = ( R 1 ) = 400 and ( R 2 ) = 3 . Why this step? Registers live inside the CPU , not out on the memory street (see Registers and Register File ). Reading a register is not a memory access. Trips = 0 .
Verify: the parent's table lists 0 memory accesses for both immediate and register modes. Both operands (25 and 400 ) were obtained without any M [ … ] appearing. ✓
Worked example Direct, memory-indirect, register-indirect on address 100
Forecast: three loads that all mention the number 100 or a register holding a pointer. Rank them by memory trips.
Direct LOAD 100. E A = A = 100 . Operand = M [ 100 ] = 500 . Why? The field is the address — one trip.
Memory-indirect LOAD (100). E A = M [ 100 ] = 500 , then operand = M [ 500 ] = 800 . Why two trips? First trip reads the pointer stored at 100 (which is 500); second trip reads the data that pointer points to. This is the extra cost the parent warned about.
Register-indirect LOAD (R1). ( R 1 ) = 400 , so E A = 400 , operand = M [ 400 ] = 700 . Why only one trip? The pointer already sits in a register — no memory trip needed to fetch it, only the final trip for the data.
Figure below: each coloured arrow is one memory trip. Follow the amber path (direct, one hop to house 100), the cyan path (indirect, hop to 100 to read the pointer 500, then a second hop to house 500), and the amber path on the right (register-indirect, the register already holds 400 so only one hop to house 400). Count the arrows and you count the trips.
Verify: operands are 500 , 800 , 700 ; trip counts 1 , 2 , 1 . Matches the parent table exactly. ✓
LOAD -4(R3) with ( R 3 ) = 408 . Displacement A = − 4 .
Here R 3 is used as a base pointer ; from the shared street table, ( R 3 ) = 408 .
Forecast: does E A move up or down the street? What's the operand?
E A = A + ( R 3 ) = ( − 4 ) + 408 = 404 . Why can A be negative? The displacement field is a signed number (see Instruction Format on field encoding). A negative offset walks backward from the base — exactly what you need for a struct field that sits before the pointer, or p[-1].
Operand = M [ 404 ] = 111 . Why? One memory trip at the computed E A .
Verify: − 4 + 408 = 404 and M [ 404 ] = 111 . The base minus 4 landed 4 houses lower — consistent with signed displacement arithmetic. ✓
Worked example What happens when
A = 0 or ( R ) = 0 ?
We use two registers from the shared table: R 1 (holds 400) and R z (holds 0 — a register whose contents happen to be zero; there is nothing special about it, it is just a normal register that currently stores 0).
Forecast: do the formulas break, or just collapse gracefully?
Displacement with A = 0 : LOAD 0(R1). E A = 0 + ( R 1 ) = 0 + 400 = 400 . Why does this matter? Displacement mode with zero offset degenerates into register-indirect — same result as LOAD (R1). So register-indirect is not a separate machine at all; it's the A = 0 corner of displacement.
Register holding 0: LOAD 100(Rz) with ( R z ) = 0 . E A = 100 + 0 = 100 , operand = M [ 100 ] = 500 . Why does this matter? Now the register contributes nothing and displacement degenerates into direct (E A = A ).
This is the deep lesson: the modes form a family , and zero inputs slide you between them.
Verify: case 1 gives E A = 400 , operand 700 (= register-indirect result from Ex 2). Case 2 gives E A = 100 , operand 500 (= direct result from Ex 2). Both collapses confirmed. ✓
int arr[] where each element is 4 bytes; read arr[3]
Base register R b = 596 (start of the array, from the shared table). Index register R 2 = 3 . Element size (scale) s = 4 .
Forecast: why can't we just do E A = 596 + 3 ?
Naïve (wrong): 596 + 3 = 599 . Why wrong? Element 3 is not 3 bytes along — it's 3 elements along, and each element is 4 bytes wide. Byte offset = index × scale.
Scaled index: E A = ( R b ) + ( R 2 ) ⋅ s = 596 + 3 ⋅ 4 = 596 + 12 = 608 . Operand = M [ 608 ] = 333 . Why multiply? The hardware's scaled-index mode bakes the × s in, so arr[i] compiles to a single instruction regardless of element width (see Pointers and Arrays ).
If instead we wanted element 0: E A = 596 + 0 ⋅ 4 = 596 , operand = M [ 596 ] = 42 . Why check element 0? It confirms the base is the address of the first element, offset zero.
Verify: 596 + 3 × 4 = 608 with M [ 608 ] = 333 , and 596 + 0 × 4 = 596 with M [ 596 ] = 42 . ✓
Worked example A forward branch and a backward loop,
P C = 200
Recall from the punctuation box: * before a target means "relative to the current P C ", so the assembler turns *+A into E A = P C + A .
Forecast: for a loop that jumps back , is the offset positive or negative?
Forward BRANCH +6: E A = P C + A = 200 + 6 = 206 . Why offset not absolute? Storing a distance makes the code relocatable — load it anywhere and P C + A still lands on the right instruction (see Instruction Set Architecture (ISA) ).
Backward BRANCH -8 (loop top): E A = P C + A = 200 + ( − 8 ) = 192 . Why negative? A loop must jump backward to repeat; the assembler encodes a negative signed offset.
Data via PC-relative LOAD *+8 (read a constant 8 past PC): the * marks it PC-relative, so E A = P C + 8 = 200 + 8 = 208 . Why? Same rule; position-independent constant pools use it too.
Figure below: the horizontal line is the address axis with P C = 200 marked in amber. Trace the cyan arrow + 6 forward to house 206 (a forward branch), the amber arrow − 8 backward to house 192 (a loop jumping back), and the white arrow + 8 forward to house 208 (a PC-relative data read). Every landing spot is just P C plus a signed distance.
Verify: 200 + 6 = 206 , 200 − 8 = 192 , 200 + 8 = 208 . Both directions land where predicted. ✓
LOAD (R1)+ then LOAD -(R1), tracking when R1 changes
Start ( R 1 ) = 400 , step d = 1 .
Forecast: does the register change before or after the memory access? It differs for + vs -.
Auto-increment (R1)+ — access first, then bump. E A = ( R 1 ) = 400 , operand = M [ 400 ] = 700 . Then R 1 ← 400 + 1 = 401 . Why this order? Post-increment: use the current pointer, then advance — perfect for *p++ streaming forward.
Auto-decrement -(R1) — bump first, then access. Now ( R 1 ) = 401 ; first R 1 ← 401 − 1 = 400 , then E A = ( R 1 ) = 400 , operand = M [ 400 ] = 700 . Why this order? Pre-decrement: back up before reading — this pairing (post-inc / pre-dec) is exactly what a stack needs (next example).
Verify: after both operations R 1 returns to 400 ; both reads see M [ 400 ] = 700 . Order confirmed. ✓
Worked example Push 77, then pop, using a stack that grows
downward
The stack pointer SP = 600 , word size d = 1 , stack grows toward lower addresses. "Grows down" means push uses pre-decrement , pop uses post-increment . See Stack and Subroutines .
Forecast: after PUSH 77 then POP, what value comes back and where does SP end?
PUSH 77 = auto-decrement store -(SP). First S P ← 600 − 1 = 599 , then M [ 599 ] ← 77 . Why pre-decrement? Make room first by moving the top pointer down, then write. If we wrote first we'd clobber the current top.
POP = auto-increment load (SP)+. E A = ( S P ) = 599 , operand = M [ 599 ] = 77 ; then S P ← 599 + 1 = 600 . Why post-increment? Read the top first , then free the slot by moving SP back up.
Net effect: we got 77 back and SP returned to 600. Why this proves correctness? A push followed immediately by a pop must be a no-op on SP and return the pushed value — it does.
Verify: pushed value 77 is recovered; SP starts and ends at 600 ; mid-sequence SP was 599 . ✓
Worked example Bits carry
A = 100 and name R 1 = ( 400 ) . Compute the operand under each interpretation.
Forecast: four modes, four answers, from the same number 100 / register 400.
Immediate: operand = A = 100 . (0 trips.) The number is the operand.
Direct: E A = 100 , operand = M [ 100 ] = 500 . (1 trip.)
Indirect: E A = M [ 100 ] = 500 , operand = M [ 500 ] = 800 . (2 trips.)
Displacement 100(R1): E A = 100 + 400 = 500 , operand = M [ 500 ] = 800 . (1 trip.)
Why this is the classic exam trap: modes 3 and 4 give the same operand 800 but for different reasons and different trip counts (2 vs 1). Never read the answer off the operand alone — always name the mode and count trips.
Verify: operands 100 , 500 , 800 , 800 with trips 0 , 1 , 2 , 1 . ✓
Worked example Displacement that overshoots a 3-bit (0–7) address space
Suppose a toy machine has only addresses 0 to 7 (address width 3 bits, so 2 3 = 8 houses). Compute LOAD 6(Rw) with ( R w ) = 5 .
Forecast: 6 + 5 = 11 , but there's no house 11. What does the hardware do?
Raw sum: E A raw = 6 + 5 = 11 . Why is this a problem? 11 > 7 ; it names a house that doesn't exist on this street.
Wrap (modulo the address space): real hardware keeps only the low 3 bits, i.e. E A = 11 mod 8 = 3 . Why modulo 2 n ? An n -bit adder simply drops the carry-out; the leftover is the remainder after dividing by 2 n . So overshoot silently wraps to the low end.
Sanity of the limit: at exactly E A raw = 8 we'd get 8 mod 8 = 0 — wrap to the very first house. Why check the boundary? 8 is the smallest overshoot; confirming it maps to 0 shows the wrap is continuous with no gap.
Verify: 11 mod 8 = 3 and 8 mod 8 = 0 . Wrap-around confirmed at both a generic and the boundary value. ✓
Recall Which cells did we cover?
All ten: zero-access (Ex1), trip-count axis (Ex2), negative displacement (Ex3), zero/degenerate collapse (Ex4), scaled index (Ex5), PC-relative both ways (Ex6), inc/dec ordering (Ex7), stack (Ex8), one-word-four-modes (Ex9), wrap-around (Ex10).
Every E A was a formula; every operand needed a trip count. Master those two habits.
Recall Quick self-test
LOAD (100) with M[100]=500, M[500]=800 — operand and trips? ::: operand 800, 2 memory trips (indirect).
LOAD 0(R1) with (R1)=400 — what mode does it degenerate to? ::: register-indirect; EA=400, operand M[400]=700.
Scaled arr[3], base 596, element size 4 — EA and operand? ::: EA = 596 + 3×4 = 608, operand M[608]=333.
Push onto a downward stack, SP=600 — which auto-mode and new SP? ::: pre-decrement -(SP); SP becomes 599.
LOAD 6(Rw), (Rw)=5, 3-bit address space — EA? ::: 11 mod 8 = 3.
The RISC-vs-CISC tension behind "how many modes should we even offer" lives in RISC vs CISC .