Registers

Last time, we ended by discussing the sixteen general-purpose registers found on most modern Intel-based computers, shown here:

64-bit register lowest 32 bits lowest 16 bits lowest 8 bytes
rax eax ax al
rbx ebx bx bl
rcx ecx cx cls
rdx edx dx dl
rsi esi si sil
rdi edi di dil
rbp ebp bp bpl
rsp esp sp spl
r8 r8d r8w r8b
r9 r9d r9w r9b
r10 r10d r10w r10b
r11 r11d r11w r11b
r12 r12d r12w r12b
r13 r13d r13w r13b
r14 r14d r14w r14b
r15 r15d r15w r15b

Some of the sixteen registers have important functions that we should talk about:

  1. When an assembly function returns, it always returns the value stored in the register %rax (or a smaller sub-register, depending on what is specified).
  2. When arguments are passed into the function, the first argument is passed into %rdi, the second argument to %rsi, the third argument to %rdx, the fourth argument to %rcx, the fifth argument to %r8, and the sixth argument to %r9.
  3. The %rdx register is often used to handle overflow problems from multiplication operations, or to overflow before we perform a division operation. (This will almost certainly make no sense right now, but we’ll go over what this means below.)
  4. The %rsp register is typically reserved to store a pointer to the “top” of the stack, and %rbp register is typically reserved to store a pointer to the “bottom of the stack.”
  5. While things are pushed to the top of the stack, in reality in computer memory, the stack grows downwards, and so the value stored in %rsp is always less than or equal to the value stored in %rbp.

Register Arithmetic

To dereference registers, we use parentheses () instead of the square brackets []. For example, if the heap memory location 0x80000010 was stored in the register %rax, then (%rax) would refer to whatever is stored in the primary memory at location 0x80000010.

Often times, we are interested in storing a pointer to the beginning of an array at a heap memory location, or pointer to somewhere on the stack in reference to the bottom of the stack. In both cases, we often don’t know the “absolute” memory location in these cases of certain objects stored in the memory, but rather their location relative to a reference, such as the pointer to the start of an array or the pointer to the base of a stack stored in %rbp. In order to denote relative memory location, we can use common addressing conventions in AT&T syntax for dealing with point locations stored in registers. The idea is that we can refer to addresses in the heap in the following notation convention:

offset(base, index, scale)

The base is the base reference memory location, often stored in some register. index can be thought of the array index in an array, and scale is the size of each array element. offset is the offset from the base reference location. In this case, this notation references the primary storage location

[base + (index * scale) + offset]

For example, let’s say that the integer 0x3 was stored at the memory location 0x80000010, and the location 0x80000000 to the start of the heap location was stored in the register %rcx. This means that the instruction

movq    (%rcx, 2, 8), %rax

moves the integer 0x3 to the register %rax. This is because 0x80000000 + (2 * 8) = 0x80000010. Here are some examples of using parentheses to dereference pointers stored in registers:

  1. (%rax). This accesses the contents of the memory at the address stored in %rax.
  2. (%rbx, %rcx). This accesses the contents of the memory at the address stored in %rbx + %rcx (ie adding the two values stored in these two registers and then treating the sum as a pointer to the primary storage).
  3. (%rbx, %rcx, 8). This accesses the contents of the memory at the address stored in %rbx + (8 * %rcx).
  4. 4(%rbx, %rcx, 8). This accesses the contents of the memory at the address stored in %rbx + (8 * %rcx) + 4.

This offset notation can also be used to do basic arithmetic! This can be done using the lea command, which we’ll introduce and talk about in the section below. Basically, the instruction looks like the following:

leaq    some calculation, destination

For example, suppose that we wanted to calculate 2 + (5 * 8) - 4, and then store the result into the register %rax. A succinct way of doing this is to use the instruction

leaq    -4(2, 5, 8), %rax

The benefit of doing calculations in this way is that (1) it is often more concise, and (2) it performs the calculation without modifying any of the flags in the ALU. Flags are something that we’ll discuss in the sections below.

Common x86 Assembly Commands

Here are a list of common x86 assembly commands and their uses.

Data Movement Instructions

Instruction Arguments Description
mov src, dst move src to dst
push src push src onto the top of the stack
pop dst pop the top of the stack to dst
cwtl sign-extend word in %ax to long in %eax
cltq sign-extend long in %eax to quad in %rax
cqto sign-extend quad in %rax to 16-byte octoword in %rdx:%rax (%rdx holds the most significant bit)

Unary Operations

Instruction Arguments Description
inc dst increment dst by one
dec dst decrement dst by one
neg dst replace dst with its additive inverse
not dst replace dst with its bitwise complement

Binary Operations

Instruction Arguments Description
leaq src, dst load address src to dst (not the value stored at src to dst)
add src, dst add src to dst
sub src, dst subtract src from dst
imul src, dst multiply dst by src
and src, dst bitwise AND on dst by src
or src, dst bitwise OR on dst by src
xor src, dst bitwise XOR on dst by src

Special Arithmetic Operations

Sometimes for multiplication and division operations, we need to worry about bit extension. This is because, for example, multiplication of two 64-bit integers can yield a 128-bit integer, and division of two 64-bit integers can yield an incorrect result if the thing being divided is not converted into a 128-bit integer first. This is perhaps easiest to understand in terms of division by a power of two using left shift and right shift operations. We discussed some of the issues with these operations if not considered carefully previously.

Instruction Arguments Description
imulq src signed multiply %rax by src, and store result in %rdx:%rax%
mulq src unsigned multiply %rax by src, and store result in %rdx:%rax%
idivq src signed divide %rdx:%rax by src, and store quotient in %rax% and remainder in %rdx%
divq src unsigned divide %rdx:%rax by src, and store quotient in %rax% and remainder in %rdx%

Shifting Operations

Instruction Arguments Description
sal or shl k, dst left shift dst by k bits
sar k, dst arithmetic right shift dst by k bits
shr k, dst logical right shift dst by k bits

Compare Instructions

Instruction Arguments Description
cmp src1, src2 sets flags according to src2 - src1
test src1, src2 sets flags according to src2 & src1

At this point, it is also worth noting what the flags are that are stored in the special dedicated FLAGS register. There are a number of them, but we’ll highlight the most important ones here:

Flag Set to 1 when...
carry flag CF sum exceeds capacity of register, or negative number was obtained in subtracting two unsigned integers
parity flag PF parity of result of last operation was even
zero flag ZF result of last operation was 0
sign flag SF result of last operation was negative

Logical Control Flow Instructions

Typically, based on a cmp result, we may want to jump to another set of instructions. This is common in implementing iterations and conditional statements.

Instruction Description FLAGS Condition
jmp jump to specified label
je / jz jump to specified label only if equal or zero ZF
jne / jnz jump to specified label only if not equal or not zero ~ZF
js jump to specified label only if negative SF
jns jump to specified label only if not negative ~SF
jg / jnle jump to specified label only if greater (signed) ~(SF^OF)&(~ZF)
jge / jnl jump to specified label only if greater than or equal (signed) ~(SF^OF)
jl / jnge jump to specified label only if less (signed) SF^OF
jle / jng jump to specified label only if less than or equal (signed) (SF^OF)|ZF

Procedure Call Instructions

Instruction Arguments Description
call label push return address and jump to label
leave set %rsp to %rbp, and then pop the top of the stack into %rbp register
ret pop return address from stack and jump there

Additional Instructions

Once again, we have only grazed the surface of the set of x86 instructions available to us in assembly. While the above represent the most common instructions, you can find the full set in the x86 assembly documentation here.

Stack Manipulation

As we mentioned previously, the stack is an important component of the primary storage, and can be thought of as growing downwards. This means that the “top” where things are immediately pushed and popped is at a lower address in the primary storage than the bottom of the stack. For example, let’s say that we pushed the number 1070, and then 972, and then 256, and then -7 onto the stack. The state of the stack after these four push operations would look something like this:

stack

Although the stack is indeed a LIFO data structure, it’s perhaps more appropriate to think of it as a “transparent stack.” What this means is that you’re able to read and write values anywhere in the stack, even if it is not at the top. You just can’t change the order of the elements or remove a stack entry found somewhere in the middle of the stack. For example, using the example above, -0x8(%rbp) would refer to 1070. In this particular case, it would also be equivalent to 0x20(%rsp).

When things are pushed and popped from a given stack frame, it is the %rsp frame that moves automatically with pushing and popping, as %rsp always points to the next free memory space on the stack.

In disassembling C code, you might notice that many assembly functions start off with and end with a similar set of commands:

push   %rbp
mov    %rsp,%rbp
// some instructions
pop    %rbp

Let’s figure out the purpose of these commands. Recall that even if we’re in different functions, all of the function in a given C program share the same stack in the primary memory. However, we don’t want the stack data from different functions being jumbled together. This is effectively what these three set of instructions are doing. When we first enter a function, the push %rbp instruction pushes the address of the previous %rbp base pointer onto the stack, and the next instruction essentially resets the stack frame. This process looks something like this:

stack-2

In this way, the previous stack data from any previous functions remains unaffected by any stack operations made by our current function. This process of pushing %rbp and then moving %rsp to %rbp is called adjusting the stack frame. To finish up the program, pop %rbp will restore the old %rbp with the previous stack data, such that from an outside program function, it effectively looks like the stack wasn’t touched by our function after we exit from the function. This functionality is crucial in order for us to be able to call functions within functions.

Moving On…

Now that we understand the basics of assembly language syntax and what are the typical commands and operations available to us, we can now try to actually read some assembly code and actually write our own as well here.