Absolute Addressing vs. Relative Addressing in Assembly Language

Differentiate between absolute addressing and relative addressing

One of the topics that genuinely changed how I think about programs — not just Assembly, but software in general — is addressing modes, specifically the difference between absolute and relative addressing. It sounds like a small technical detail, but it actually explains why modern operating systems can load the same program at different memory locations every single time you run it (a security feature called ASLR), and why position-independent code became the default in modern compilers. Let’s dig into both.

What Is an Addressing Mode?

An addressing mode is simply the method the CPU uses to figure out where to find an operand — whether that’s a value in a register, a constant, or (most relevant here) a location in memory. When an instruction needs to reference memory — to jump to code, or to load/store data — it has to specify that memory location somehow, and there are fundamentally two philosophies for doing so: absolute and relative.

Absolute Addressing

Absolute addressing (also called direct addressing) means the instruction specifies the exact, fixed memory address of the operand. The address doesn’t depend on where the current instruction is located — it’s a hardcoded number.

; x86 (32-bit) absolute addressing example
mov eax, [0x00404000]      ; load the 4 bytes stored at fixed address 0x00404000
jmp 0x00401050               ; jump directly to a fixed address
; Referencing a labeled global variable also compiles to an
; absolute address in non-PIC (non-position-independent) code
section .data
counter dd 0

section .text
mov eax, [counter]         ; assembler resolves 'counter' to a fixed address

The upside of absolute addressing is simplicity — the address is baked directly into the instruction, so there’s no extra computation at runtime. The downside is inflexibility: if the program (or a shared library) gets loaded at a different base address than expected, every absolute reference becomes wrong unless it’s patched by the loader (a process called relocation).

Relative Addressing

Relative addressing (also called PC-relative or RIP-relative addressing on x86-64) specifies the target as an offset from the current instruction pointer (RIP on x86-64, PC on ARM), rather than as a fixed absolute value.

; x86-64 RIP-relative addressing (the default in 64-bit mode for many cases)
lea rax, [rip + counter]     ; rax = address of 'counter', computed relative to RIP
mov eax, [rip + counter]      ; load the value at 'counter' via RIP-relative offset
; Relative jump/branch — very common for loops and conditionals
loop_start:
    dec ecx
    jnz loop_start            ; JNZ encodes a signed offset relative to the next instruction

Under the hood, a relative jump like JNZ loop_start doesn’t store the absolute address of loop_start at all — it stores a small signed integer (how many bytes forward or backward to jump from the end of the current instruction). This is why relative jumps are typically far more compact in machine code than absolute jumps.

Instruction Encoding Comparison

Addressing typeTypical operand sizeEncoding example (conceptual)
Absolute (32-bit)4 bytesE9 00 40 40 00 (jump to fixed 0x00404000)
Relative (8-bit, “short” jump)1 byteEB 05 (jump forward 5 bytes from here)
Relative (32-bit, “near” jump)4 bytesE9 05 00 00 00 (jump forward 5 bytes, larger range)

Why RIP-Relative Addressing Became the Default in x86-64

This is genuinely one of my favorite pieces of Assembly trivia. In 32-bit x86, absolute addressing was standard — programs were typically loaded at predictable, fixed base addresses, so hardcoding addresses worked fine. But two things changed that:

  1. Shared libraries needed to be loadable at different addresses in different processes simultaneously, without needing every absolute address inside them patched at load time (expensive and it breaks copy-on-write sharing between processes).
  2. Address Space Layout Randomization (ASLR) became a standard security mitigation — the OS deliberately loads executables and libraries at randomized base addresses on every run, to make memory-corruption exploits harder to write.

x86-64 responded to this by adding true RIP-relative addressing as a first-class addressing mode, letting compilers generate Position-Independent Code (PIC) — code that works correctly no matter where in memory it gets loaded, because every internal reference is relative to the current instruction pointer rather than a fixed absolute address.

flowchart TD
    A[Program loaded into memory] --> B{Addressing mode used?}
    B -->|Absolute| C[Address hardcoded at compile time]
    C --> D{Load address matches expectation?}
    D -->|No, e.g. due to ASLR| E[Loader must patch every absolute reference - relocation]
    D -->|Yes| F[Works directly, no patching needed]
    B -->|Relative / RIP-relative| G[Address computed as offset from current instruction pointer]
    G --> H[Works correctly regardless of load address - no patching needed]

Addressing Modes on ARM

ARM also distinguishes between absolute-style and PC-relative addressing, though the terminology and mechanics differ slightly.

; ARM PC-relative literal load (very common for constants too large to encode directly)
LDR R0, =0x12345678        ; assembler emits a PC-relative load from a literal pool

; ARM branch instructions are inherently PC-relative
B   loop_start                ; encodes a signed offset from PC
BL  my_function                 ; same, but also saves return address in LR

ARM’s branch instructions (B, BL, BEQ, BNE, etc.) are always PC-relative by design — there’s no “absolute branch” instruction in the classic ARM ISA. To branch to a truly arbitrary absolute address, you load that address into a register first and use BX/BLX (branch and exchange, using a register operand) instead.

LDR R0, =some_absolute_address
BX  R0                            ; branch to the absolute address held in R0

Practical Use Cases

Comparing Absolute and Relative Addressing

AspectAbsolute AddressingRelative Addressing
Instruction sizeOften larger (full address embedded)Often smaller (small signed offset)
Portability across load addressesPoor — needs relocationExcellent — works anywhere
Compatibility with ASLR/PIERequires runtime patchingWorks natively, no patching needed
RangeUnlimited (any address in address space)Limited by offset field width (e.g., ±2GB for 32-bit relative on x86-64)
Typical useFixed-address embedded systems, legacy 16/32-bit codeModern shared libraries, loops, PIE executables
DebuggabilityAddress is directly visible in disassemblyRequires computing target from RIP/PC + offset

Debugging Tips

When you’re stepping through disassembly in GDB or objdump and see something like:

0x0000000000401136 <+13>:  lea    0x2ec3(%rip),%rax   # 0x404000 <counter>

That comment after the # is the disassembler helpfully computing the absolute address for you — the actual encoded instruction only contains the relative offset 0x2ec3. If you ever manually decode RIP-relative instructions by hand, remember the offset is calculated from the address of the next instruction, not the current one.

Common Mistakes

  1. Manually computing a “relative” offset incorrectly by forgetting it’s measured from the end of the current instruction, not its start.
  2. Assuming absolute addresses in a binary are stable — under ASLR, they change on every execution, so hardcoding an address you found once in a debugger for a permanent patch will fail.
  3. Forgetting that far jumps/calls may need relative displacement fields wider than 8 bits — a “short” jump only covers a limited range (-128 to +127 bytes); the assembler will throw an error or switch encodings if your target is farther away.
  4. Writing non-PIC code and expecting it to work as a shared library — absolute addressing baked into a .so file requires costly load-time relocation and breaks memory sharing across processes.

Best Practices

FAQs

Q: Is relative addressing always more efficient than absolute addressing? Usually in terms of code size, yes, since offsets are smaller than full addresses. But it’s not strictly “faster” at execution time — the real win is portability and reduced need for runtime relocation.

Q: Why can’t ARM branch instructions use absolute addressing? By design — ARM’s ISA was built to minimize instruction size and maximize position independence, so branches are baked as PC-relative from the start. Absolute branches require an explicit load-then-branch-to-register sequence.

Q: Does RIP-relative addressing have a range limit? Yes — on x86-64, the RIP-relative displacement field is typically 32 bits (signed), giving roughly a ±2GB reach from the current instruction, which is why extremely large executables occasionally need special handling.

Q: How does this relate to ASLR? ASLR relies on code being position-independent (i.e., using relative addressing internally) so the OS can load it at a randomized base address without having to patch every single internal reference.

Summary and Key Takeaways

Absolute addressing hardcodes a fixed memory address directly into an instruction, while relative addressing expresses a target as an offset from the current instruction pointer. What looks like a minor encoding detail turns out to underpin some of the most important developments in modern software security and system design — from shared library loading to Address Space Layout Randomization. If you’re writing Assembly today, especially for x86-64 or ARM, understanding when and why to use relative addressing isn’t optional trivia; it’s foundational to writing code that actually works correctly in a modern, security-hardened operating environment.

References

Exit mobile version