x86-64 Registers, Addressing, and Position-Independent Code
x86-64 is the 64-bit extension of the x86 instruction-set architecture. AMD introduced it as AMD64; Intel implements a substantially compatible version under the name Intel 64. “x64” is a common shorthand, especially in Microsoft documentation, but it is not a separate architecture. This lesson uses x86-64 while naming AMD64, Intel 64, or Microsoft x64 when a vendor or ABI uses that name. We will examine only small, owned C examples and their ordinary compiler output; this is an exercise in understanding programs, not in constructing injected code.
Long mode and the extended register file
On a 64-bit operating system, ordinary 64-bit programs execute in 64-bit submode of long mode. Long mode adds 64-bit operand and address capabilities, eight general-purpose registers, RIP-relative addressing, and a revised instruction encoding. It does not make every operation 64 bits: compilers deliberately use 8-, 16-, 32-, and 64-bit instructions according to the C type and the required arithmetic.
| Width | Example names for the accumulator | Important effect of a write |
|---|---|---|
| 64 bit | RAX | Replaces all of RAX. |
| 32 bit | EAX | Writes the low 32 bits and zeroes the high 32 bits of RAX in 64-bit mode. |
| 16 bit | AX | Replaces only the low 16 bits. |
| 8 bit | AL, AH | Replaces only that byte; high-byte registers have encoding restrictions. |
The original eight registers are RAX, RBX, RCX, RDX, RSI, RDI, RBP, and RSP. Long mode adds R8 through R15. Their 32-bit spellings are R8D through R15D, and their lower portions have names such as R8W and R8B. RIP is the instruction pointer and RFLAGS holds condition flags. Vector registers are separate: XMM0–XMM15 exist in the baseline 64-bit register set, with wider YMM and ZMM views only when the relevant extensions are enabled.
A REX prefix makes the new registers and 64-bit operand sizes encodable. One consequence is easy to overlook: instructions needing a REX prefix cannot encode the legacy high-byte names AH, BH, CH, and DH. Let the assembler and compiler choose encodings rather than attempting to infer an instruction’s legality from a register name alone.
Extension is a type rule, not decoration
The EAX rule is one of the most useful facts when reading output. A compiler can produce a known 64-bit zero-extended unsigned value with a 32-bit write, avoiding a dependency on the old upper half:
; long widen(unsigned int n) { return n; }
; SysV AMD64: n arrives in EDI
mov eax, edi ; EAX write makes RAX equal to zero-extended EDI
ret
That is different from signed widening. For long widen(int n), output may use movsxd rax, edi, which sign-extends bit 31 through bits 32–63. For smaller source types, movzx zero-extends and movsx sign-extends. The ABI may already require a caller to extend an argument, but a compiler may still extend it again after loading from memory or when optimizing a calculation. Never decide signedness from register width alone; inspect the source type, the instruction, and subsequent comparisons.
Partial-register writes behave differently. Writing AL does not clear bits 8–63. Modern compilers generally avoid problematic partial-register patterns when a full value is needed, but hand-written code must establish every bit it later consumes. A 32-bit arithmetic operation also calculates modulo 232 and then zero-extends its result; that can be exactly the desired unsigned behavior, not an accidental truncation.
Addresses and canonical form
An address held in a 64-bit register is not automatically a valid virtual address. Current x86-64 implementations use fewer than 64 virtual-address bits. A canonical address has its unused high bits equal to a copy of the implemented top address bit. With a common 48-bit virtual-address width, bits 63 through 48 must copy bit 47; processors with five-level paging commonly use 57 bits instead. The operating system chooses mappings within the architectural and implementation limits. A non-canonical address faults when used as an address; it is not a useful “wrapped” pointer.
Canonicality is not a portable substitute for pointer validation. Code should retain typed pointers supplied by its own allocations and APIs, check array bounds in its own logic, and never manufacture pointers by guessing address ranges. Also distinguish an address-sized integer from a pointer: conversions have language and ABI rules, while arithmetic on a pointer outside its defined object can be invalid before hardware ever sees an address.
Effective addresses and LEA
Most memory operands use an effective-address expression: base plus index times scale plus displacement. In Intel syntax, qword ptr [rbx + rsi*4 + 16] means “load or store eight bytes at that calculated address.” The scale is 1, 2, 4, or 8; it is an addressing-mode feature, not an arbitrary multiply. Operand size belongs to the instruction or an explicit qualifier, since an address expression alone has no data width.
lea means load effective address, but it does not load memory. It performs the address arithmetic and writes the numerical result to a register without dereferencing the bracketed location:
; long three_n_plus_five(long n), n in RDI, result in RAX
lea rax, [rdi + rdi*2 + 5] ; RAX = 3*n + 5; no memory access occurs
ret
Compilers use LEA for addresses and sometimes for addition and small constant multiplies because it does not change condition flags. It is incorrect to translate every LEA as “take the address of a C variable”; it may be arithmetic. Conversely, it is incorrect to assume it is faster than every add sequence. The surrounding dependency chain and target microarchitecture decide performance.
RIP-relative code and position independence
In 64-bit mode, an instruction can address memory relative to the next instruction pointer. A compiler commonly emits code conceptually like this:
; static const int bias = 7;
; int read_bias(void) { return bias; }
mov eax, DWORD PTR [rip + bias] ; linker fills the displacement
ret
The encoded displacement is relative, so moving the code and its nearby static data together does not require embedding an absolute virtual address in this instruction. That makes RIP-relative addressing central to position-independent code (PIC). A shared library can be loaded at a different address each run without rewriting every internal reference. It complements, rather than replaces, normal relocation processing.
For an externally visible data object or function, the compiler may use an indirection defined by the object format and dynamic linker: on ELF systems, a Global Offset Table (GOT) entry and Procedure Linkage Table (PLT) are common; on PE/COFF systems, an import address table is common. The exact sequence depends on visibility, -fPIC, PIE settings, linker choices, and whether the target can be resolved locally. A call in a relocatable object may show a placeholder displacement and a relocation record instead of a final address. That is expected, not broken assembly.
A reproducible, owned-code observation
Put this in a file you own:
static int offset = 7;
int add_offset(int x) { return x + offset; }
On a GCC or Clang system, cc -O2 -fPIC -S -masm=intel sample.c -o sample.s requests Intel-syntax assembly. Compare it with cc -O2 -fPIC -c sample.c -o sample.o followed by objdump -dr -Mintel sample.o. The -r view is important: it prints relocations beside instructions, preventing an unresolved displacement from being misread as a final address. On LLVM installations, llvm-objdump -dr --x86-asm-syntax=intel sample.o offers a comparable view. Do not infer a fixed instruction sequence: optimization can fold the static value, select a different load, or remove the function when whole-program information is available.
Exercises
- For an owned function returning
unsigned int, find a 32-bit write and explain why its 64-bit register view has zero high bits. - Change a function’s parameter from
unsigned inttoint. Identify a zero- or sign-extension instruction, or explain why the ABI made it unnecessary at that point. - Compile one local and one external reference with and without PIC. Record the relocation type and symbol shown by your object inspection tool; do not patch or execute the object.
- Annotate an LEA in compiler output as arithmetic or an address calculation, citing the source expression that supports your conclusion.
Continue with x86-64 System V and Windows ABI Practice for the rules that assign those registers to calls, and Reading x86-64 Compiler Output and Object Files for relocations in context. The earlier x86 Assembly and Calling Conventions provides the baseline terminology.
dispelled