Skip to content

Lesson 2 of 9 · 22m

Reading x86-64 Without Panicking

Fifteen instructions cover ninety percent of real disassembly. Learn the vocabulary, the calling convention, and the stack frame — the pattern-matching does the rest.

You do not need the 5,000-page Intel manual. Compiled code uses a small vocabulary obsessively; a dozen instructions cover ninety percent of what a disassembler shows you. Learn the vocabulary, the calling convention, and the shape of a stack frame — then let pattern-matching carry the load.

The working vocabulary

mov  rax, [rdi+0x18]   ; load the value at address rdi+0x18
lea  rax, [rdi+0x18]   ; compute the address itself, no dereference
push rbp / pop rbp     ; save / restore on the stack
call 0x401040 / ret    ; call a function / return to caller
cmp  eax, 0x2a         ; set flags = eax - 0x2a, discard the result
test eax, eax          ; flags from eax AND eax — the classic null check
jz   / jnz / jl / jge  ; conditional jumps on those flags
jmp  0x401050          ; unconditional jump
xor  eax, eax          ; idiom: zero a register
nop                    ; do nothing — alignment padding and patch space

lea shows up constantly doing arithmetic in disguise — lea eax, [rdi+rdi*4] is x*5 wearing a trench coat. Compilers love it because it's a one-instruction multiply-add that doesn't touch flags.

The System V calling convention

On Linux x86-64, integer arguments go rdi, rsi, rdx, rcx, r8, r9 in order; the return value lands in rax. rbx, rbp, and r12–r15 are callee-saved — when a prologue pushes them, the function intends to use them and will restore them on the way out. This one fact lets you read any call site instantly: the register loads immediately before it are the arguments.

Anatomy of a function

push rbp
mov  rbp, rsp            ; establish frame pointer
sub  rsp, 0x30           ; reserve 48 bytes of locals
mov  [rbp-0x14], edi     ; first arg stored into a local
mov  [rbp-0x20], rsi     ; second arg (looks like a pointer)
...
mov  eax, [rbp-0x14]     ; compute return value into rax
leave                    ; = mov rsp,rbp ; pop rbp
ret

[rbp-0x14] is a local variable — Ghidra will name it local_14. Count the locals, watch which get written versus only read, and the function's working memory maps itself.

Two syntaxes, one machine

GDB defaults to AT&T order (mov %rdi,%rax — source first); Ghidra, IDA, and x64dbg show Intel order (mov rax,rdi — destination first). One-time fix: set disassembly-flavor intel, preferably in ~/.gdbinit so you never see AT&T again.

Field notes

  • test eax,eax + jz is a zero/null check; cmp followed by jae/jb means the source compared unsigned, jge/jl means signed — the jump mnemonic preserves type information the decompiler will guess at.
  • Windows x64 uses a different convention: rcx, rdx, r8, r9, plus 32 bytes of caller-reserved shadow space. Know which world your binary lives in.
  • At -O2, frame pointers are usually omitted — locals become [rsp+...] and their offsets shift as rsp moves. Same concept, harder to track; the decompiler earns its keep here.