X86-64 Architecture Guide

For the code-generation project, we shall expose you to a simplified version of the x86-64 platform.

Example

Consider the following Decaf program:

class Program {

    int foo(int x) {
        return x + 3;
    }

    void main() {
        int y;

        y = foo(callout("get_int_035"));
        if (y == 15) {
            callout("printf_035", "Indeed! \'tis 15!\n");
        } else {
            callout("printf_035", "What! %d\n", y);
        }
    }

}

One compiled version of this program might look like this:

foo:
    enter   $0, $0
    mov     16(%rbp), %rax
    add     $3, %rax
    leave
    ret

    .globl main
main:
    enter   $(8 * 3), $0

    call    get_int_035
    push    %rax

    call    foo
    add     $8, %rsp
    mov     %rax, -8(%rbp)

    mov     -8(%rbp), %r10
    mov     $15, %r11
    cmp     %r10, %r11
    mov     $0, %r11
    mov     $1, %r10
    cmove   %r10, %r11
    mov     %r11, -16(%rbp)

    mov     -16(%rbp), %r10
    mov     $1, %r11
    cmp     %r10, %r11
    je      .fifteen

    push    -8(%rbp)
    push    $.what
    call    printf_035
    add     $(2 * 8), %rsp
    jmp     .fifteen_done

.fifteen:
    push    $.indeed
    call    printf_035
    add     $(1 * 8), %rsp
.fifteen_done:

    mov     $0, %rax
    leave
    ret

.indeed:
    .string "Indeed, \'tis 15!\n" 

.what:
    .string "What! %d\n"

We shall dissect this assembly listing carefully and relate it to the Decaf code. Note that this is not the only possible assembly of the program; it only serves as an illustration of some techniques you can use in this project phase.

foo:
    enter   $(8 * 0), $0
    ...
    leave
    ret
    mov     16(%rbp), %rax
    add     $3, %rax
    .globl main
main:
    enter   $(8 * 3), $0
    ...
    mov     $0, %rax
    leave
    ret
    call    get_int_035
    push    %rax
    call    foo
    add     $8, %rsp
    mov     %rax, -8(%rbp)
    mov     -8(%rbp), %r10
    mov     $15, %r11
    cmp     %r10, %r11
    mov     $0, %r11
    mov     $1, %r10
    cmove   %r10, %r11
    mov     %r11, -16(%rbp)
    mov     -16(%rbp), %r10
    mov     $1, %r11
    cmp     %r10, %r11
    je      .fifteen
    ...
    jmp     .fifteen_done
.fifteen:
    ...
.fifteen_done:
.indeed:
    .string "Indeed, \'tis 15!\n" 

.what:
    .string "What! %d\n"

Reference

This handout only mentions a small subset of the rich possibilities provided by the x86-64 instruction set and architecture. For a more complete (but still readable) introduction, consult The AMD64 Architecture Programmer’s Manual, Volume 1: Application Programming.

Registers

In the assembly syntax accepted by gcc, register names are always prefixed with %. For the first part of the project, we shall use only five of the x86-64’s sixteen general-purpose registers. All of these registers are 64 bits wide.

Register Purpose Saved across calls
%rax return value No
%rsp stack pointer Yes
%rbp base pointer Yes
%r10 temporary No
%r11

Instruction Set

Each mnemonic opcode presented here represents a family of instructions. Within each family, there are variants which take different argument types (registers, immediate values, or memory addresses) and/or argument sizes (byte, word, double-word, or quad-word). The former can be distinguished from the prefixes of the arguments, and the latter by an optional one-letter suffix on the mnemonic.

For example, a mov instruction which sets the value of the 64-bit %rax register to the immediate value 3 can be written as

    movq    $3, %rax

Immediate operands are always prefixed by $. Un-prefixed operands are treated as memory addresses, and should be avoided since they are confusing.

For instructions which modify one of their operands, the operand which is modified appears second. This differs from the convention used by Microsoft’s and Borland’s assemblers, which are commonly used on DOS and Windows.

Opcode Description
Copying values
mov src, dest Copies a value from a register, immediate value or memory address to a register or memory address.
cmove %src, %dest Copies from register %src to register %dest if the last comparison operation had the corresponding result (cmove: equality, cmovne: inequality, cmovg: greater, cmovl: less, cmovge: greater or equal, cmovle: less or equal).
cmovne %src, %dest
cmovg %src, %dest
cmovl %src, %dest
cmovge %src, %dest
cmovle %src, %dest
Stack management
enter $x, $0 Sets up a procedure’s stack frame by first pushing the current value of %rbp on to the stack, storing the current value of %esp in %ebp, and finally decreasing %esp to make room for x quadword-sized local variables.
leave Removes local variables from the stack frame by restoring the old values of %rsp and %rbp.
push src Decreases %rsp and places src at the new memory location pointed to by %rsp. Here, src can be a register, immediate value or memory address.
pop dest Copies the value stored at the location pointed to by %rsp to dest and increases %rsp. Here, dest can be a register or memory location.
Control flow
jmp target Jump unconditionally to target, which is specified as a memory location (for example, a label).
je target Jump to target if the last comparison had the corresponding result (je: equality; jne: inequality).
jne target
Arithmetic and logic
add src, dest Add src to dest.
sub src, dest Subtract src from dest.
imul src, dest Multiply dest by src.
idiv src, dest Divide dest by src.
shr src, dest Shift dest to the left or right by src bits.
shl src, dest
ror src, dest Rotate dest to the left or right by src bits.
cmp src, dest Set flags corresponding to whether dest is less than, equal to, or greater than src

Stack Organization

Global and local variables are stored on the stack, a region of memory that is typically addressed by offsets from the registers %rbp and %rsp. Each procedure call results in the creation of a stack frame where the procedure can store local variables and temporary intermediate values for that invocation. The stack is organized as follows:

Position Contents Frame
8n+16(%rbp) argument n Previous
... ...
16(%rbp) argument 0
8(%rbp) return address Current
0(%rbp) previous %rbp value
-8(%rbp) locals and temps
...
0(%rsp)

Calling Convention

The caller pushes arguments on to the stack in reverse order. Finally, it pushes the return address and transfers control to the callee. The callee places its return value in %rax and is responsible for cleaning up its local variables as well as for removing the return address from the stack. It is not responsible for removing the arguments.

The call, enter, leave and ret instructions make it easy to follow this calling convention.

The standard calling convention used by C programs under Linux on x86-64 is a little different; see System V Application Binary Interface—AMD64 Architecture Processor Supplement for details. Specifically, it optimizes calls by passing the first few arguments in registers instead of on the stack. As a result, your program cannot directly call out to arbitrary C procedures yet. Instead, we have provided two functions, printf_035 and get_int_035, which have been specifically adapted to this simplified convention.

Function Description
printf_035(fmt, arg1, ...) Print a formatted string to standard output, exactly like printf(3).
get_int_035() Read a single signed decimal integer from standard input and return its value.