x86 Assembly Notes

These are my notes on the x86 Assembly Language which is covered in lectures 10-13 of CS 107.

By: Rohan Sikand

Table of contents:


We will cover the x86 assembly language over the next four lectures (lectures 10-13).


Introduction to x86 and Moving Data

Lecture 10 notes.

So we know that everything is represented in bits. But what about the literal C program source code itself? Yep! That is also converted into bits.

Viewing assembly

In your terminal window, compile your C program using make which should produce the executable which is binary machine code. Now to view this executable in assembly, type:

objdump -d <executable_name>

Let us now learn how to interpret the assembly representation. We will break down what is outputted by objdump. First, here is the C source of the program we are looking at.

// c code of assembly below 
int sum_array(int arr[], int nelems) {
	int sum = 0;
	for (int i = 0; i < nelems; i++) {
		sum += arr[i];
	}
	return sum;
}

Here is an example of assembly for the above code:

Each line represents an instruction.

Dissecting this down, we have the function name (instructions are divided into sub-blocks based on function calls):

we also have the memory addresses of each instruction:

As expected, we must store the instructions somewhere in memory. Thus, these are the addresses of each instruction.

and the machine code for each instruction:

then finally we have the human-readable version of the machine code for each instruction (i.e. the actual assembly)


Instructions

The fundamental unit of assembly is an instruction which is represented on the right hand side of each line in the breakdown above. Let us interpret the structure of each assembly instruction.

We have the opcode which is the name of the operation (there are different types of operations which we will explore):

We can also have arguments:

We can break down each argument here. Anything prefixed with a $ is a constant numerical value (also called an immediate value):

and anything prefixed with a % is something called a register which is something we will introduce next.


Registers

Register: A fast read/write memory slot (small storage area) on the CPU (processor) that can hold variable values.
  • Like a scratch pad for the processor. That is, it loads data into and out of these registers when it needs to perform some sort of operation on such data.
x86 registers
The register layer of abstraction - if you think about it, in order to convert a high-level program such as a C source into 0s and 1s, all of the functionalities must be converted into a linear and atomic sequence. Atomic meaning each piece of functionality in the program itself must be broken down into the lowest possible level—the fundamental unit, which is a simple operation (such as "add two numbers"). To accomplish this, we need some form of scratch space to store things like the result of an operation for use later (i.e. scratch paper). That is exactly what registers provide for us. This is the register layer of abstraction.

Operations

Now that we have a fundamental understanding of instructions and registers, let us put them to use. We introduce some fundamental operations here. First up is mov. We then generalize the form of each operand in each instruction.

mov

Let us now introduce our first operation.

mov: assembly instruction that copies bytes from one place to another.

The src and dest can be one of:

So in other words, you have five possible combinations:

Example:

mov %rbx,_____

Operand forms

We now introduce some neat tricks we can use to do things like pointer dereferencing and arithmetic at the assembly level.

We will see a lot of common patterns in assembly instructions that represent certain forms. Here, we introduce some useful forms we will work with (and we use mov to illustrate such forms):

mov (%rbx),_____ 
mov 0x10(%rax),_________
mov (%rax,%rdx),__________

We can even combine them. In general, we have the following form:

Imm(rb, ri) is equivalent to address Imm + R[rb] + R[ri]

As a principle, if you think about it, everything above is modeled by the above form. For example, if you have no immediate before (i.e. no displacement), then the displacement is really just 0 in the above form. In other words, you should dissect and construct forms based on the above form and not really based on the operational principles themselves.

Another way of thinking about the above form is that it is kind of like array indexing. You have your displacement to get to the certain address of the array. Then, you have your start index (which is the bit width of an element) and go up until you have the element that you want.

More forms

We now introduce another form:

mov (,%rdx,4),______

We have introduced a lot of forms, but the general form is indeed:

Imm(rb,ri,s) = Imm + R[rb] + R[ri]*s

Here is a nice chart:


Assembly: Arithmetic and Logic

Lecture 11 notes.

We will now dive deeper into our study of assembly and learn about how how to perform arithmetic and logical operations in assembly.

Note that in our study of assembly, our general perspective is to learn how to read assembly and understand the C code that generated it rather than write assembly from scratch.

Data and register sizes

We will introduce new terminology in terms of bytes that we will work with in assembly. We have:

This is useful stuff to know when analyzing assembly instructions since assembly instructions can have suffixes to refer to these sizes:

Subsets of registers

In addition to different sizes of data, there are different sizes of registers as well. These have different naming conventions and are sort of like nesting dolls. Example:

Register responsibilities

Some registers take on special responsibilities during program execution:

mov variants

Like we saw with the registers, the instruction opcodes can also be suffixed that specifies the size of the data to move:

For example, movl moves 4 bytes.

A couple funky things/additions to note:

lea

We now introduce a new instruction for arithmetic operations.

lea: The lea instruction copies an “effective address” from one place to another. Unlike mov, which copies data at the address src to the destination, lea copies the value of src itself (for example, an address rather than dereferencing) to the destination.
lea is mov without the dereferencing.

Examples:

Unlike mov, which copies data at the address src to the destination, lea copies the value of src itself to the destination.

You might think... why don't we just use mov without dereferencing using parentheses. Well, recall that only one of the src or dest can be a register... not both. Thus, we can use lea instead. Another motivation for lea is that what happens if you want to use an operand form (e.g. scaling factor, addition) but do not want to dereference? You can't. So you must use lea.

Logical and arithmetic operations

So far we have been dealing with binary instructions. We also have unary instructions that operate with only one operand:

We also have more binary operations:

Note that they both cannot be memory locations (like with mov). Also notice the bitwise operations.

> 64-bit arithmetic

Here is a thought: if registers are only 64 bits big... how do we multiply two 64 bit numbers together? That would be larger than 64 bits but our registers cap out at 64 bits. The answer is:

Division

Division is similar (can do 128 bit/64 bit number):

Note: this is actually how division is done even with < 64 bit numbers. You must sign extend the higher order bits first to fill in %rdx in that case. For this, use cqto.

Bit shifting

Top 2 are identical.

Reverse Engineering


Assembly: Control Flow

Lecture 12 Notes.

We will now talk about control flow. This is a very meta topic.

%rip: register that stores the address of the next assembly instruction to execute.

So, as you will see, we use this register for things like if-statements and loops.

jmp

The jmp instruction is an important one.

jmp: The jmp instruction jumps to another instruction in the assembly code (“Unconditional Jump”).

The destination can be hardcoded into the instruction (direct jump):

jmp 404f8 <loop+0xb> # jump to instruction at 0x404f8

The destination can also be one of the usual operand forms (indirect jump):

jmp *%rax # jump to instruction at address in %rax

But hold on... this is just for unconditional jumps (e.g. while (true). What happens if we want a conditional jump? That is what we will see next.

Conditional jumps

Let us start off with this example:

if (x > y) {
	// do something, a
} else {
	// do something, b
} 

In assembly, the conditional takes up two instructions:

  1. First, we calculate the condition result.
  1. Then, we jump to a or b based on the condition result.

Thus, we have the following common assembly pattern for this:

1. cmp S1, S2 // compare two values
2. je [target] or jne [target] or jl [target] or ... // conditionally jump with if equal, if not equal, and if less than respectively. 

This is like jmp but it only jumps if the prior condition is true. To achieve this logic, there are many variants of jmp. That is, there are variants of jmp that jump only if certain conditions are true (“Conditional Jump”). The jump location for these must be hardcoded into the instruction (not stored in a register).

Examples:

// Jump if %edi > 2
cmp $2, %edi
jg [target]

// Jump if %edi != 3
cmp $3, %edi
jne [target]

// Jump if %edi == 4
cmp $4, %edi
je [target]

// Jump if %edi <= 1
cmp $1, %edi
jle [target]

Condition codes

You might be wondering... how does the computer know what the value of the comparison was if we never stored it in memory to see if the jump statement should be executed? That is,

How does the jump instruction know anything about the compared values in the earlier instruction?
Condition codes: The CPU has special registers called condition codes that are like “global variables”. They automatically keep track of information about the most recent arithmetic or logical operation.

These special condition code registers are one bit and store the results of the most recent arithmetic or logical operation.

Most common condition codes (note these are all booleans represented by a 1 or 0):

So basically, cmp subtracts operand 1 from operand 2 (s2 - s1) and flips the values of the condition codes accordingly.

For example, you could have a string comparison using strcmp and it may return a positive number which the computer will know because SF will be marked as a 0.

Read cmp S1, S2 as compare S2 to S1 by calculating S2 – S1

The examples above but now with comments explaining how the comparison works:

// Jump if %edi > 2
// calculates %edi – 2
cmp $2, %edi
jg [target]

// Jump if %edi != 3
// calculates %edi – 3
cmp $3, %edi
jne [target]

// Jump if %edi == 4
// calculates %edi – 4
cmp $4, %edi
je [target]

// Jump if %edi <= 1
// calculates %edi – 1
cmp $1, %edi
jle [target]

You can parse through the logic yourself to figure out which condition codes are checked for each comparison, but the common ones you could theoretically just remember. Here is the chart where each conditional jump instruction is annotated telling you what condition codes it looks after):

If-statements

Let us use our knowledge to fully understand how we can understand things like if-statements in assembly.

Notice how if the cmp is true, then the jump, je, moves down to a different location then back up.

If-else

There is a funky pattern for if-else statements which you should be familiar with. As described above in normal if-statements, if the statement is true, you jump somewhere to perform the content in the if-statement body. However, with if-else, it is kind of in reverse:

First, you compare and if true, you jump to the else body first. If not true, you simply execute the next instruction sequentially which is the if-statement body. Because of the fact that if the compare instruction evaluates to true then you go the else body, the actual comparison is reversed from what is seen in the C code (e.g. if (x > y) is really if (x < y). See lecture for examples.

Loops

Now let us talk about loops in assembly.

Two common patterns we see:

In this case, the test is for seeing if the conditional is false (i.e. reverse and compare).

Another less common pattern (often seen in earlier versions of GCC):

For loops

For loops actually are represented in a very similar manner to while loops. This is because you can actually represent a for loop using a while loop:

Thus, we only need to add two instructions to our general pseudocode for assembly:


Assembly: Function Calls and the Runtime Stack

We will wrap up our discussion of assembly with function calls. This undoubtedly might be the hardest part so pay close attention to all the little details.

We wish to:

Revisiting %rip

Calling functions

We first must understand the terms caller and caller.

caller function calls the callee function. That is, the function that is being called is callee the callee and the function that is calling the other function is the caller.

To call functions in assembly, we need to do a couple things:

Remember that function calls work by adjusting the stack and each stack frame represents a function call. So does assembly interact with the stack? Via a special register:

%rsp is a special register that stores the address of the current “top” of the stack (the bottom in our diagrams, since the stack grows downwards).

Examples:

%rsp must point to the same place before a function is called and after that function returns, since stack frames go away when a function finishes

Interacting with the stack

The push instruction pushes the data at the specified source onto the top of the stack, adjusting %rsp accordingly.
Only works in 8 byte increments.
The pop instruction pops the topmost data from the stack and stores it in the specified destination, adjusting %rsp accordingly.

Passing control

We need to remember where we left off in a certain function when we call another function (i.e. what instruction to execute next after the callee function is done executing). But the problem is %rip will be pointing at different things because the callee will be updating it. So we bookmark our spot by appending it to the top of the stack before we move to the instructions for the callee. Then, once we are done, we take the bookmarked value from the top of the stack and store it back into %rip. Then, we adjust %rsp.

Example (5 total sequences):

But how does this happen in assembly? Via these new instructions:

call: instruction that pushes the address of the instruction immediately following the call instruction onto the stack and sets %rip to point to the beginning of the function (its instructions in assembly that is) specified by the operand.

The ret instruction does the opposite:

ret: instruction that pops the instruction address from the top of the stack (i.e. the one the was appended by call and stores it in %rip.

Passing data

That is, there are special registers that store parameters and the return value.
To call a function, we must put any parameters we are passing into the correct registers. (%rdi, %rsi, %rdx, %rcx, %r8, %r9, in that order).
  • Then, the callee can expect the parameters to be stored in these registers.
%rax: stores the return value of the callee. Return value must manually be placed into %rax.

Some common themes you will see in the assembly generated:

A lot of prepwork to call a function before the function is actually called.

Local storage and memory management

We have not really talked much about things like local variables and how that works. In actuality, we store as many local variables into registers as we can and then after those fill up, we start allocating the data to the stack. There are also three times in which we just go straight to the stack (i.e. don't even attempt to store into register):

As an aside, even without function calls, we might need to adjust the stack pointer before adding things like local variables.

Register restrictions

Problem: what if funcA is building up a value in register %r10, and calls funcB in the middle, which also has instructions that modify %r10? funcA’s value will be overwritten!

Thus, we must make some rules of the road to which the instructions must abide by when moving things in and out of registers.

Solution: make some “rules of the road” that callers and callees must follow when using registers so they do not interfere with one another.