How to Read x86-64 Registers and Instructions
From partial registers, operand sizes, MOV and LEA to machine-code encoding
How to Read x86-64 Registers and Instructions
These blog posts are written as lessons. Actual code can differ by processor generation, operating system, ABI, assembler, compiler, and build options, so always identify the target environment.
At first, rax, eax, ax, and al look like four registers, and mov seems to have dozens of unrelated forms. The useful skill is not memorizing every mnemonic. It is tracking what width is read, where the value comes from, where it goes, and which machine state changes.
This article uses Intel syntax to connect general-purpose and partial registers, operand sizes, mov, extension operations, lea, arithmetic, and basic machine-code encoding.
Keep three layers separate
C expression total += values[i];
Assembly notation add rax, qword ptr [rcx + rdx*8]
Machine bytes 48 03 04 D1
C expresses types and intent. Assembly is a human-readable representation of instructions and operands. Machine code is the byte stream decoded by the processor. There is no guaranteed one-to-one mapping between a source line and an instruction.
The general-purpose registers
x86-64 exposes sixteen 64-bit general-purpose registers: rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, and r8 through r15. rip tracks instruction execution. Some uses are architectural—stack instructions implicitly use rsp—while argument registers are selected by the platform ABI.
| Register | Common role |
|---|---|
rax | Integer return value and temporary arithmetic |
rcx, rdx | General values, shift count, or ABI arguments |
rsi, rdi | General values, string operations, or System V arguments |
rbp | General value or optional frame pointer |
rsp | Current top of the stack |
r8–r15 | Additional x86-64 general-purpose registers |
Partial names share one storage location
rax, eax, ax, and al name overlapping parts of the same register.
| Name | Portion accessed |
|---|---|
rax | All 64 bits |
eax | Low 32 bits |
ax | Low 16 bits |
al | Low 8 bits |
ah | Bits 8–15, the legacy high-byte name |
The critical rule is that writing a 32-bit general-purpose register in 64-bit mode clears the upper 32 bits.
mov rax, 0FFFFFFFFFFFFFFFFh
mov eax, 7
; RAX = 0000000000000007h
An 8- or 16-bit write leaves the remaining bits unchanged.
mov rax, 1122334455667788h
mov al, 0AAh
; RAX = 11223344556677AAh
That is why a compiler may zero eax before using setcc al. The legacy names ah, bh, ch, and dh cannot be encoded in an instruction that needs a REX prefix; avoiding them usually makes new code simpler.
Operand width is part of the operation
mov al, byte ptr [rcx] ; 1 byte
mov ax, word ptr [rcx] ; 2 bytes
mov eax, dword ptr [rcx] ; 4 bytes
mov rax, qword ptr [rcx] ; 8 bytes
The same address is used, but the access size differs. An incorrect width can read or overwrite an adjacent field, cross a page boundary, or violate a structure-layout contract. When the width is not implied, state it explicitly:
mov dword ptr [rcx], 1
mov copies; it does not consume the source
mov rax, rbx
mov rax, qword ptr [rcx]
mov qword ptr [rcx], rax
mov eax, 42
A normal mov does not clear its source. Most general mov forms also cannot copy directly from one memory operand to another.
; mov [rdi], [rsi] ; not a general MOV form
mov rax, [rsi]
mov [rdi], rax
Do not turn this into a universal claim that x86 instructions can touch only one memory location. String and specialized instructions exist, and read-modify-write operations have their own forms. Check the instruction reference.
Extension reveals signedness
The byte 0xFF can mean unsigned 255 or signed -1.
movzx eax, byte ptr [rcx] ; 000000FFh, 255
movsx eax, byte ptr [rcx] ; FFFFFFFFh, -1 in 32 bits
movsxd rax, eax ; sign-extend a signed 32-bit value to 64 bits
movzx fills new upper bits with zero; movsx copies the original sign bit. Assembly does not retain C type names, so extension, comparison, and branching patterns are evidence of signedness, not perfect source reconstruction.
lea calculates an address without reading memory
lea rax, [rcx + rdx*4 + 16]
rax receives the effective address. The memory stored at that address is not read.
lea rax, [rcx + 8] ; rax = rcx + 8
mov rax, [rcx + 8] ; rax = the eight-byte value stored there
Compilers also use lea for flag-preserving integer arithmetic.
lea eax, [rcx + rcx*2] ; eax = rcx * 3
The brackets borrow the addressing grammar; they do not force a memory access in lea.
Two-operand arithmetic overwrites its destination
add rax, rdx ; rax = rax + rdx
sub rcx, 8 ; rcx = rcx - 8
and eax, 0FFh ; eax = eax & 0xFF
xor edx, edx ; edx = 0, and therefore all of rdx is zero
These operations also update condition flags. lea normally does not. When a later jcc, setcc, or cmovcc appears, find the instruction that actually produced the flags it consumes.
Do not mix Intel and AT&T syntax
; Intel: destination first
mov rax, qword ptr [rbx + 8]
# AT&T: source first, % on registers, size suffix
movq 8(%rbx), %rax
GDB, objdump, GCC, and Clang can display either syntax. The CPU operation is the same, but reversing the operands reverses the data flow, so identify the syntax before reading a listing.
x86-64 instructions have variable length
An x86 instruction can occupy from one to fifteen bytes.
48 89 D8 mov rax, rbx
B8 2A 00 00 00 mov eax, 42
48 8B 44 D1 10 mov rax, [rcx + rdx*8 + 16]
An encoding may contain legacy, REX, VEX, or EVEX prefixes; an opcode; ModR/M; SIB; a displacement; and an immediate. You do not need to memorize the bytes. The structure matters when checking instruction boundaries, relocations, patches, or a disassembler that started one byte late.
Trace a complete tiny function
One possible compilation of int transform(int x) { return x * 3 + 5; } under the System V ABI is:
transform:
lea eax, [rdi + rdi*2]
add eax, 5
ret
edi contains the input and eax carries the integer return value. lea computes three times the input, add adds five, and the write to eax clears the upper half of rax. Do not assume that hardware wraparound proves equivalent C semantics for every input: signed C overflow is undefined behavior.
A practical reading checklist
For each instruction, ask:
- Is this Intel or AT&T syntax?
- What is the operand width?
- Which state is read and which state is overwritten?
- Does a 32-bit write clear an upper half?
- How is each effective address calculated?
- Does the instruction produce or consume flags?
- Does the ABI assign an argument, return, or preservation role?
Common mistakes
- Treating
eaxandraxas independent registers. - Assuming
mov al, 1makes all ofraxequal to one. - Believing
leadereferences its bracketed operand. - Reading
movlike C++ move semantics that empties a source object. - Inferring exact source types and intent from disassembly alone.
Exercise
Compile these functions with GCC or Clang using -O2 -S -masm=intel, or with MSVC using /O2 /FAs.
#include <stdint.h>
uint64_t widen_u8(const uint8_t* p) { return *p; }
int64_t widen_i8(const int8_t* p) { return *p; }
uintptr_t offset(const int* p, size_t i) {
return (uintptr_t)&p[i];
}
Find the zero/sign extension difference, determine whether offset reads memory or only computes an address, and note whether the return path writes eax or rax.
Summary
Reading x86-64 begins with state transitions. Partial registers overlap, a 32-bit write clears the upper half, mov copies, extension exposes integer interpretation, and lea computes an effective address without loading from it. The next article applies this model to arrays, structures, stack movement, and function frames.