X86 Cheat Sheet



A CS107 joint staff effort (Erik, Julie, Nate)

x86-64 (also known as just x64 and/or AMD64) is the 64-bit version of the x86/IA32 instruction set. Below is our overview of its features that are relevant to CS107. There is more extensive coverage on these topics in Chapter 3 of the B&O textbook. See also our x86-64 sheet for a compact reference.

The table below lists the commonly used registers (sixteen general-purpose plus two special). Each register is 64 bits wide; the lower 32-, 16- and 8-bit portions are selectable by a pseudo-register name. Some registers are designated for a certain purpose, such as %rsp being used as the stack pointer or %rax for the return value from a function. Other registers are all-purpose, but have a conventional use depending on whether caller-owned or callee-owned. If the function binky calls winky, we refer to binky as the caller and winky as the callee. For example, the registers used for the first 6 arguments and return value are all callee-owned. The callee can freely use those registers, overwriting existing values without taking any precautions. If %rax holds a value the caller wants to retain, the caller must copy the value to a 'safe' location before making a call. The callee-owned registers are ideal for scratch/temporary use by the callee. In contrast, if the callee intends to use a caller-owned register, it must first preserve its value and restore it before exiting the call. The caller-owned registers are used for local state of the caller that needs to preserved across further function calls.

  1. Intel x86 Assembler Instruction Set Opcode Table. ADD Eb Gb 00: ADD Ev Gv 01: ADD Gb Eb 02: ADD Gv Ev 03: ADD AL Ib 04: ADD eAX Iv 05: PUSH ES 06: POP ES 07: OR Eb Gb.
  2. X86-64 Assembly Language Summary Dr. Orion Lawlor, last update 2019-10-14 These are all the normal x86-64 registers accessible from user code: Name Notes Type 64-bit long 32-bit int 16-bit short 8-bit char rax Values are returned from functions in this register.
RegisterConventional useLow 32-bitsLow 16-bitsLow 8-bits
%raxReturn value, callee-owned%eax%ax%al
%rdi1st argument, callee-owned%edi%di%dil
%rsi2nd argument, callee-owned%esi%si%sil
%rdx3rd argument, callee-owned%edx%dx%dl
%rcx4th argument, callee-owned%ecx%cx%cl
%r85th argument, callee-owned%r8d%r8w%r8b
%r96th argument, callee-owned%r9d%r9w%r9b
%r10Scratch/temporary, callee-owned%r10d%r10w%r10b
%r11Scratch/temporary, callee-owned%r11d%r11w%r11b
%rspStack pointer, caller-owned%esp%sp%spl
%rbxLocal variable, caller-owned%ebx%bx%bl
%rbpLocal variable, caller-owned%ebp%bp%bpl
%r12Local variable, caller-owned%r12d%r12w%r12b
%r13Local variable, caller-owned%r13d%r13w%r13b
%r14Local variable, caller-owned%r14d%r14w%r14b
%r15Local variable, caller-owned%r15d%r15w%r15b
%ripInstruction pointer
%eflagsStatus/condition code bits

We will uses the standard AT&T syntax for writing x86 assembly code. The full x86 instruction set is large and complex (Intel's x86 instruction set manuals comprise over 2900 pages), and we do not cover it all in this guide. For example, there is a 16-bit subset of the x86 instruction set. Using the 16-bit programming model can be quite complex.

True to its CISC nature, x86-64 supports a variety of addressing modes. An addressing mode is an expression that calculates an address in memory to be read/written to. These expressions are used as the source or destination for a mov instruction and other instructions that access memory. The code below demonstrates how to write the immediate value 1 to various memory locations in an example of each of the available addressing modes:

A note about instruction suffixes: many instructions have a suffix (b, w, l, or q) which indicates the bitwidth of the operation (1, 2, 4, or 8 bytes, respectively). The suffix is often elided when the bitwidth can be determined from the operands. For example, if the destination register is %eax, it must be 4 bytes, if %ax it must be 2 bytes, and %al would be 1 byte. A few instructions such as movs and movz have two suffixes: the first is for the source operand, the second for the destination. For example, movzbl moves a 1-byte source value to a 4-byte destination.

When the destination is a sub-register, only those specific bytes in the sub-register are written with one broad exception: a 32-bit instruction zeroes the high order 32 bits of the destination register.

Mov and lea

By far most frequent instruction you'll encounter is mov in one of its its multi-faceted variants. Mov copies a value from source to destination. The source can be an immediate value, a register, or a memory location (expressed using one of the addressing mode expressions from above). The destination is either a register or a memory location. At most one of source or destination can be memory. The mov suffix (b, w, l, or q) indicates how many bytes are being copied (1, 2, 4, or 8 respectively). For the lea (load effective address) instruction, the source operand is a memory location (using an addressing mode from above) and it copies the calculated source address to destination. Note that lea does not dereference the source address, it simply calculates its location. This means lea is nothing more than an arithmetic operation and commonly used to calculate the value of simple linear combinations that have nothing to do with memory locations!

The mov instruction copies the same number of bytes from one location to another. In situations where the move is copying a smaller bitwidth to a larger, the movs and movz variants are used to specify how to fill the additional bytes, either sign-extend or zero-fill.

A special case to note is that a mov to write a 32-bit value into a register also zeroes the upper 32 bits of the register by default, i.e does an implicit zero-extend to bitwidth q. This explains use of instructions such as mov %ebx, %ebx that look odd/redundant, but are, in fact, being used to zero-extend from 32 to 64. Given this default behavior, there is no need for an explicit movzlq instruction. To instead sign-extend from 32-bit to 64-bit, there is an movslq instruction.

The cltq instruction is a specialized movs that operates on %rax. This no-operand instruction does sign-extension in-place on %rax; source bitwidth is l, destination bitwidth is q.

Arithmetic and bitwise operations

X86 Assembly Cheat Sheet

The binary operations are generally expressed in a two-operand form where the second operand is both a source to the operation and the destination. The source can be an immediate constant, register, or memory location. The destination must be either register or memory. At most one of source or destination can be memory. The unary operations have one operand which is both source and destination, which can be either register or memory. Many of the arithmetic instructions are used for both signed and unsigned types, i.e. there is not a signed add and unsigned add, the same instruction is used for both. Where needed, the condition codes set by the operation can be used to detect the different kinds of overflow.

Branches and other use of condition codes

The special %eflags register stores a set of boolean flags called the condition codes. Most arithmetic operations update those codes. A conditional jump reads the condition codes to determine whether to take the branch or not. The condition codes include ZF (zero flag), SF (sign flag), OF (overflow flag, signed), and CF (carry flag, unsigned). For example, if the result was zero, the ZF is set, if a operation overflowed (into sign bit), OF is set.

The general pattern for all branches is to execute a cmp or test operation to set the flags followed by a jump instruction variant that reads the flags to determine whether to take the branch or continue on. The operands to a cmp or test are immediate, register, or memory location (with at most one memory operand). There are 32 variants of conditional jump, several of which are synonyms. Here are some example branch instructions.

Setx and movx

There are two other families of instructions that read/react to the current condition codes. The setx instructions set a destination register to 0 or 1 according to the status of condition x. The cmovx instructions will conditionally execute a move based on whether condition x holds. The x is a placeholder for any of the conditional variants: e, ne, s, ns, etc.

For the setx instruction, the destination must be a single-byte sub-register (e.g. %al for the low byte of %rax). For the cmovx instructions, both the source and destination must be registers.

Function call stack

The %rsp register is used as the 'stack pointer'; push and pop are used to add/remove values from the stack. The push instruction takes one operand: an immediate, a register, or a memory location. Push decrements %rsp and copies the operand to be tompost on the stack. The pop instruction takes one operand, the destination register. Pop copies the topmost value to destination and increments %rsp. It is also valid to directly adjust %rsp to add/remove an entire array or a collection of variables with a single operation. Note the stack grows downward (toward lower addresses).

Call/return are used to transfer control between functions. The callq instruction takes one operand, the address of the function being called. It pushes the return address (current value of %rip, which is the next instruction after the call) onto the stack and then jumps to the address of the function being called. The retq instruction pops the return address from the stack into %rip, thus resuming at the saved return address.

To set up for a call, the caller puts the first six arguments into registers %rdi, %rsi, %rdx, %rcx, %r8, and%r9 (any additional arguments are pushed onto the stack) and then executes the call instruction.

When callee finishes, it writes the return value (if any) to %rax, cleans up the stack, and use retq instruction to return control to the caller.

The target for a branch or call instruction is most typically an absolute address that was determined at compile-time. However there are cases where the target is not known until runtime, such as a switch statement compiled into a jump table or when invoking a function pointer. For these, the target address is computed and stored in a register and the branch/call variant is used je *%rax or callq *%rax to read the target address from the specified register.

The debugger has many features that allow you to trace and debug code at the assembly level. You can print the value in a register by prefixing its name with $ or use the command info reg to dump the values of all registers:

Sheet

The disassemble command will print the disassembly for a function by name. The x command supports an i format which interprets the contents of a memory address as an encoded instruction.

You can set a breakpoint at a particular assembly instruction by its direct address or offset within a function

X86 Reverse Engineering Cheat Sheet

You can advance by instruction (instead of source line) using the stepi and nexti commands.