An Introduction to x86_64 Assembly Language

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_64 Registers

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.

Some Examples

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)

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
       sub rax, 1

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.

Wrapping Up

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:

20 thoughts on “An Introduction to x86_64 Assembly Language

  1. iwasanewt says:

    Thanks for taking the time to write this introduction!

    So, as far as I can tell, the only difference between 62 and 32 bit asm are the “super-extended” registers… Also, probably a typo in “Prefixing the register names with “E”s is this valid in x64 assembly – “, where “this” should read “still”.

    • Adam Thompson says:

      Also, probably a typo in “Prefixing the register names with “E”s is this valid in x64 assembly – “, where “this” should read “still”.

      Fixed now :)

    • Wow, thanks for the introduction. I have been gifted with knowledge, after reading your gift to the world. I now have some interest in writing my own computer language. Before this introduction I thought that assembly was binary code.

  2. Martin Haeberli says:

    Nice! one small suggestion – where you say “former” (which here would mean the first of two choices) I believe you mean the “latter” (which would mean the last of two choices).

  3. Chris Cooper says:

    Thanks for this and for the links to other tutorials/documentation. I’m a higher level developer professionally, (C#/Java with occasional C++/Objective C) and you hit the mark when you said most of us don’t have much experience with ASM outside of college. I can certainly understand the benefits of knowing the underlying architecture behind what we do on a daily basis, but I was wondering if you could provide any specific examples of times when knowing ASM has been a benefit to debugging your higher level code? Either way, thanks for presenting an important topic in a very understandable way.

    • Adam Thompson says:

      It’sIn my experience it’s not so much that it helps directly, but more indirectly. You of course can debug without ever needing to know a bit of assembly. However, if you understand assembly you arguably have at least some understanding of what your program is actually doing once it has been compiled. This alone can help you make sense of what’s happening when things go wrong. Not to mention that a language like C will start to make a lot more sense if you understand assembly (being that C is a pretty low-level high-level language).

      This article also points out a few other examples and is targeted for the .NET paltform:

      In my opinion it just never hurts to have a solid understanding of what’s going on under the hood ;)

  4. Nice tutorial, though there were some mistakes:
    > RAX, RCX, RDX, RBX, RSP, RSI, RDI, and RIP.
    You forgot RBP

    > EAX, ECX, EDX, EBX, ESP, EBP, ESI, and EDI.
    if EIP is another register, shouldn’t you say there are 9 registers total?

    > mov RCX, 10 ; loop 10 times
    Shouldn’t it have a CMP or J* with the register containing i and then an INC on that same register. Wouldn’t that be the way it is written in assembly.

    It would be nice if you included an explanation on how to run some assembly from the command line.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s