Once upon a time it would have been common place for a software developer to sit down and get their work done using assembly language. These days, however, most developers hardly, if ever, find the need to touch a single line of assembly (aside from, perhaps, some debugging). Sure, there are some applications in which we may need to squeeze every ounce of efficiency out of our programs, but in this day and age you’ll be hard pressed to come any where close to the code optimizations made by modern compilers. Many use this as an excuse to never really learn assembly language (aside from the brief bit you may spend on it in a computer science course), but there is still a great deal of benefit to know it.
I personally find assembly language useful for reverse engineering work, but even a common developer can gain a great deal of insight into how the computer interprets their code by learning assembly. Furthermore, assembly can prove to be immensely helpful when it comes to debugging your programs. For these reasons I strongly believe that it is well worth any developers time to get at least a basic understanding of assembly language.
The aim of this post is to introduce you to the basics of the x86_64 assembly language. This is by no means meant to be a comprehensive guide on the topic of the x86_64 architecture or its version of assembly language. If you are looking for that I would suggest that you take a look at the Intel Software Developers Manuals, which you can find at the end of this post (along with some other resources that you may find useful).
The x86_64 Architecture
Assembly language is the lowest level of abstraction to the CPU without actually diving into the machine code itself. As a result of this fact, assembly language is directly dependent on its platform. For example, x86_64 assembly language is not going to be the same as ARM assembly or MIPS assembly or even x86 assembly (though it isn’t too far off from x86). We must thus have an understanding of the architecture that we are interested in targeting before we can begin to look at the specifics of its implementation of assembly language. In this case we will be targeting the x86_64 architecture.
x86_64 refers to the 64-bit architecture that is used in AMD and Intel processors. It is an extension developed for the 32-bit, x86 platform. It is worth mentioning that there are some subtle differences between the AMD and Intel implementations of the x86_64 architecture. For the most part they function in the same manner, meaning that the majority of assembly code written will run on either implementation. There are, however, a few instances in which this is not the case. The area of overlap for the two platforms is known as x64 assembly, which is what we will be dealing with in this article. There are also some advances topics that deal with hardware specific capabilities, which I will not be covering here.
Some Notes on Number Systems
For the purpose of this article we will be defining a byte as being 8-bits, a word as 16-bits, a double word as 32-bits, a quadword as 64-bits, and a double quadword as 128-bits. This is the same scheme that Intel uses on their introduction to x86_64 assembly language. Technically speaking, the 64-bit architecture does allow these values to vary, but for the purposes of this introduction it should be safe for us to assume that these values are tautologically true. Also following along with the scheme of the Intel white paper, we will be using little endian notation for memory addresses (the lower significant bytes are stored in lower memory addresses). This is, after all, the way both Intel and AMD architectures actually work.
x86 assembly (remember that x86_64 is just an extension of x86) has 8 primary registers that we are concerned with: EAX, ECX, EDX, EBX, ESP, EBP, ESI, and EDI. Just as a quick aside, since these are x86 registers, these can only store up to 32-bit values. Anyway, the first four of these registers (EAX, ECX, EDX, and EBX) are what we call general purpose registers and are known as the accumulator, counter, data, and base registers, respectively. These mainly act as temporary storage locations when the CPU is executing a program.
The other four registers (ESP, EBP, ESI, EDI) are also general purpose registers, but they are usually referred to as pointers and indexes. They are known as the stack pointer, base pointer, source index, and destination index, respectively. The first two of these registers (ESP and EBP) are especially important to program execution. The stack pointer (ESP) points to the last item pushed onto the stack. It is also worth mentioning that the stack grows towards lower memory addresses. The base pointer (EBP) deals with stack frames.
Another extremely important register is the instruction pointer (EIP) register. This register points to the next instruction that the CPU will execute.
So, just how do these registers apply to x86_64 assembly language? Well, remember that x86_64 is really nothing more than an extension to x86. As a result all of our registers from the above x86 section are still available on x86_64. They have, however, been extended from a max size of 32-bits to 64-bits. As a result they are now known as RAX, RCX, RDX, RBX, RSP, RBP, RSI, RDI, and RIP.
In other words, the “E”s have been replaced by “R”s. Well, ok, that statement is only half true. Prefixing the register names with “E”s is still valid in x64 assembly – It will just give you access to the first 32-bits of that register instead of the full 64-bits.
While we’re on this subject, for the RAX, RBX, RCX, and RDX registers, you can access just the lower 16-bits of the register by removing the initial r (so RAX would just be AX). Furthermore, you can access the lower byte of these 16-bits by replacing the “X” with and “L” (so AL for AX) and the upper byte by replacing the “X” with an “H” (AH for AX). This isn’t terribly important for this introduction, but it is certainly worth knowing in the event you ever decide to dive deeper into this subject.
In addition to these 9 registers, the x86_64 architecture also adds the register R8-R15. We will also want to be familiar with the RFLAGS registers, which stores various flags that are used for the results of operations as well as control of the CPU. It may also be worth noting that there are a number of other registers (for the FPU, and some other special-purpose registers) that we will not be concerning ourselves with here.
We are finally at a point where we can start taking a look at some assembly language code! Before we dive right in, however, we need to now that there are a number of different syntaxes for assembly. The two most common of these are the Intel and AT&T syntaxes. For the purpose of this introduction I will be using the Intel syntax, though once you know one it is a trivial task to pick up another. Assembly instructions in the Intel syntax use the following pattern:
operation destination, source
The operation is some assembly instruction, whereas the destination and the source will be either a memory address, a register or a literal value. In x64 assembly there are a myriad of operations, but we will just be concerning ourselves with some of the most common ones.
Common Assembly Instructions
The MOV opcode is used to move to/from/between memory and registers. This is used when I want to, for example, move a value from a given location in memory into a register.
These are used to push and pop values onto/off the stack, respectively.
Used to add values. The latter (ADC) is used to add with a carry.
Used to subtract values, with the latter being subtract with a carry.
Used to multiply values. The latter (IMUL) performs unsigned multiplication.
Used to divide values, with the latter being unsigned division.
Used to increment/decrement values, respectively.
Used to perform their respective bitwise operations.
An unconditional jump. This will jump the execution to a specified location.
Conditional jumps (jump if equal, jump if not equal, jump on carry, jump on not carry). There are also many other conditional jumps, but these are some of the most common.
Used to negate a value.
Used to perform comparisons.
Used to call a sub-routine.
Used for return values.
This instruction literally performs no operation.
Uses the ECX register to perform a loop.
As I mentioned earlier, there are many more instructions besides these, but these should be enough to get your feet wet.
Let’s actually put some of this into practice by taking a look at some examples.
Consider the following bit of C++ code:
for (int i=0; i < 10; i++) // Code to do something
We could implement this loop in assembly as follows:
mov RCX, 10 ; loop 10 times looper: ; this is a label ; Code to do something would go here loop looper ; performs the looping
The RCX register is commonly used to keep track of loops. We use the mov instruction to move the literal value 10 into the RCX register. This corresponds to how many times we will loop. We then reach the instruction loop looper. The loop instruction will decrement the value in RCX. If the value in RCX is not 0 it will jump to the specified label (in this case the label is looper).
Now, Let’s consider another piece of C++ code:
if (num > 1) num++; else num--;
To keep things simple, I am going to assume that the value of num has already been loaded into the RAX register. This would be implemented in assembly as follows:
cmp rax, 1 jle else add rax, 1 jmp xout else: sub rax, 1 xout: nop
We start off by comparing the values in RAX (which stores the value of the variable num) with 1. the jle instruction tells us to jump to the else label if RAX is less than or equal to 1. Otherwise, we add 1 to the value in RAX. We then jump to the label xout (which prevents us from hitting the code that is part of the else clause). If we were to jump to the else portion we would subtract 1 from the value of RAX before moving on to the xout code. In this example the xout portion just has a nop instruction, but in a real program there would likely be further program code at this point.
I could certainly go into a lot more detail on the subject of x64 assembly, but we are already nearing two-thousand words for this post as it is and this is only meant to be a brief introduction. This hopefully gives you a very basic understanding of what x86_64 assembly is and how it works. For more information on this subject I would highly recommend the following resources: