How Memory Addresses Are Represented in Assembly Language

How are memory addresses represented in Assembly language

Every variable, instruction, and piece of data your program touches lives somewhere in memory — and Assembly language is where you finally see exactly how that “somewhere” is expressed. If you’ve ever wondered what’s really happening behind a high-level array index or pointer dereference, this is where it all becomes concrete. Let’s walk through how memory addresses are represented in Assembly, from basic numbering systems to full addressing modes on x86-64 and ARM.

What Is a Memory Address?

A memory address is a unique numerical identifier for a specific byte location in a computer’s memory (RAM). Think of memory as a giant array of bytes, each with its own index — that index is the address. On a 64-bit system, addresses are typically represented as 64-bit values, though the actual usable address space is often smaller (commonly 48 bits are used in practice, with the rest reserved).

Memory addresses are almost universally represented in hexadecimal notation in Assembly language and debuggers, because hex maps cleanly onto binary (each hex digit = 4 bits) and is far more compact than binary while still being easy for programmers to reason about.

Binary:      0000000001000000000000000000000000000000000000
Hexadecimal: 0x400000

Basic Memory Layout Diagram

Before diving into addressing modes, it helps to visualize how a typical process’s memory is organized:

graph TB
    A["High Addresses"] --> B["Stack (grows downward)"]
    B --> C["Memory-Mapped Region / Shared Libraries"]
    C --> D["Heap (grows upward)"]
    D --> E["BSS Segment (uninitialized data)"]
    E --> F["Data Segment (initialized global/static data)"]
    F --> G["Text/Code Segment (program instructions)"]
    G --> H["Low Addresses"]

Each of these segments occupies a distinct range of memory addresses, and Assembly instructions reference specific addresses within them — whether that’s fetching an instruction from the text segment or reading a global variable from the data segment.

Representing Addresses in Assembly Syntax

Different assemblers use different syntax conventions, but the core ideas are the same. The two dominant syntax styles for x86/x86-64 are AT&T syntax (used by GNU Assembler/GAS) and Intel syntax (used by NASM and MASM).

Syntax StyleExampleNotes
Intel (NASM)mov eax, [ebx]Destination first, brackets denote memory reference
AT&T (GAS)movl (%ebx), %eaxSource first, parentheses denote memory reference, registers prefixed with %

This post primarily uses Intel syntax (NASM-style) for clarity, since it’s more common in modern tutorials, but ARM syntax is shown in its standard form as well.

Direct (Absolute) Addressing

The simplest form: the instruction directly specifies a fixed memory address.

mov eax, [0x400000]   ; load the value stored at address 0x400000 into eax

This is straightforward but inflexible — hardcoding a numeric address is rare in real programs since it breaks portability and works against techniques like ASLR (Address Space Layout Randomization).

Register Indirect Addressing

Here, a register holds the memory address, and the instruction accesses the memory location that register points to.

mov eax, [ebx]    ; ebx holds an address; load the value at that address into eax

This is extremely common — it’s how pointers work at the Assembly level. If ebx contains 0x7ffeead2, then [ebx] means “the value stored at address 0x7ffeead2.”

Base + Displacement (Offset) Addressing

This mode adds a constant offset to a base register’s value, which is essential for accessing structure fields or array elements at known fixed offsets.

mov eax, [ebx + 4]     ; load value at address (ebx + 4)

If ebx points to the start of a structure, [ebx + 4] might represent the second 4-byte field of that structure.

Indexed Addressing (Base + Index)

Used heavily for array traversal, this mode combines a base register (array start) with an index register (current position).

mov eax, [ebx + ecx]         ; base ebx, index ecx
mov eax, [ebx + ecx*4]       ; base ebx, index ecx scaled by 4 (common for int arrays)

The *4 scale factor is especially useful because a 4-byte int array means element i sits at base + i*4. x86 supports scale factors of 1, 2, 4, and 8, matching common data type sizes (byte, word, dword, qword).

Full x86-64 Addressing Mode Syntax

mov eax, [base + index*scale + displacement]

Example:

mov eax, [rbx + rcx*4 + 8]

This computes the effective address as: rbx + (rcx * 4) + 8.

RIP-Relative Addressing (x86-64 Specific)

x86-64 introduced RIP-relative addressing, which computes an address relative to the current instruction pointer (RIP). This is crucial for position-independent code, since it allows referencing data without needing an absolute address baked into the instruction.

mov eax, [rip + some_variable]   ; address = current RIP + offset to some_variable

This is why modern 64-bit Linux binaries (compiled with -fPIC) rely so heavily on RIP-relative addressing — it lets the OS load the binary at any base address (thanks to ASLR) without needing to patch every single memory reference.

ARM Addressing Modes

ARM (both 32-bit and AArch64) uses a load/store architecture, meaning only specific ldr (load) and str (store) instructions can access memory — arithmetic instructions only work on registers.

Immediate offset:

ldr x0, [x1, #8]      ; load from address (x1 + 8) into x0

Register offset:

ldr x0, [x1, x2]      ; load from address (x1 + x2) into x0

Pre-indexed (updates base register before use):

ldr x0, [x1, #8]!     ; x1 = x1 + 8, then load from new x1 into x0

Post-indexed (updates base register after use):

ldr x0, [x1], #8      ; load from x1 into x0, THEN x1 = x1 + 8

Pre- and post-indexed addressing are especially useful for looping through arrays, since they combine the memory access and the pointer increment into a single instruction.

Comparison Table: Addressing Modes

Addressing Modex86-64 ExampleARM ExampleTypical Use Case
Direct/Absolutemov eax, [0x400000](rare, usually via register)Fixed global variables (legacy code)
Register Indirectmov eax, [ebx]ldr x0, [x1]Pointer dereference
Base + Displacementmov eax, [ebx+4]ldr x0, [x1, #4]Struct field access
Indexed (Base+Index*Scale)mov eax, [ebx+ecx*4]ldr x0, [x1, x2, lsl #2]Array traversal
RIP-Relativemov eax, [rip+var]N/A (uses adr/adrp)Position-independent code
Pre/Post-IndexedN/A (less common)ldr x0, [x1], #8Loop-based pointer walking

How the CPU Computes Effective Addresses

Internally, the CPU’s Address Generation Unit (AGU) computes the final “effective address” from these components before the memory access actually happens.

flowchart LR
    A[Base Register Value] --> D[Address Generation Unit]
    B["Index Register × Scale"] --> D
    C[Displacement/Offset] --> D
    D --> E[Effective Memory Address]
    E --> F[Memory / Cache Access]

This calculation happens extremely fast — often in a single clock cycle — because it’s a dedicated hardware unit separate from the main ALU.

Virtual Addresses vs Physical Addresses

Everything discussed so far describes how addresses look and behave from the perspective of a running program — but there’s an important layer of translation happening underneath that every Assembly programmer should understand: virtual memory.

The addresses your Assembly instructions reference ([rbx], [rip+var], and so on) are almost always virtual addresses, not the actual physical location in RAM chips. The operating system, working with the CPU’s Memory Management Unit (MMU), translates virtual addresses into physical addresses transparently, using page tables.

graph LR
    A["Virtual Address (used in Assembly instructions)"] --> B["MMU - Memory Management Unit"]
    B --> C["Page Table Lookup"]
    C --> D["Physical Address (actual RAM location)"]
    D --> E["Physical Memory Access"]

This translation happens for essentially every memory access, though it’s cached in a hardware structure called the Translation Lookaside Buffer (TLB) to avoid the overhead of a full page table walk on every single access. From your Assembly code’s point of view, none of this is visible — you simply write mov eax, [rbx], and the hardware and OS handle the virtual-to-physical translation behind the scenes.

This abstraction is what makes several important OS features possible:

It’s worth noting that a small category of Assembly programming — writing OS kernels, bootloaders, or device drivers — does need to work directly with physical addresses, particularly during early boot before the MMU and page tables are even set up. In that context, understanding the distinction between virtual and physical addressing isn’t just academic; it’s essential to getting the system to boot at all.

Practical Use Cases

A Closer Look: Addressing Within a Stack Frame

One of the most common real-world uses of base+displacement addressing is accessing local variables and function parameters within a stack frame. Walking through a concrete example ties together everything covered above.

Consider this simple C function:

int compute(int a, int b) {
    int sum = a + b;
    int doubled = sum * 2;
    return doubled;
}

When compiled (unoptimized) for x86-64, the function typically sets up a stack frame like this:

compute:
    push rbp
    mov rbp, rsp          ; rbp now points to the base of this stack frame
    sub rsp, 16            ; allocate 16 bytes for local variables

    mov [rbp-4], edi       ; store parameter a at rbp-4
    mov [rbp-8], esi       ; store parameter b at rbp-8

    mov eax, [rbp-4]       ; load a
    add eax, [rbp-8]       ; add b
    mov [rbp-12], eax      ; store sum at rbp-12

    mov eax, [rbp-12]      ; load sum
    add eax, eax           ; double it
    mov [rbp-16], eax      ; store doubled at rbp-16

    mov eax, [rbp-16]      ; return value goes in eax
    leave                  ; restore rsp/rbp
    ret

Here’s a visual of how these variables map onto actual stack memory addresses relative to rbp:

AddressContent
rbp - 4Parameter a
rbp - 8Parameter b
rbp - 12Local variable sum
rbp - 16Local variable doubled
rbp (base)Saved previous rbp value
rbp + 8Return address (pushed by call)

This pattern — using negative offsets from rbp for locals/parameters, and positive offsets for the return address and any stack-passed arguments beyond what fits in registers — is nearly universal across x86-64 compiled code, and understanding it is essential for reading stack traces, using a debugger effectively, or manually writing Assembly functions that need to interoperate with C. ARM uses an analogous pattern with its frame pointer (X29) and stack pointer (SP), though the exact offsets and calling convention details differ per the AAPCS64 standard.

Debugging Memory Addresses

In GDB, examining memory addresses is one of the most common debugging tasks:

(gdb) x/4xw 0x7ffeead2       # examine 4 words in hex at address 0x7ffeead2
(gdb) print &myVariable       # get address of a variable
(gdb) print *(int*)0x400000   # dereference an address as an int

Understanding addressing modes makes it far easier to read disassembled code (via objdump -d or Ghidra) and understand what a compiled program is actually doing with memory.

Troubleshooting Address-Related Bugs

A few recurring troubleshooting patterns come up repeatedly when working with memory addresses in Assembly, and recognizing them quickly can save hours of debugging:

Common Mistakes

  1. Confusing the value in a register with the address it representsmov eax, ebx copies a value, while mov eax, [ebx] dereferences an address.
  2. Off-by-one errors in scale factors — using the wrong scale (e.g., *2 instead of *4) when indexing into an int array leads to incorrect memory access.
  3. Forgetting alignment requirements — some architectures fault or slow down significantly on misaligned memory accesses.
  4. Hardcoding absolute addresses — breaks with ASLR and non-PIE vs PIE binary differences; almost always avoided in modern code.

Best Practices

FAQs

Q: Why are memory addresses shown in hexadecimal instead of decimal? Hex maps cleanly to binary (4 bits per digit), making it far easier to reason about memory layout, bit patterns, and alignment than decimal would be.

Q: What’s the difference between a memory address and a pointer? A pointer, in high-level languages, is a variable that stores a memory address. In Assembly, there’s no distinct “pointer type” — it’s just a register or memory location holding a numeric address.

Q: Why does x86-64 use RIP-relative addressing? It enables position-independent code, which is required for ASLR and shared libraries to work efficiently without patching every address reference at load time.

Q: Are addressing modes the same across all CPU architectures? No — while the general concepts (direct, indirect, indexed) are common, the exact syntax and available combinations differ significantly between architectures like x86-64 and ARM.

Summary and Key Takeaways

References

Exit mobile version