Instructions
Instruction Categories
You do not need to memorise hundreds of mnemonics. You need to know which family an instruction belongs to, because the family tells you what operands it accepts and what side effects it has.
| Family | Purpose | Representative mnemonics |
|---|---|---|
| Data movement | Copy or convert bit patterns between places | mov, movzx, movsx, lea, xchg |
| Arithmetic | Integer maths and its flags | add, sub, imul, idiv, inc, dec, neg |
| Logic & shifts | Bit-level manipulation | and, or, xor, not, shl, shr, sar |
| Comparison | Set flags for a later decision | cmp, test |
| Control flow | Change which instruction runs next | jmp, jz/je, call, ret, loop |
| Stack | Push and pop from the run-time stack | push, pop, pushfq, popfq |
| System | Talk to the kernel or the CPU itself | syscall, int, cpuid, rdtsc |
| String | Bulk operations with rep | movsb, stosb, scasb + rep |
The Operand Rules
Whatever the family, the same three constraints apply — they come from how x86-64 encodes instructions, not from the assembler:
- At most one memory operand.
mov [a], [b]is illegal; go through a register. - Sizes must match.
mov eax, rbxis illegal; both sides must be 32 bits, or you use an extension instruction. - The destination cannot be an immediate. Constants are read-only, so
mov 5, eaxmakes no sense.
Where the destination is memory and the source is a constant, the assembler genuinely cannot infer the width, and you must supply it: mov qword [ptr], 0.
Moving Data
mov is the most frequent instruction in compiled code, and it is also the most misused, because it copies bits verbatim — it never converts anything.
mov rax, rbx ; register to register
mov eax, 7 ; immediate to register
mov [rsp+8], rax ; register to memory
mov rcx, [rsp+8] ; memory to register
Two rules trip everyone up. First, you cannot move memory to memory — one operand must be a register. Second, both operands must be the same size; shrinking or growing a value needs a dedicated instruction.
Widening Values: movzx and movsx
When a small value must be used as a larger one, the empty high bits need a decision: fill with zeros, or copy the sign bit? That decision is exactly the difference between the two instructions.
mov al, 0xFF ; al = 1111 1111
movzx eax, al ; zero-extend -> eax = 00000000 00000000 00000000 11111111 = 255
movsx eax, al ; sign-extend -> eax = 11111111 11111111 11111111 11111111 = -1
; 64-bit sign extension from a 32-bit source needs the 'd' variant:
movsxd rax, eax ; note: `movsx rax, eax` does NOT exist
This is the mechanism behind every C type conversion between char, short, and int. Getting it wrong is how a signed value silently becomes a huge positive one.
Other Movement Instructions
xchg rax, rbx ; swap two registers in one instruction
lea rax, [rbx + 8] ; compute an address (never reads memory)
push rax ; put a value on the stack (rsp -= 8)
pop rbx ; take a value off the stack (rsp += 8)
Logic and Shifts
Bitwise Instructions
These operate bit by bit and never carry: bit n of the result depends only on bit n of the operands.
and rax, 0x0F ; keep the low nibble, clear everything else
or rax, 0x80 ; force bit 7 on
xor rax, 0xFF ; invert the low byte
not rax ; invert every bit (one's complement)
xor eax, eax ; FASTEST way to set a register to zero
xor eax, eax deserves special mention. Because the same register is on both sides, the result is always zero regardless of the input, and the CPU recognises this pattern as a dependency-free zeroing idiom.
Shifts and Rotates
shl rax, 1 ; shift left 1 -> multiply by 2
shr rax, 1 ; shift right 1 -> unsigned divide by 2
sar rax, 1 ; arithmetic right shift -> SIGNED divide by 2
rol rax, 4 ; rotate left: the bits that fall out return on the right
The count may be a constant or the cl register. Knowing whether your data is signed determines whether you choose shr (fills with 0) or sar (copies the sign bit) — they agree only for non-negative values.
Compare and Test
These two instructions produce no useful result value — they exist to set the flags for a following conditional jump.
cmp rax, rbx ; computes rax - rbx, sets flags, THROWS AWAY the result
; ZF=1 if equal, SF/OF decide "less than" for signed values
test rax, rax ; computes rax AND rax, sets flags, discards the result
; ZF=1 only if rax is zero -> "is this register zero?"
The test reg, reg idiom is worth memorising: it is the cheapest way to ask "is this zero?" or "is the sign bit set?" without changing the value. Compilers emit it constantly.
Conditional Move
A branch can be expensive if the CPU mispredicts it. cmov avoids the branch entirely by moving a value only when a condition holds — no jump, no pipeline flush.
; rax = max(rax, rbx) with no branch at all
mov rcx, rax ; assume rax is the larger one
cmp rax, rbx ; compare
cmovl rcx, rbx ; if rax < rbx, take rbx instead
mov rax, rcx ; rax now holds the maximum
cmov is strictly better than a branch when the two outcomes are equally likely; it is worse when the branch is highly predictable, because it always does the extra work.
Summary
- x86-64 instructions fall into a few families: data movement, arithmetic, logic/shift, comparison, control flow, and system.
movnever converts; usemovzxto zero-extend andmovsx/movsxdto sign-extend.and/or/xor/notwork bit by bit;xor reg, regis the standard idiom for zero.cmpandtestset flags without producing a result — they feed conditional jumps.cmovreplaces branchy selection when prediction would be unreliable.
Next: Arithmetic — the instructions that actually compute, and the flags they set.