Understanding x86 Assembly (Part 1: Registers)
You’re in a computer architecture class. The professor shows you a bunch of x86 assembly code, and you have absolutely no idea what is going on. Well, you’re in the right place!
This article is part of a planned series. Check back soon when the next part is posted!
Memory Layout
Memory is simply an array of bytes, each byte having its own address. When a program is executed, the operating system allocates a chunk of memory to the program. That memory (called address space) is divided into different segments as shown below:
- The text section stores the program executable. When you compile a C program, the compiler converts your code to
0
s and1
s, which represent instructions that the CPU will execute. Those0
s and1
s are going to be loaded into this text section when you run the program. - The data section stores initialized data (i.e. global variables that have been initialized).
- The bss section stores uninitialized data (i.e. global variables that have not been initialized). In case you’re curious, it stands for Block Started by Symbol, named after an ancient assembler operator.
- The heap section are memory that you can dynamically reserve from calling malloc. The heap typically grows upwards, which means it grows toward larger memory addresses.
- The stack section are memory that your functions and the local variables inside your functions live. It is divided into stack frames, which grow downwards (i.e. grow toward lower memory addresses).
Keep this memory layout in mind. We will come back to it later when we start programming in assembly.
Registers
It turns out that modern computers require more than a physical memory to operate. Inside the CPU, there exists a small piece of memory called registers. Registers are extremely fast, because it can be directly accessed by the CPU. Modern x86–64 processors have 16 general-purpose 64-bit registers, whose names can be overwhelming to understand at first. So I’ll provide some historical context to help you understand them better. But first, the overall layout of the x86–64 registers:
Okay. Ignore all the descriptions on the right. In fact, ignore the entire image above. Don’t try to understand it right now. I put it purely for reference. We can come back to this later.
Backstory
8-bit registers
Long long time ago, in the age of the Intel 8080, registers are only 8 bits in size. They are simply named A, B, C, D, E, H, and L registers.
- The A register, in particular, was used as a primary accumulator. It accumulates (as you’ll see when we get to the actual assembly part) the result of most operations.
- The B register stands for the base register, which historically was used to store the base address of something we want to reference in memory (think arrays in C, where the reference address would be the address of the first element).
- The C register was the counter register, used to store counts just like the modern counter variables.
- The D register stands for the data register. It was used to store the data of most I/O operations.
In addition to general-purpose registers, we have status register, or flag bits, which are a series of bits that represent the status of certain operations. For example,
- the sign bit (S) will be set to
1
if the result of the previous operation is negative, and0
if the result is non-negative. - The zero bit (Z) will be set to
1
if the result of the previous operation is zero, and0
if the result is non-zero, so on, so forth.
And then we have some special registers, like the stack pointer (SP), and the instruction pointer (IP) or the program counter, which we’ll get back to in a moment.
16-bit registers
Fast forward a few years later, the Intel 8086 came out. The architects of the 8086 added a slew of other registers, and made them 16-bit in size. Among all the changes, the original A, B, C, and D registers had a slight modification to their names. Since the registers are 16 bits now, we can divide the original registers up into two 8-bit registers like these:
The AH and AL registers are called the “high byte” and the “low byte” respectively. Each of them are 8-bit (a byte) in size, but are paired up to form the 16-bit AX register (X in this case just stands for pair).
For example, if I have 0100 1101
stored in AX,
- AH would store
0100
, - AL would store
1101
, - AX would represent the entire
0100 1101
.
Same goes for the BX, CX, and DX registers. At this point, I should introduce two new terminologies. You are going to hear these terms a lot more often now: a byte, which just means 8 bits; and a word, which means 16 bits.
The 8086 also contains a couple of new word registers and flag bits, among them:
- the SI (Source Index) register, used as a pointer to a source in stream operations
- the DI (Destination Index) register, used as a pointer to a destination in stream operations
- the BP (Base Pointer) register, used as a pointer to the base of a stack frame (we’ll see examples of this)
- the SP (Stack Pointer) register — okay, you have seen this one before, and it’s used as a pointer to the current position in the stack (we’ll also see examples of this soon)
Here is a picture summary of what we learned so far.
Don’t worry about the segment registers yet. We’ll probably never touch them in your class.
If you’re feeling pretty overwhelmed at this point, stop reading. Go take a break. Don’t look at the diagram above, but look at the diagram below instead, because things are about to get interesting.
32-bit registers
Modern computers nowadays work on at least 32-bit (long or dword, short for double word) registers. This time, same concept as before, EAX (stands for extended AX) refer to the entire 32-bit value. If you want to access the lower word value of EAX, you can still use AX. Sadly, you won’t be able to access the higher word value of EAX this time.
To give an example, suppose EAX stores 1100 0100 1110 0010
,
- AL would be
0010
, - AH would be
1110
, - AX would be
1110 0010
, - EAX would be
1100 0100 1110 0010
.
It is important that you are familiar with all the registers you have learned so far, so study the diagram above!
64-bit registers
Here’s the fun part: if I show you the image I told you to skip in the beginning, it kinda starts to make sense.
Yes, that’s how 64-bit (qword, short for quad word) registers look like. You’ll notice that you can now access the entire 64-bit value with RAX (that R just stands for register, I guess they ran out of names…). EAX, AX, AH and AL are still there to maintain backward compatibility. Same goes for all the other registers.
You also have additional general-purpose registers, R8 to R15, which can be used to store anything you like. In fact, this whole time, you can store anything you like in any register — like you don’t need to store counter values in RCX, because RCX is just a regular ol’ piece of memory!
You know the full names of those registers you learned earlier? They’re mostly archaic and meaningless now (except RSP). Modern conventions don’t use them the way their names suggest (as you’ll learn in later parts of this article), but knowing what those names stand for is still useful sometimes for organizational purposes.
Alright, time to learn actual x86 assembly. Check out Part 2 of the article!
🚀🚀🚀
Fun fact: x86–64 is a superset of x86 architecture. It was created by AMD but it got so popular, Intel adopted it for their own use. You can read the history of how they got their names here.
Pictures adapted from “x86–64 Machine-Level Programming” by Randal E. Bryant and David R. O’Hallaron.