x86 Registers and Instructions
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 |
cl s |
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:
- 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). - 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
. - 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.) - 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.” - 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:
(%rax)
. This accesses the contents of the memory at the address stored in%rax
.(%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).(%rbx, %rcx, 8)
. This accesses the contents of the memory at the address stored in%rbx + (8 * %rcx)
.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:
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:
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 push
ing %rbp
and then mov
ing %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.