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 syntax | AT&T syntax | |
|---|---|---|
| Operand order | dst, src | src, dst |
| Register prefix | none | % |
| Immediate prefix | none | $ |
| Memory operand | [rax+8] | 8(%rax) |
| Example | mov 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-bit | 32-bit | 16-bit | 8-bit | Legacy role (still relevant) |
|---|---|---|---|---|
rax | eax | ax | al | Accumulator; return value |
rbx | ebx | bx | bl | Callee-saved general purpose |
rcx | ecx | cx | cl | Loop counter; 4th arg (SysV) / 1st arg (Win64) |
rdx | edx | dx | dl | 3rd arg (SysV) / 2nd arg (Win64); high bits of mul/div |
rsi | esi | si | sil | 2nd arg (SysV); string-instruction source |
rdi | edi | di | dil | 1st arg (SysV); string-instruction dest |
rbp | ebp | bp | bpl | Frame/base pointer (often omitted, see below) |
rsp | esp | sp | spl | Stack pointer |
r8–r15 | r8d–r15d | r8w–r15w | r8b–r15b | Extra 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 ofraxbecome 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 — seeingmov al, 5right aftermov rax, rbxmeans only the bottom byte ofraxchanged. ah/bh/ch/dhaddress the second byte (bits 8–15) ofax/bx/cx/dx— an x86-32 legacy encoding quirk with no equivalent in ther8–r15family, which have no high-byte alias at all.
Instruction pointer and flags register
rip(32-bit:eip) is the instruction pointer — ARM’spc, 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’sNZCVcondition flags incpsr, but with more bits and no equivalent to ARM’s explicitSsuffix (on x86, whether flags are set is a property of the instruction, not an optional flag). The flags that matter for reading disassembly:
| Flag | Bit | Meaning | Set by |
|---|---|---|---|
ZF | Zero | Result was 0 | arithmetic, cmp, test |
SF | Sign | Result’s MSB was 1 (negative, signed) | arithmetic, cmp, test |
CF | Carry | Unsigned overflow/borrow | arithmetic, shifts |
OF | Overflow | Signed overflow | arithmetic |
PF | Parity | Low byte of result has even # of 1 bits | rarely 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]
scaleis 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:
| Form | Example | Meaning |
|---|---|---|
| Direct | mov eax, [0x140003000] | Absolute address (rare with ASLR; more common as RIP-relative in practice) |
| Register indirect | mov eax, [rbx] | *rbx |
| Base + displacement | mov eax, [rbx+8] | Struct field access, rbx->field_at_offset_8 |
| Base + index*scale | mov eax, [rbx+rcx*4] | Array indexing, int_array[rcx] |
| Full form | mov eax, [rbx+rcx*4+0x10] | Struct member that is itself an array, or array-of-structs indexing |
| RIP-relative | lea 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:
| Keyword | Bytes | Bits |
|---|---|---|
byte | 1 | 8 |
word | 2 | 16 |
dword | 4 | 32 |
qword | 8 | 64 |
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
- x86 Instructions and the Stack — the actual instruction set, the stack, and calling conventions.
- Reading Compiled Code Patterns — how source-level constructs (if/loops/structs/vtables) look once compiled to the instructions and addressing modes introduced here.
- x86 Reverse Engineering for Games — applying all of this with a disassembler, plus a worked crackme walkthrough.
- Cheat Engine and Memory Scanning — memory scanning and a trainer-style memory-edit walkthrough.
- All Articles