Data Movement
Most instructions a program runs simply move data. We cover the mov family and its size suffixes, the three operand forms, the full memory addressing mode D(Rb,Ri,S) and its special cases, lea for address arithmetic, and how push and pop manipulate the stack pointer %rsp on a stack that grows toward lower addresses.
╌╌╌╌
The single most common thing a processor does is copy bytes from one place to
another — register to register, memory to register, register to memory. Before any
arithmetic can happen the operands must be in the right registers, and afterward
the result must be written back. This lesson covers the mov family that does
that copying, the operand forms that name where data lives, the full memory
addressing mode, the address-computing lea, and the stack operations
push and pop.
The three operand forms
Every operand an instruction reads or writes takes one of three forms, and learning to classify them at a glance is most of the battle in reading assembly.
- Immediate — a literal constant, written with a
$prefix, as in$0x1for$255. Immediates can only be sources, never destinations. - Register — the contents of one of the sixteen registers (at any of its four
widths), written with a
%prefix:%rax,%edi,%al. - Memory — the bytes at a computed address, written with the address in
parentheses:
(%rax)meansthe bytes at the address held in
%rax.
The one rule that constrains every move: at most one operand may be a memory reference. x86-64 has no instruction that copies memory directly to memory; such a copy is two instructions, through a register.
The mov family
The mov instruction copies the value of its source into its
destination, leaving the source unchanged — register to register, memory to
register, or register to memory. The size suffix fixes how many bytes are copied.
The basic copy instruction is mov, carrying a size suffix that fixes how many
bytes move. movb moves one byte, movw two, movl four, movq eight. The
source and destination widths must agree with the suffix.
movq $0x4050, %rax # immediate -> register (8 bytes)
movb %al, (%rdi) # register -> memory (1 byte)
movl (%rsi), %edx # memory -> register (4 bytes)
movw %ax, %bx # register -> register (2 bytes)
Two specialized moves handle width changes when copying a small value into a
larger register. movz zero-extends the source (fills the high bytes with
zeros) and movs sign-extends it (replicates the sign bit), each taking a
two-letter suffix naming the source then destination widths.
movzbq %al, %rbx # zero-extend byte -> quad
movsbl %al, %edx # sign-extend byte -> long
The full family is small and follows one naming scheme: mov then z or s,
then the source width, then the destination width. The source is always narrower
than the destination, so the reachable pairs are these.
| Instruction | Source → dest | Extension |
|---|---|---|
movzbw / movsbw | byte → word | zero / sign |
movzbl / movsbl | byte → long | zero / sign |
movzwl / movswl | word → long | zero / sign |
movzbq / movsbq | byte → quad | zero / sign |
movzwq / movswq | word → quad | zero / sign |
— / movslq | long → quad | sign only |
Two gaps in the table are worth naming. There is no movzlq for a
long-to-quad zero-extend, because a plain movl into the 32-bit destination
already zeroes the upper four bytes: movl %eax, %eax is the idiomatic 32-to-64
zero-extend, so a dedicated instruction would be redundant. Sign extension has no
such shortcut, so movslq (also spelled cltq when the operand is %eax) does
exist. Every other combination is a genuine two-width move that the narrow write
alone cannot express.
That movl shortcut is the upper-byte-zeroing rule from
the machine's view
put to work: any l-suffixed write clears the top four bytes, so the compiler
leans on it wherever an int-to-long zero-extend is needed.
The zero-versus-sign choice changes the numeric value. Take the
byte 0xFF sitting in %al. As an unsigned char that is 255; as a signed char
it is . Widening it to a quad must preserve whichever interpretation the C type
demanded, and the two mov variants do exactly that.
The low byte FF is the same in both rows; only the seven new bytes differ, and
that difference is the entire distinction between unsigned and signed widening.
This is why C's integer-promotion rules compile to one instruction or the other, and
why a stray movzbl where the code meant movsbl turns a small negative into a
large positive.
The full addressing mode
A memory operand specifies how to compute an address. The general form combines a constant displacement, a base register, an index register, and a scale factor, and the hardware evaluates the address by a single formula.
The scale's allowed values match the sizes of the primitive types — 1, 2, 4,
8 — which is no accident: indexing an array A[i] of -byte elements is
precisely base(,Ri,S), computing in one operand.
Most operands use only part of the form, and the special cases are worth memorizing because they are what you actually meet in compiled code.
| Form | Effective address | Name |
|---|---|---|
(%rax) | indirect | |
8(%rax) | base + displacement | |
(%rax,%rcx) | indexed | |
(%rax,%rcx,4) | scaled indexed | |
0x40(,%rcx,8) | scaled, no base |
A worked example fixes the arithmetic. Suppose %rdx holds 0x100 and %rcx
holds 3. Then 0x8(%rdx,%rcx,4) addresses
, and a movq from it
reads the 8 bytes starting there.
A worked memory access, end to end
To see the addressing mode drive an actual load, fix a small memory picture and run
one instruction against it. Suppose an array of 8-byte longs begins at 0x1000,
%rdx holds that base 0x1000, and %rcx holds the index 2. The instruction
movq (%rdx,%rcx,8), %rax should load the third element.
The processor evaluates the operand in two phases. First it computes the effective
address from the formula: . Then it reads the 8 bytes starting
at 0x1010 and copies them into %rax. If the long stored there is 0x2A
(decimal 42), %rax ends holding 0x2A; the base register %rdx and index %rcx
are untouched, since the address arithmetic happens in a hidden adder, not in them.
The two-phase reading — compute an address, then touch memory once — is the whole mental model for every memory operand. The next instruction stops after the first phase.
lea computes addresses
There is one instruction that uses the addressing-mode syntax but does not
touch memory: lea, load effective address.
Where a mov from D(Rb,Ri,S)
fetches the bytes at the computed address, lea computes the address and writes
the address itself into the destination register.1
# %rdi = x. Compute &A[i] style addresses without a memory access.
leaq 7(%rdi), %rax # rax = x + 7
leaq (%rdi,%rdi,2), %rax # rax = x + 2x = 3x
leaq 0(,%rdi,8), %rax # rax = 8x
Because the address formula is a sum of a constant, a register, and a
scaled register, lea doubles as a fast way to compute Imm + a + b·{1,2,4,8}
in a single instruction, often more cheaply than the equivalent add-and-multiply.
Compilers reach for it constantly for small multiplications and array-offset math;
we return to this trick in
arithmetic and logic.
The stack: push and pop
The run-time stack is a region of memory that grows toward lower addresses,
with %rsp always pointing at the top element — the lowest occupied address.
Two instructions maintain it. pushq S makes room by decrementing %rsp by 8 and
writes S to the new top; popq D reads the top into D and reclaims the space
by incrementing %rsp by 8.
Because the stack lives in ordinary memory, the top element is reachable as
(%rsp), the element below it as 8(%rsp), and so on. pushq %rbp is therefore
exactly equivalent to the pair subq $8, %rsp then movq %rbp, (%rsp), and
popq %rax to movq (%rsp), %rax then addq $8, %rsp. The single instructions
are shorter encodings of those pairs.
A numeric trace nails the pointer arithmetic. Say %rsp holds 0x7fffffffe018
and %rax holds 0x9. Executing pushq %rax does two things in order: it
subtracts 8 from %rsp, giving 0x7fffffffe010, then writes 0x9 to the memory
at that new address. A following pushq %rbx (with %rbx = 0x4) repeats the
pattern, lowering %rsp to 0x7fffffffe008 and storing 0x4 there. Now a
popq %rcx reads (%rsp) — the 0x4 just pushed — into %rcx and adds 8 back,
returning %rsp to 0x7fffffffe010. The stack is last-in, first-out precisely
because push and pop move %rsp in opposite directions by the same 8.
This same stack is where call saves its return address and where procedures keep
their local frames, the subject of
procedures. For now the
key facts are the direction of growth and the role of %rsp.
Wider moves and the red zone
The mov family CS:APP presents is the integer core, but the same processor moves
data in wider units the compiler reaches for constantly. The SSE and AVX extensions
add 128-, 256-, and 512-bit vector registers (%xmm0, %ymm0, %zmm0) and their
own move instructions: movdqa/movdqu copy 16 bytes aligned or unaligned,
vmovups copies 32. When gcc vectorizes a loop or inlines a small memcpy, the
assembly is full of these wide moves rather than a run of movqs, so recognizing
movdqu (%rsi), %xmm0 as load 16 bytes
is part of reading optimized output. These
sit in the same source lineage — Intel's own architecture manuals document them
alongside the integer moves — and follow the identical operand grammar, only wider.2
One System V detail CS:APP mentions only in passing is worth stating outright
because it surprises debugger users: the red zone. The ABI reserves the 128
bytes below %rsp as scratch space a leaf function (one that calls nothing) may
use without moving %rsp at all. A short leaf function can therefore keep locals at
-8(%rsp), -16(%rsp), and so on, emitting no subq $N, %rsp prologue, because it
is guaranteed nothing — not even an interrupt handler on the same stack — will
clobber that region. This is why some compiled leaf functions appear to write below the stack
and are still correct.3
Footnotes
- Bryant & O'Hallaron, CS:APP, §3.5.1 — Load Effective Address:
leaqcomputes the effective address of a memory operand and stores it, doubling as compact arithmetic. ↩ - Intel, Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 1 (2023), §10–§15 — the SSE/AVX/AVX-512 register files and their aligned/unaligned move instructions (
movdqa,movdqu,vmovups). ↩ - Matz, Hubička, Jaeger & Mitchell, System V Application Binary Interface, AMD64 Architecture Processor Supplement (v1.0, 2018), §3.2.2 — the 128-byte red zone below
%rspthat leaf functions may use without adjusting the stack pointer. ↩
╌╌ END ╌╌