x86 Addressing Modes, Part 1 — Immediate and Direct Access
The foundations of memory access: static allocation, addressing modes, and the first steps toward low-level thinking.
Welcome back to our series on x86 assembly programming. If you are new, you can check out the series overview.
So far, we have learned the fundamentals of instructions and registers in x86 assembly. But writing real-world programs requires memory access, so we must learn how to deal with memory. If you can master this topic, you level up as a programmer.
There are two kinds of memory where we can keep our program’s data: registers and main memory. We have already learned about using registers; they are the fastest possible memory units in the hardware. But they are very limited in numbers, while real-world code needs much more memory than that.
Apart from that, registers can only hold primitive type values. The integer registers (the 16 general-purpose ones we learned about) handle integers, while separate floating-point registers exist in x86 for floating-point operations. However, we need a way to store and access composite types, such as arrays and structs, which is only possible using main memory.
Accessing memory in assembly is a big topic, so we’ll split it into a multipart series covering each addressing mode step by step as there are several memory addressing modes, and learning to effectively use each of them is crucial for us to read and write assembly code. So, I am going to split this topic into a multipart series. In this first part, we will cover the following topics:
Regions of memory in a process’s address space: stack, heap, and data
Immediate addressing mode
Direct addressing mode
In future parts, we will cover the following:
Indirect addressing mode
Offset-based addressing mode
Indexed addressing mode
We’ll start by understanding how data is organized in memory before we explore addressing modes. Now, let’s dive in!
I’m also publishing this in the form an ebook (PDF). If you don’t wish to upgrade to a subscription, you can purchase the PDF using the following link. If you are a paid subscriber, you can get it at a discount (monthly subs: 20% and annual subs: 50%). Please email me for the discounted link.
Regions of Memory in Process Address Space
When programming in high-level languages, you would have learned about the concept of scope or the lifetime of a variable. For example, a global variable lives for the duration of the program; local variables are automatically destroyed when the function returns. And, you can dynamically allocate memory on the heap that lives until it is freed.
When programming in assembly, we need similar scopes. However, there is no compiler to help us out, so we must do it ourselves. These scopes can be achieved by storing data in different regions in the address space of the process. So, we must start there.
There are three main regions in the process’s address space where you can decide to store your program’s data, as shown in the following diagram.
Stack segment: The
stacksegment is primarily used to implement function calls and to store function local data, such as variables and arguments. We will learn to use the stack when we talk about functions in assembly.Data Segment: The
datasegment is used to store static data. For example, whenever you create global variables or constants in your programs, the compiler may put them in the data segment. The advantage of the data segment is that it is burned as part of the program binary and loaded during startup. As a result, there is no memory allocation overhead at runtime.Heap Segment: The
heapsegment is used for dynamic memory allocation at runtime. For example, when growing an array, or creating nodes for a tree or a linked list.
In this article, we will mostly use the data segment, and we’ll cover heap and stack in future articles on dynamic memory allocation and function calls.
But, before jumping to memory access modes, we should spend a few minutes to learn how to do static memory allocation in the .data section, as we will be using static memory throughout the rest of this article.
Static Memory Allocation in the .data section
The data segment in the process’s address space is populated based on the contents in the .data section of the executable binary. When we want to create static data in our program, such as global variables or constants, we can put them in the .data section of our program.
To create a static value in the .data section, we need to do three things:
Create a label: At the time of writing assembly, we don’t know the exact memory address of the values or instructions, so we must use labels. At linking time, the linker replaces labels with the final addresses in the object code that it generates. So, creating a label for the value gives us a way to refer to its address.
Declare the size: We need to tell the assembler the size of the value, so that it can create that much space in the
.datasection. If you read the article on registers, you may recall that we have the following sizes:.quad: For 8-byte values.long: For 4-byte values.word: For 2-byte values.byte: For single-byte valuesApart from these, we also have the
.ascizmacro to create a nul-terminated ASCII string.
Declare the value: Finally, provide the value.
The following example shows how we can create an 8-byte integer value in the .data section with the label ANSWER_TO_LIFE:
This example allocates a single 64-bit value, but it is also possible to create more complex structures. For instance, we can create a struct-like object as shown in the example below:





