x86 Assembly Fundamentals

A from-scratch introduction to x86 and x86-64 assembly, written for readers who already know a RISC ISA (this one assumes some old ARM/AArch32 knowledge) and want to get productive reading x86 quickly — specifically for reverse engineering compiled binaries (games, in particular) rather than for writing assembly by hand. Where it helps, each concept is anchored to the closest ARM equivalent instead of introduced from zero.

x86 vs x86-64, and why this series uses Intel syntax

x86 is a CISC (complex instruction set) architecture, the opposite design philosophy from ARM’s RISC: instructions are variable-length (1–15 bytes), can address memory directly as an operand to an arithmetic instruction (ARM forces you through explicit load/store), and the instruction set has accreted extensions for four decades (x87 FPU, MMX, SSE/SSE2/…/AVX-512). You do not need to know all of it — 95% of what you’ll read in a disassembler is a fairly small core instruction set plus SSE for floating point.

x86-64 (a.k.a. AMD64, x64, IA-32e) is the 64-bit extension introduced by AMD in 2003. Almost everything reversed today is x86-64; plain 32-bit x86 still shows up in older or indie titles and in DRM/packers. The two differ mainly in register width and count, and in calling convention (covered in x86 Instructions and the Stack) — the instruction mnemonics themselves are almost identical.

There are two assembly syntaxes for the same instructions:

Intel syntaxAT&T syntax
Operand orderdst, srcsrc, dst
Register prefixnone%
Immediate prefixnone$
Memory operand[rax+8]8(%rax)
Examplemov eax, [rbx+4]mov 4(%rbx), %eax

Ghidra, IDA, x64dbg, and Cheat Engine’s disassembler all default to Intel syntax, and that’s what this whole series uses. objdump/GDB on Linux default to AT&T unless told otherwise (objdump -M intel, GDB’s set disassembly-flavor intel) — worth knowing so you’re not confused the first time you see the operands in the “wrong” order.

General-purpose registers

x86-64 has 16 general-purpose registers, each 64 bits wide. This is the single biggest structural difference from ARM to internalize: ARM gives you 31 uniformly-named Xn/Wn registers; x86-64 gives you 16, several of which carry legacy, semantically-loaded names and sub-register aliasing — writing to a narrower slice of a register doesn’t clear the rest of it the way you might expect.

64-bit32-bit16-bit8-bitLegacy role (still relevant)
raxeaxaxalAccumulator; return value
rbxebxbxblCallee-saved general purpose
rcxecxcxclLoop counter; 4th arg (SysV) / 1st arg (Win64)
rdxedxdxdl3rd arg (SysV) / 2nd arg (Win64); high bits of mul/div
rsiesisisil2nd arg (SysV); string-instruction source
rdiedididil1st arg (SysV); string-instruction dest
rbpebpbpbplFrame/base pointer (often omitted, see below)
rspespspsplStack pointer
r8–r15r8d–r15dr8w–r15wr8b–r15bExtra registers added in x86-64, no legacy role

There is no ARM-style “any register can hold any argument” uniformity — argument passing is register-specific (see x86 Instructions and the Stack), so learning which register conventionally means what pays off immediately when reading a disassembled call.

Sub-register aliasing, the sharpest gotcha coming from ARM (where w0 is simply the bottom 32 bits of x0 with no surprises):

  • Writing a 32-bit register (mov eax, ...) zero-extends into the full 64-bit register — the top 32 bits of rax become zero. This is an explicit x86-64 rule, unlike 16-bit and 8-bit writes.
  • Writing a 16-bit (mov ax, ...) or 8-bit (mov al, ...) register preserves the untouched upper bits of the containing register. This is a frequent source of bugs when hand-writing asm and a frequent source of confusion when reading it — seeing mov al, 5 right after mov rax, rbx means only the bottom byte of rax changed.
  • ah/bh/ch/dh address the second byte (bits 8–15) of ax/bx/cx/dx — an x86-32 legacy encoding quirk with no equivalent in the r8–r15 family, which have no high-byte alias at all.

Instruction pointer and flags register

  • rip (32-bit: eip) is the instruction pointer — ARM’s pc, but on x86-64 it is not a general-purpose register you can freely move data into; it can only be read implicitly (call, RIP-relative addressing) or written via control-flow instructions (jmp, call, ret).
  • rflags (32-bit: eflags) is a status register set implicitly as a side effect of arithmetic and compare instructions — the rough analogue of ARM’s NZCV condition flags in cpsr, but with more bits and no equivalent to ARM’s explicit S suffix (on x86, whether flags are set is a property of the instruction, not an optional flag). The flags that matter for reading disassembly:
FlagBitMeaningSet by
ZFZeroResult was 0arithmetic, cmp, test
SFSignResult’s MSB was 1 (negative, signed)arithmetic, cmp, test
CFCarryUnsigned overflow/borrowarithmetic, shifts
OFOverflowSigned overflowarithmetic
PFParityLow byte of result has even # of 1 bitsrarely used for control flow

Conditional jumps (je, jg, jb, …) test combinations of these flags rather than a separate condition-code field the way ARM’s cond suffix does — the mapping from mnemonic to flag combination is covered in x86 Instructions and the Stack.

Addressing modes

x86’s memory operand syntax packs base register, index register, scale, and displacement into one bracketed expression — the ARM equivalent of writing out [base, index, LSL #scale] addressing but usable directly as an operand to almost any instruction, not just loads/stores:

[base + index*scale + displacement]
  • scale is restricted to 1, 2, 4, or 8 (matching byte/word/dword/qword element sizes — this is what makes it useful for array indexing without a separate shift instruction).
  • Any of the four components can be omitted. Common forms you’ll actually see:
FormExampleMeaning
Directmov eax, [0x140003000]Absolute address (rare with ASLR; more common as RIP-relative in practice)
Register indirectmov eax, [rbx]*rbx
Base + displacementmov eax, [rbx+8]Struct field access, rbx->field_at_offset_8
Base + index*scalemov eax, [rbx+rcx*4]Array indexing, int_array[rcx]
Full formmov eax, [rbx+rcx*4+0x10]Struct member that is itself an array, or array-of-structs indexing
RIP-relativelea rax, [rip+0x2f13]PIE/ASLR-safe access to a global/string constant

RIP-relative addressing is worth calling out specifically: since Windows and Linux both load executables at randomized base addresses (ASLR), compilers emit references to globals and string literals as an offset from the current instruction, not an absolute address. In a disassembler this displacement is usually already resolved and annotated with the symbol/string it points at — a fast way to find code of interest (see x86 Reverse Engineering for Games).

Data sizes and suffixes

Because a bare register operand like [rbx] doesn’t tell the assembler how many bytes to move, ambiguous memory operands are annotated with a size prefix:

KeywordBytesBits
byte18
word216
dword432
qword864

e.g. mov dword [rbx], 1 unambiguously stores a 4-byte immediate — necessary because mov [rbx], 1 alone doesn’t specify whether that’s a byte, word, dword, or qword write. When the other operand is a register (mov eax, [rbx]) the size is implied by the register and the prefix is normally omitted.

See also