Lesson 2.21,643 words

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 $0x1f or $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) means the bytes at the address held in %rax.
The three operand forms. An immediate is a literal carried in the instruction; a register names one of the sixteen slots; a memory operand is the bytes at an address. Only memory involves a trip to RAM.

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.

mov as a copy. The value in the source is duplicated into the destination; the source keeps its value. Here movq (%rsi), %rdx copies 8 bytes from the memory cell at %rsi into the register %rdx.

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.

movs.sassembly
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.

extend.sassembly
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.

InstructionSource → destExtension
movzbw / movsbwbyte → wordzero / sign
movzbl / movsblbyte → longzero / sign
movzwl / movswlword → longzero / sign
movzbq / movsbqbyte → quadzero / sign
movzwq / movswqword → quadzero / sign
— / movslqlong → quadsign 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.

Extending the byte 0xFF from %al to a quad. movzbq fills the new high bytes with zeros (value 255); movsbq replicates the sign bit 1 (value -1). The source bits are identical; the fill bytes decide the number.

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.

The general addressing mode D(Rb,Ri,S). The effective address is the displacement plus the base register plus the scaled index; the operand is the memory found there.

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.

FormEffective addressName
(%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.

Resolving 0x8(%rdx,%rcx,4) with %rdx=0x100 and %rcx=3. The three terms sum to the effective address 0x114; the operand is the quad word in memory at that address.

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.

movq (%rdx,%rcx,8), %rax with %rdx=0x1000 and %rcx=2. The effective address 0x1010 selects the third 8-byte element; its value 0x2A is copied into %rax while the base and index registers keep their values.

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

mov versus lea on the same operand. Both compute the address from the operand; mov then loads the bytes living there, while lea stops at the address and stores the number itself, never touching memory.
lea.sassembly
# %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.

pushq %rax decrements %rsp by 8 and stores the value at the new top; the stack grows downward, so the top sits at the lowest address. popq reverses this, reading the top and raising %rsp back.

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.

pushq %rax then pushq %rbx then popq %rcx, tracing %rsp (low bytes shown). Each push lowers %rsp by 8 and writes; the pop reads the top and raises %rsp by 8, so it returns the last value pushed (4) and restores the pointer.

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

  1. Bryant & O'Hallaron, CS:APP, §3.5.1 — Load Effective Address: leaq computes the effective address of a memory operand and stores it, doubling as compact arithmetic.
  2. 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).
  3. 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 %rsp that leaf functions may use without adjusting the stack pointer.

╌╌ END ╌╌