Confessions of a Code Addict

Confessions of a Code Addict

x86 Addressing Modes, Part 2: Indirect, Indexed, and Offset-based Modes

In this final part on addressing modes, we learn about indirect, indexed and offset-based addressing modes along with some interesting examples and exercises.

Abhinav Upadhyay's avatar
Abhinav Upadhyay
Aug 09, 2026
∙ Paid

Welcome back to our series on x86 assembly programming. If you are new, you can check out the series overview.

A Programmer’s Guide to x86-64 Assembly (Series Overview)

A Programmer’s Guide to x86-64 Assembly (Series Overview)

Abhinav Upadhyay
·
July 16, 2025
Read full story

In the previous part of this article, we covered some of the basics of x86 addressing modes, such as immediate addressing and direct memory addressing. This 2nd part continues where we left and completes the coverage of all the x86 addressing modes, along with all the caveats that you need to be aware of. Along the way, you will also implement some very interesting examples and exercises, such as parsing the command line arguments on the stack, implementing the magic 8-ball game, and implementing a lookup table in assembly.

Here’s what we will learn:

  • Indirect memory addressing: this is how pointers work in C

  • Offset-based addressing: this is how we access fields inside a struct

  • Indexed addressing: this is how we access elements of an array

  • The generalised addressing mode syntax in x86 assembly: all the other addressing modes are simplifications of this generalised syntax

Let’s start with indirect memory addressing.

I’m also publishing this series on x86 assembly 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.

Get Ebook


Indirect Memory Addressing

In the previous article, we learned about direct memory addressing, which is only useful when you know the exact memory address that you can encode in the instruction. This is mostly possible for static data of your program because their addresses are known at assembly and linking time. However, most real-world programs work with dynamically allocated memory where the actual memory address is known only at runtime, so direct memory addressing is not viable for such cases. We need indirect memory addressing.

In indirect memory addressing, the memory address is stored in a register. In this mode, the processor needs to first read the address from the register, do the memory access, and then execute the instruction.

For example, when we allocate dynamic memory on the heap using malloc or mmap, it returns the address of the allocated memory. The returned address is kept in a register like RAX. Now, if we want to read or write this memory, we need to tell the processor to treat the value in RAX as an address, perform a memory access at that address, and execute the actual instruction.

This is different from direct memory addressing because there the address was part of the instruction, so the processor did not need to get the address from anywhere. In contrast, here the processor first needs to read the address from the register, and then access memory. You can also think of indirect memory addressing as adding one layer of indirection to memory access.

While dynamic memory allocation in C is easy, in assembly it is very involved, so we will learn to do that in a future article. Instead, to learn how to use indirect memory addressing, we will copy the address of a value stored in the .data section into a register.

So, how do we copy the address of a label into a register? We know that when we write:

# move the value at the label ANSWER_TO_LIFE into rax
movq ANSWER_TO_LIFE, %rax

The instruction tells the processor to copy the value stored at the address ANSWER_TO_LIFE into rax. However, when we want to copy the address itself into a register, we need to use the $ prefix that turns the instruction into immediate addressing mode:

# move the address of the label into rax
movq $ANSWER_TO_LIFE, %rax

After this, the register rax contains the memory address where the value 42 is stored. The following diagram visualizes this

RAX contains address of a value stored in the .data section
RAX contains address of a value stored in the .data section

The diagram shows that after executing that mov instruction, the register rax contains the memory address of the value in the .data section.

Once we have an address in a register, whenever we want to dereference that address, we need to use indirect addressing mode syntax. The following snippet shows how to copy a value by dereferencing the address in rax.

# indirect addressing mode
movq (%rax), %rdi # dereference address in rax and copy the value from memory into rdi

The syntax is a bit different from what we have seen so far. When we write “movq %rax, %rdi”, we tell the processor to copy the value stored in rax into rdi. But when we surround one of the registers with parentheses, the assembler generates a different encoding of the instruction that tells the processor that the register contains an address, and it needs to get the value from that address.

We can see the difference in the encoding using objdump just like we did for direct memory addressing. The following objdump output shows the difference in the encoding for the instructions movq %rax, %rdi and movq (%rax), %rdi.

0000000000401000 <_start>:
401000:  48  89  c7  mov  %rax,%rdi
401003:  48  8b  38  mov (%rax),%rdi

You can see that at the machine code level, these two result in two very different encodings.

Base Address and Base Register

Before continuing further, let’s introduce two important terms that we should understand in the context of assembly programming.

Base Address

When working with memory addresses, what we have is the starting address of a value in memory; this is also called the base address of the value.

For example, an 8-byte value 42 may be stored at the address 0x0010. Because its size is 8 bytes, it spans from the address 0x0010 up to 0x0018. So, 0x0010 is the base address.

Knowing the base address helps us compute addresses for more complex data types, such as arrays and structs. For example, if we have a struct with three int type fields, and we want to access its 3rd field, we would calculate the address as base address + 8.

Base Register

When a register contains a base address for a value in memory, we refer to it as the base register. In the above example, rax is a base register.

Hands-on Example of Indirect Addressing Mode

Now, let’s put all of this together and write a simple program that reads an integer value from memory, multiplies it by 2, and exits with the result of the multiplication as its status code.

.data # create the .data section
# create a 64-bit integer value
ANSWER_TO_LIFE: .quad 42

.text
.globl _start
_start:
# copy the address to rax
movq $ANSWER_TO_LIFE, %rax # rax becomes the base register
# copy the value from memory address in rax into rdi
movq (%rax), %rdi
imulq $2, %rdi # multiply value in rdi by 2
movq $60, %rax # put exit syscall no in rax
syscall # execute exit syscall

You can assemble, link, and run it. I also encourage you to step through this in gdb to see that rax contains an address value after the first mov instruction.

Indirect Addressing and Pointers in C

When we use pointers in C, the compiler generates assembly code that uses indirect memory addressing to make it work. The C pointer syntax is just a syntactic sugar to hide this detail.

For example, if you call malloc or mmap to allocate memory in your C program, they return the address of the allocated memory. This return value is stored in the rax register as per the x86-64 calling convention. And, later when we dereference that memory using the * operator, the compiler generates assembly instructions which use indirect memory access. The following diagram shows this, but you can also test it out yourself using Compiler Explorer.

Exercise: Traverse a Linked List

This x86 assembly series is available to paid subscribers. If you’d like to continue reading the rest of this article and access the full series, you can upgrade your subscription.

Alternatively, you can purchase the x86 assembly book using the link below.

Get PDF

User's avatar

Continue reading this post for free, courtesy of Abhinav Upadhyay.

Or purchase a paid subscription.
© 2026 Abhinav Upadhyay · Publisher Privacy ∙ Publisher Terms
Substack · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture