The Workhorse Instruction

If you counted the instructions in every listing so far, mov would win by a landslide. It is the most common instruction in x86-64, and for good reason: since the CPU does its work in registers, almost everything begins with moving a value somewhere useful and ends with moving a result back. Learn to read mov fluently and most of a listing reads itself.

Despite the name, mov does not "move" in the sense of leaving the source empty. It copies. After mov rbp, rsp, both registers hold the same value. A better mental name is "copy," but the mnemonic is mov, so that is what we call it.

Remember the Intel rule from Chapter 1: destination first, source second. So mov dst, src means "copy src into dst."

The Three Things mov Connects

A mov always has a destination and a source, and each can (within limits) be one of three kinds of operand:

  • an immediate (a literal constant, like 42)
  • a register (like eax)
  • a memory location (like DWORD PTR [rdi])

The one combination the hardware does not allow is memory-to-memory: you cannot copy directly from one memory address to another in a single mov. To do that the compiler loads into a register first, then stores. Everything else is fair game, and the rest of this lesson walks through the common forms.

Immediate to Register

The simplest mov loads a constant into a register. You saw a wall of these when main set up arguments:

	mov	edi, 5

"Put the literal 5 into edi." This is how every numeric literal in your C++ gets into the CPU before anything can be done with it. There is no immediate-to-immediate or computation here; it is a pure load of a known value.

Register to Register

Copying one register into another is just as direct:

	mov	eax, edi

"Copy edi into eax." This is everywhere. A function that simply returns its argument often boils down to a single register-to-register mov to get the value into the return register eax, followed by ret. Recall from the previous lesson that when the destination is a 32-bit name like eax, this also zeroes the upper half of rax as a side effect.

Loads and Stores: Talking to Memory

The interesting movs are the ones that touch memory. A load reads from memory into a register; a store writes from a register out to memory. In Intel syntax a memory operand is written with square brackets, and the value in the brackets is an address.

Here is the clearest possible pair, a function that stores a value through a pointer and one that loads through a pointer:

void store(int* p, int value)
{
    *p = value;
}

int load(int* p)
{
    return *p;
}

At -O2, with no stack-frame noise in the way:

store(int*, int):
	mov	DWORD PTR [rdi], esi
	ret
load(int*):
	mov	eax, DWORD PTR [rdi]
	ret

This is worth studying closely, because these two lines are the essence of every memory access you will ever read.

In store, the pointer p arrived in rdi and the value in esi. The instruction mov DWORD PTR [rdi], esi means: "take the 4-byte value in esi and write it to the memory address held in rdi." The destination is [rdi] (a memory location), so this is a store. The brackets are the giveaway: rdi holds an address, and [rdi] is the memory at that address. That is precisely what *p = value means in C++.

In load, mov eax, DWORD PTR [rdi] reads the 4 bytes at the address in rdi into the register eax. The source is in brackets, so this is a load. That is return *p.

Notice the symmetry: brackets on the destination means you are writing to memory (store); brackets on the source means you are reading from memory (load). And DWORD PTR confirms the access is 4 bytes, matching int.

Why the Size Keyword Is Sometimes Required

When at least one operand is a register, the register's name already tells the CPU the size, so the size keyword is just helpful confirmation. But consider storing a literal into memory:

	mov	DWORD PTR -4[rbp], 0

Here neither operand is a register: the destination is memory and the source is the immediate 0. Without DWORD PTR, the assembler would have no way to know whether you mean to write 1, 2, 4, or 8 bytes of zeros. The size keyword resolves the ambiguity. This is exactly the case Chapter 1 mentioned: Intel states the size only when it is otherwise unknowable.

The Store-Then-Reload Pattern at -O0

Now you can fully decode the verbose -O0 style you keep seeing. Here is a function that copies its argument into a local and returns it:

int passthrough(int x)
{
    int y { x };
    return y;
}
passthrough(int):
	push	rbp
	mov	rbp, rsp
	mov	DWORD PTR -20[rbp], edi
	mov	eax, DWORD PTR -20[rbp]
	mov	DWORD PTR -4[rbp], eax
	mov	eax, DWORD PTR -4[rbp]
	pop	rbp
	ret

Read the four middle movs as a chain of loads and stores:

  1. mov DWORD PTR -20[rbp], edistore the argument x into a stack slot.
  2. mov eax, DWORD PTR -20[rbp]load it back into eax.
  3. mov DWORD PTR -4[rbp], eaxstore it into the slot for the local y.
  4. mov eax, DWORD PTR -4[rbp]load y back into eax to return it.

Every one of those is a mov between a register and a stack memory operand. This store-then-immediately-reload churn is the hallmark of unoptimized code: the compiler gives every variable a memory slot and shuttles values back and forth instead of keeping them in registers. At -O2 the whole thing collapses, because the optimizer realises y is just x and the value can stay in a register the entire time.

A Quick Word on movzx and movsx

You met movsx (sign-extend) and movzx (zero-extend) in the last lesson. They are members of the mov family for the case where source and destination are different sizes. A plain mov requires matching sizes; when you need to copy a smaller value into a larger register and fill the extra bits, you use movzx (fill with zeros, for unsigned) or movsx (fill with the sign bit, for signed). Read them exactly like mov, just with the widening built in.

Key Takeaways

  • mov dst, src copies src into dst; it does not blank the source. Destination comes first (Intel syntax).
  • Operands can be immediate, register, or memory; the one forbidden combination is memory-to-memory.
  • A load reads memory into a register (brackets on the source); a store writes a register to memory (brackets on the destination).
  • In Intel syntax, [rdi] is the memory at the address held in rdi; that is how C++ pointer dereferences appear.
  • A size keyword like DWORD PTR is required when no register operand pins down the size (e.g. immediate-to-memory).
  • Unoptimized code is full of store-then-reload mov chains through stack slots; the optimizer removes most of them.
  • movzx and movsx are mov variants that copy a smaller value into a larger register, zero- or sign-extending the difference.