Moving Data with mov
The mov instruction in its three forms: register to register, immediate to register, and loads and stores to memory.
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:
mov DWORD PTR -20[rbp], edi— store the argumentxinto a stack slot.mov eax, DWORD PTR -20[rbp]— load it back intoeax.mov DWORD PTR -4[rbp], eax— store it into the slot for the localy.mov eax, DWORD PTR -4[rbp]— loadyback intoeaxto 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, srccopiessrcintodst; 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 inrdi; that is how C++ pointer dereferences appear. - A size keyword like
DWORD PTRis required when no register operand pins down the size (e.g. immediate-to-memory). - Unoptimized code is full of store-then-reload
movchains through stack slots; the optimizer removes most of them. movzxandmovsxaremovvariants that copy a smaller value into a larger register, zero- or sign-extending the difference.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Moving Data with mov - Quiz
Test your understanding of the lesson.
Practice Exercises
Follow a Copy Chain Through mov
Finish the relay function so the value passed in is copied through the chain of locals: first into second, then second into third. The program prints relay(42) and relay(7). Once your output is correct, read the assembly panel and trace the mov instructions that implement the chain: the immediate-to-register mov that loads 42 in main, and the load/store pairs (mov DWORD PTR -8[rbp], eax) that shuttle the value between stack slots at -O0.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!