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 type | Typical operand size | Encoding example (conceptual) |
|---|---|---|
| Absolute (32-bit) | 4 bytes | E9 00 40 40 00 (jump to fixed 0x00404000) |
| Relative (8-bit, “short” jump) | 1 byte | EB 05 (jump forward 5 bytes from here) |
| Relative (32-bit, “near” jump) | 4 bytes | E9 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:
- 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).
- 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
- Loop constructs: nearly every conditional/unconditional jump inside a loop body (
JNZ,JMP,LOOP) uses relative addressing because the target is nearby and it keeps code compact. - Position-independent shared libraries (.so on Linux, .dylib on macOS, .dll with relocations on Windows): rely heavily on relative addressing to be loadable at any base address.
- Self-relocating bootloaders: bootloader code often can’t assume where it will be loaded in early boot stages, so it uses relative addressing exclusively for internal jumps.
- Return-oriented programming (ROP) defenses and exploit mitigation: understanding absolute vs. relative addressing is essential when analyzing how ASLR and PIE (Position-Independent Executables) protect against certain classes of memory-corruption exploits.
Comparing Absolute and Relative Addressing
| Aspect | Absolute Addressing | Relative Addressing |
|---|---|---|
| Instruction size | Often larger (full address embedded) | Often smaller (small signed offset) |
| Portability across load addresses | Poor — needs relocation | Excellent — works anywhere |
| Compatibility with ASLR/PIE | Requires runtime patching | Works natively, no patching needed |
| Range | Unlimited (any address in address space) | Limited by offset field width (e.g., ±2GB for 32-bit relative on x86-64) |
| Typical use | Fixed-address embedded systems, legacy 16/32-bit code | Modern shared libraries, loops, PIE executables |
| Debuggability | Address is directly visible in disassembly | Requires 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
- Manually computing a “relative” offset incorrectly by forgetting it’s measured from the end of the current instruction, not its start.
- 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.
- 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.
- Writing non-PIC code and expecting it to work as a shared library — absolute addressing baked into a
.sofile requires costly load-time relocation and breaks memory sharing across processes.
Best Practices
- Prefer letting the assembler/compiler choose the addressing mode automatically (e.g., via labels) rather than hand-computing raw addresses.
- When writing shared libraries or PIE executables, always target RIP-relative (x86-64) or PC-relative (ARM) addressing — check your build flags (
-fPIC,-pie) if using a compiler toolchain around your Assembly. - Use relative jumps for local control flow (loops, conditionals) since they’re both more compact and more portable.
- Reserve absolute addressing for genuinely fixed hardware locations, such as memory-mapped I/O registers in embedded/firmware development.
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
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1, Section 3.7 (Addressing Modes) — intel.com/sdm
- AMD64 Architecture Programmer’s Manual, Volume 1 — amd.com
- ARM Architecture Reference Manual, “Addressing modes” — developer.arm.com
- System V Application Binary Interface, AMD64 Architecture Processor Supplement (PIC/PIE discussion) — gitlab.com/x86-psABIs
- GNU Assembler Manual, Addressing sections — sourceware.org/binutils
