Skip to Content

Basic X86-64 Assembly Language

Intel first created the beginnings of its “x86” assembly language way back in about the early 1980’s with its 8088 8-bit CPU. This was followed quickly by a 16-bit version called the 8086, from which we get the “86” in “x86”. The late 1980’s saw the first 32-bit CPU, the 80386, and many 32-bit redesigns were done; eventually a need to move to 64 bits arose, and so they created the “x86-64” extension. (Actually, AMD created it before Intel!)

8 bit, 16 bit, 32 bit, and 64 bits!

The problem was always that Intel insisted on all of the CPUs to be backwards compatible, so they kept all the old lesser-bit instructions and just added the more-bit instructions. Since instructions operate on registers, this was also true about registers. This was done by using unique suffixes on instructions and prefixes on register names!

Original 16 bit register: %ax; then 32-bit register: %eax; then 64-bit: %rax

So “e” is the 32-bit register prefix, and “r” is the 64-bit register prefix. These are not separate registers! %ax is the lowest 16 bits of %eax, which is in turn the lowest 32 bits of %rax.

Original 16 bit move instruction: “mov”; then 32-bit move: “movl”; 64-bit: “movq”.

So “l” (ell) is the 32-bit instruction suffix, and “q” is the 64-bit instruction suffix. “l” stands for “long”, and “q” stands for “quad”.

C Programming Language use of 64 bit sizes

Interestingly, the official definition of the C programming language does not specify how many bits each datatype should have! It only specifies the rule:

char <= short <= int <= long <= long long

In other words, a valid C compiler could make them all the same!

When compiling for a 64-bit CPU, the vast majority of C compilers use the following sizes (in bits): char=8, short=16, int=32, long=64, long long = 64.

This is why when we generate the assembly code from a C program using “gcc -S”, for all of the integer operations we see the use of the “%e..” registers and the “…l” instructions (that end with ell). These are the 32-bit versions of everything.

However, pointers are generally 64 bits, so any time we are passing by reference (arrays, string constants, etc.), or generating code that uses pointers, we will see the use of the “%r..” registers and the “…q” instructions.

Registers

In addition to the legacy alpha-named registers, x86-64 added some numbered registers, and so now there are 16 64-bit registers: %rax, %rbx, %rcx, %rdx, %rdi, %rsi, %rbp, %rsp, and %r8-15. While they all work generally, many have very specific purposes. For example, %rsp is the stack pointer and should never be used for anything else; %rbp is generally used as a call frame pointer and not for anything else; etc. The alpha-named registers all have a %eXX 32-bit equivalent, but the numbered registers do not.

Function Calling

In 32-bit x86, all function arguments were passed on the stack; this is drastically changed in x86-64. In x86-64, the first six function arguments are passed in the registers %rdi, %rsi, %rdx, %rcx, %r8, and %r9, in order. If a function has more than six arguments, the rest are passed on the stack.

The function return value is passed in %rax (and %rdx if more bits needed).

The registers %rbx, %rbp, and %r12-15 are considered callee-saved registers. This means they are not freely available to use in a function; if a function wants to use them, it must first save them (by pushing them on the stack), and then restore them at the end. All other registers are considered freely available (the caller must save them before the call if it needs their values after the call).

The Stack

“The Stack” refers to the hardware-supported function call stack, where each function call creates an activation record (also known as a stack frame or call frame) on the stack, which contains information necessary for the function call to operate. This information generally includes: argument values (if not passed in registers), the return address, callee-saved register values, and local variables. Not every function call will have all of these, but every function call will at least have a return address, which is the place in the calling function to return to when the function call ends.

The CPU supports the stack with the %rsp register, known as the stack pointer. This register contains the memory address of the top of the stack, and the stack grows “downward” towards lower memory addresses. So subtracting 64 from the stack pointer is equivalent to opening up 64 bytes of usable memory space on the stack! Indeed, this is exactly how space for local variables is created!

In x86-64, usually the first two instructions in a function save the current %rbp on the stack (“pushq %rbp”) and then copy the stack pointer to %rbp (“movq %rsp, %rbp”). This makes %rbp a__ frame pointer__ for the current function call. Then all references to local variables and to arguments in memory are made using indirect addressing with %rbp. Since the %rbp is initialized before space is created on the stack (by subtraction), then local variable space is all with a negative offset from %rbp. So the memory address of a local variable looks something like “-16(%rbp)”, which is assembly syntax for subtracting 16 from the address value in %rbp, and using the result in indirect addressing.

Miscellany

The whole set of possible x86 instructions: https://en.wikipedia.org/wiki/X86_instruction_listings

A very useful x86-64 summary: https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf

X86 C function calling conventions (register usage): https://wiki.osdev.org/Calling_Conventions