x86-64 Summary

x86-64 Summary

Addressing Modes #

Operands of most operations may either be a register, an immediate value, or the contents of memory. A memory address in general is made of an immediate, two registers, and a scale on one of the the registers: imm + rA + rB * s where s is one of 1, 2, 4, or 8.

Two Syntaxes #

For mostly historical reasons, x86-64 has two different syntaxes.

FeatureIntelAT&T
Registerrsp%rsp
Immediate10$10
Register + immediate address[rsp + 23]23(%rsp)
Register + register * 4 + immediate address[rsp + r8 * 4 + 23]23(%rsp, %r8, 4)
a += badd rax, rbxaddq %rbx, %rax

AT&T syntax tends to be more explicit than Intel.

The width specifiers are:

BitsHistorical nameIntel nameAT&T suffixRegister names
8byteBYTEbah, al, r9b
16wordWORDwax, r9w
32double wordDWORDleax, r9d
64quad wordQWORDqrax, r9q

The most popular *nix toolchains default to AT&T syntax. The most popular Windows toolchains default to Intel syntax.

Registers #

The registers are a mess. Thanks, backwards compatibility.

8-bit16-bit32-bit64-bitCallingCallee-saveNotes
al, ahaxeaxraxreturnspecial meaning for multiply and divide instructions
cl, chcxecxrcxarg 4
dl, dhdxedxrdxarg 3special meaning for multiply and divide instructions
bl, bhbxebxrbxyes
splspesprspyesstack pointer
bplbpebprbpyes
silsiesirsiarg 2
dildiedirdiarg 1
r8br8wr8dr8arg 5
r9br9wr9dr9arg 6
r10br10wr10dr10
r11br11wr11dr11
r12br12wr12dr12yes
r13br13wr13dr13yes
r14br14wr14dr14yes
r15br15wr15dr15yes

The registers overlap in the low-order bits. For example, if r15 is 0x0123456789abcdef, then r15d is 0x89abcdef, r15w is 0xcdef, and r15b is 0xef. You can also have names for the for the lowest byte (ending with _l) and the second lowest byte (ending with _h).

x86 is really old, and it cares a lot about backwards compatibility. For that reason, a lot of functionality (such as floating point operations) is done in separate register banks with their own operations. We won’t worry about those.

Important Instructions #

Move #

There are different kinds of mov instructions. When moving from a smaller source to a larger destination, you can use movzx to fill the high-order bits with zeros or movsx to fill them with the source’s high-order bit. There’s a special “swap” instruction, xchg. There are special moves for moving between register banks as well as conditional moves.

Jump #

Jumps move the pc to a new location. jmp does this conditionally. Some jumps are conditional:

  • je (jump if equal)
  • jne (jump if not equal)
  • ja (jump if above)
  • jae (jump if above or equal)
  • jb (jump if below)
  • jbe (jump if below or equal)
  • jg (jump if greater)
  • jge (jump if greater or equal)
  • jl (jump if less)
  • jle (jump if less or equal)

The difference between above/below and greater/less is that above/below is unsigned and greater/less is signed.

Some jumps only check single bits, such as js (which checks the sign bit).

Conditions in x86-64 are based on “condition codes”, a set of single-bit flags that store enough information to compare a value to zero.

Load Effective Address #

One instruction, lea, is widely used. Instead of loading the contents of memory at an address, it loads the address itself. The nice thing here is that, since addresses are computed using addition and multiplication, lea can be used to do some basic arithmetic. For example, a = 5 * b + 20 can be written in AT&T syntax as lea 20(%rbx, %rbx, 4), %rax.

ALU Operations #

InstructionIs like
add+=
sub-=
and&=
or|=
xor^=
shl<<=
shr>>= (zero-extended)
sar>>= (sign-extended)

There’s also multiplication and division, but they’re a little weird.

imul X multiplies rax by register X, storing the 128-bit result with the high-order 64 bits in rdx and the low order in rax.

idiv X divides rax by register X, storing the quotient in rax and the remainder in rdx.

Push and Pop #

push X is roughly equivalent to:

rsp -= 8
memory[rsp] = X

pop X is roughly equivalent to:

X = memory[rsp]
rsp += 8

Note that some programs only use 32-bit and smaller values, and use a variant of push and pop that adjust esp by 4 instead of rsp by 8.

Call and Return #

call X means “push the address of the next instruction, then jmp X.” ret means “pop pc.” This isn’t otherwise possible because pc is a special register.

No Operation #

Compilers generate a lot of operations that technically do nothing, called “no-ops”. The most common one is nop, which does nothing. These are often used to align instructions with specific addresses and perform optimizations.

Calling Conventions #

It is common for ISAs to be accompanied by a recommended calling convention. This involves three primary components:

  • Argument passing: Invoking a function (with call) involves jumping to its code and storing where to return to. That code needs to know where to find its arguments. x86-64’s most common callend convention puts the arguments, in order, in rdi, rsi, rdx, rcx, r8, and r9. Remaining arguments, if any, are pushed onto the stack, last to first, before the call.
  • Return value passing: The code that invokes a function needs to know from where to retrieve its return value. x86-64’s most common calling convention puts the return value in rax.
  • Callee- and caller-save registers: In general, both the code that invokes a function and the code of the function itself will use all the program registers. This means that the old values of these registers must be saved and restored. x86-64 calling conventions disinguish between callee-save and caller-save registers. a caller-save register is one that the invoking code must assume the invoked code might have changed, thus necessitating saving it before the call if it contains meaningful data to the invoking code. It is also one that the invoked code can use without first saving and later restoring. A callee-save register is one the the invoking code can assume the invoked code will not change, and thus the invoking code does not need to save before the call. It is also one that the invoked code cannot use unless it first saves its value and restores that saved value to the register before returning. The most common way to save a register is to push its contents to the stack using push (as, e.g., push %rax) or a similar rsp-based mov (as, e.g., mov %rax, -32(rsp)). x86-64’s most common caling convention identifies rbx, rsp, rbp, and r12 through r15 as callee-save and all others as caller-save.

This is all convention, of course, but it is strongly in your best interests to follow it.