x86/x86_64 Architecture
Introduction
The x86 architecture, originally designed by Intel in 1978 for the 8086 processor, has become the most commercially successful instruction set architecture in history. Its 64-bit extension, x86_64 (also called AMD64 or Intel 64), dominates server, desktop, and laptop computing. Understanding x86/x86_64 is essential for Linux kernel developers, as it’s the primary development platform and the most widely deployed architecture.
This chapter covers the architecture’s key features: registers, addressing modes, segmentation, paging, privilege rings, and how the Linux kernel uses these facilities.
Architecture Overview
Evolution of x86
graph LR
I8086["8086<br>1978<br>16-bit"] --> I286["80286<br>1982<br>Protected Mode"]
I286 --> I386["80386<br>1985<br>32-bit"]
I386 --> I486["80486<br>1989<br>Pipelined"]
I486 --> PENTIUM["Pentium<br>1993<br>Superscalar"]
PENTIUM --> P6["Pentium Pro<br>1995<br>Out-of-order"]
P6 --> X86_64["x86_64<br>2003<br>64-bit<br>(AMD)"]
X86_64 --> MODERN["Modern x86_64<br>AVX-512, AMX<br>Hybrid cores"]
style X86_64 fill:#f96,stroke:#333,stroke-width:2px
style MODERN fill:#bbf,stroke:#333,stroke-width:2px
x86_64 Key Features
x86_64 Architecture Highlights
───────────────────────────────
Registers: 16 general-purpose (64-bit), 16 SSE/AVX (128/256/512-bit)
Address space: 48-bit virtual (256 TB), up to 52-bit physical (4 PB)
Page sizes: 4KB, 2MB (huge), 1GB (gigantic)
Privilege levels: 4 rings (0-3), typically only 0 (kernel) and 3 (user)
Instructions: CISC (variable-length, 1-15 bytes)
Endianness: Little-endian
Extensions: SSE, SSE2, AVX, AVX2, AVX-512, AMX, BMI, etc.
Registers
General-Purpose Registers
x86_64 has 16 general-purpose registers (extended from 8 in 32-bit):
graph TB
subgraph "General Purpose Registers (64-bit)"
RAX["RAX<br>Accumulator<br>Return value"]
RBX["RBX<br>Base<br>Callee-saved"]
RCX["RCX<br>Counter<br>Arg 4 (Windows)"]
RDX["RDX<br>Data<br>Arg 3 (Windows)"]
RSI["RSI<br>Source Index<br>Arg 2"]
RDI["RDI<br>Dest Index<br>Arg 1"]
RBP["RBP<br>Base Pointer<br>Callee-saved"]
RSP["RSP<br>Stack Pointer"]
R8["R8<br>Arg 5"]
R9["R9<br>Arg 6"]
R10["R10<br>Scratch"]
R11["R11<br>Scratch"]
R12["R12<br>Callee-saved"]
R13["R13<br>Callee-saved"]
R14["R14<br>Callee-saved"]
R15["R15<br>Callee-saved"]
end
subgraph "Special Registers"
RIP["RIP<br>Instruction Pointer"]
RFLAGS["RFLAGS<br>Status Flags"]
end
subgraph "Segment Registers"
CS["CS<br>Code Segment"]
SS["SS<br>Stack Segment"]
DS["DS<br>Data Segment"]
ES["ES<br>Extra Segment"]
FS["FS<br>Thread-local storage"]
GS["GS<br>Per-CPU data (kernel)"]
end
style RAX fill:#f96,stroke:#333
style RSP fill:#bbf,stroke:#333
style RIP fill:#9f9,stroke:#333
Register Naming Conventions
Register Size Access
────────────────────
RAX 64-bit Full register
EAX 32-bit Lower 32 bits (zero-extends to 64)
AX 16-bit Lower 16 bits
AL 8-bit Lower 8 bits
AH 8-bit Bits 8-15 (only for legacy regs)
Example: RAX = 0x123456789ABCDEF0
EAX = 0x9ABCDEF0
AX = 0xDEF0
AL = 0xF0
AH = 0xDE
New registers (R8-R15):
R8 64-bit
R8D 32-bit (lower 32)
R8W 16-bit (lower 16)
R8B 8-bit (lower 8)
Segment Registers in 64-bit Mode
/*
* In x86_64 long mode, segmentation is mostly disabled.
* CS, DS, ES, SS are flat (base=0, limit=max).
* FS and GS retain base addresses (stored in MSRs).
*
* Linux uses:
* GS.base (kernel) → per-CPU data area
* FS.base (user) → thread-local storage (TLS)
*/
/* Reading GS.base in kernel code */
static inline unsigned long cpu_read_gs_base(void)
{
return __rdmsr(MSR_GS_BASE);
}
/* Setting FS.base for user TLS */
static inline void wrfsbase(unsigned long addr)
{
asm volatile("wrfsbase %0" :: "r"(addr));
}
Addressing Modes
Address Calculation
x86_64 Addressing Modes
───────────────────────
Effective Address = Segment Base + Base + (Index × Scale) + Displacement
Components:
Base: Any GPR
Index: Any GPR except RSP
Scale: 1, 2, 4, or 8
Displacement: 8-bit, 16-bit, or 32-bit immediate
Examples (AT&T syntax):
movq (%rax), %rbx # [RAX]
movq 8(%rax), %rbx # [RAX + 8]
movq (%rax,%rcx), %rbx # [RAX + RCX]
movq (%rax,%rcx,4), %rbx # [RAX + RCX*4]
movq 8(%rax,%rcx,4), %rbx # [RAX + RCX*4 + 8]
movq symbol(%rip), %rbx # RIP-relative (PC-relative)
RIP-Relative Addressing
; x86_64 uses RIP-relative addressing by default for position-independent code
; This is essential for shared libraries and KASLR
; Load variable relative to instruction pointer
mov rax, [rel my_variable] ; RIP-relative
; In Linux kernel code:
; All kernel code is compiled with -mcmodel=kernel (negative 2GB range)
; KASLR randomizes the kernel base address
Segmentation
Historical Context
Segmentation Evolution
───────────────────────
8086 (Real Mode):
Physical address = Segment × 16 + Offset
20-bit address space (1 MB)
80286 (Protected Mode):
Segment descriptors with base, limit, and permissions
24-bit address space (16 MB)
80386 (32-bit Protected Mode):
32-bit segments
Flat memory model possible
4 GB address space
x86_64 (Long Mode):
Segmentation essentially disabled (flat model)
CS, DS, ES, SS: base=0, limit=max
FS, GS: functional (used for TLS and per-CPU data)
Segment Descriptor
/* Segment descriptor (8 bytes, 32-bit mode) */
struct segment_desc {
uint16_t limit_0_15; /* Limit bits 0-15 */
uint16_t base_0_15; /* Base bits 0-15 */
uint8_t base_16_23; /* Base bits 16-23 */
uint8_t type:4; /* Segment type */
uint8_t s:1; /* Descriptor type (0=system, 1=code/data) */
uint8_t dpl:2; /* Descriptor privilege level */
uint8_t p:1; /* Present */
uint8_t limit_16_19:4; /* Limit bits 16-19 */
uint8_t avl:1; /* Available for use */
uint8_t l:1; /* Long mode (1=64-bit) */
uint8_t d:1; /* Default operand size */
uint8_t g:1; /* Granularity (0=byte, 1=4KB) */
uint8_t base_24_31; /* Base bits 24-31 */
} __attribute__((packed));
Paging
Paging Modes
graph TD
subgraph "4-Level Paging (Standard)"
PML4["PML4 (Page Map Level 4)<br>512 entries × 512GB = 256TB"]
PDP["PDPT (Page Directory Pointer Table)<br>512 entries × 1GB"]
PD["PD (Page Directory)<br>512 entries × 2MB"]
PT["PT (Page Table)<br>512 entries × 4KB"]
PAGE["4KB Page"]
PML4 --> PDP --> PD --> PT --> PAGE
end
subgraph "5-Level Paging (Ice Lake+)"
PML5["PML5<br>512 entries"]
PML4B["PML4"]
PDPB["PDPT"]
PDB["PD"]
PTB["PT"]
PAGEB["4KB Page"]
PML5 --> PML4B --> PDPB --> PDB --> PTB --> PAGEB
end
style PML4 fill:#f96,stroke:#333
style PML5 fill:#bbf,stroke:#333
Page Table Entry
/* x86_64 page table entry (8 bytes) */
struct page_table_entry {
uint64_t present:1; /* Page is present in memory */
uint64_t writable:1; /* Page is writable */
uint64_t user_access:1; /* User-mode access allowed */
uint64_t page_write_through:1; /* Write-through caching */
uint64_t page_cache_disable:1; /* Disable caching */
uint64_t accessed:1; /* Page has been accessed */
uint64_t dirty:1; /* Page has been written to */
uint64_t huge_page:1; /* 2MB page (in PD) or 1GB (in PDPT) */
uint64_t global:1; /* Not flushed on CR3 switch */
uint64_t available:3; /* OS-available bits */
uint64_t page_frame:40; /* Physical page frame number */
uint64_t available2:7; /* More OS-available bits */
uint64_t protection_key:4; /* Protection key (PKU) */
uint64_t execute_disable:1; /* No-execute (NX) bit */
};
Linux Page Table Levels
/* Linux uses these macros for page table manipulation */
/* 4-level page tables */
#define PGDIR_SHIFT 39 /* PML4 level */
#define PUD_SHIFT 30 /* PDPT level */
#define PMD_SHIFT 21 /* PD level */
#define PAGE_SHIFT 12 /* PT level (4KB pages) */
/* Page table sizes */
#define PTRS_PER_PGD 512 /* Entries in PML4 */
#define PTRS_PER_PUD 512 /* Entries in PDPT */
#define PTRS_PER_PMD 512 /* Entries in PD */
#define PTRS_PER_PTE 512 /* Entries in PT */
/* Address masking */
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PGDIR_MASK (~(PGDIR_SIZE-1))
/* Converting addresses to page table indices */
static inline pgd_t *pgd_offset(pgd_t *pgd, unsigned long address)
{
return pgd + pgd_index(address);
}
static inline unsigned long pgd_index(unsigned long address)
{
return (address >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1);
}
Privilege Rings
Ring Structure
graph TB
subgraph "Protection Rings"
RING0["Ring 0 (Kernel Mode)<br>Full access to all resources<br>All instructions available"]
RING1["Ring 1<br>(Rarely used)"]
RING2["Ring 2<br>(Rarely used)"]
RING3["Ring 3 (User Mode)<br>Restricted access<br>Must use syscalls for kernel services"]
end
RING0 -->|syscall/sysret| RING3
RING0 -->|interrupt| RING3
style RING0 fill:#f96,stroke:#333,stroke-width:2px
style RING3 fill:#bbf,stroke:#333,stroke-width:2px
Privilege Ring Usage
────────────────────
Ring 0 (Kernel):
• Linux kernel code
• Device drivers
• Full access to all memory
• Can execute privileged instructions (CLI, STI, HLT, IN, OUT)
• Access to all I/O ports
• Can modify page tables, control registers
Ring 1, 2 (Unused by Linux):
• Could be used for device drivers (theory)
• Linux keeps everything in Ring 0 or Ring 3
• Some hypervisors use Ring 1 for guest kernels
Ring 3 (User):
• All user applications
• Cannot execute privileged instructions
• Cannot directly access I/O ports
• Cannot modify page tables
• Must use syscalls to request kernel services
• Protected memory from other Ring 3 processes
System Calls
; x86_64 system call convention (System V ABI)
; Arguments: RDI, RSI, RDX, R10, R8, R9
; System call number: RAX
; Return value: RAX
; Clobbered: RCX, R11
; Example: write(1, "hello\n", 6)
section .data
msg db "hello", 10
section .text
global _start
_start:
mov rax, 1 ; syscall number: sys_write
mov rdi, 1 ; fd: stdout
lea rsi, [rel msg] ; buf: pointer to string
mov rdx, 6 ; count: 6 bytes
syscall ; invoke kernel
mov rax, 60 ; syscall number: sys_exit
xor rdi, rdi ; status: 0
syscall
/* Kernel-side syscall entry (simplified) */
/* arch/x86/entry/entry_64.S */
/*
* Syscall entry point for 64-bit programs.
* Entered via SYSCALL instruction.
*/
entry_SYSCALL_64:
/* Save user context */
swapgs /* Switch to kernel GS base */
mov [gs:cpu_tss_sp0], rsp /* Save user RSP */
mov rsp, [gs:cpu_tss_sp1] /* Load kernel RSP */
/* Save registers to pt_regs */
push rdi
push rsi
push rdx
/* ... save all registers ... */
/* Look up syscall handler */
mov rax, [rsp + ORIG_RAX]
call [sys_call_table + rax*8]
/* Return to userspace */
/* ... restore registers ... */
sysretq
Kernel Memory Layout
x86_64 Virtual Address Space
x86_64 Linux Virtual Address Space (4-level paging)
────────────────────────────────────────────────────
0xFFFFFFFFFFFFFFFF ┌─────────────────────┐
│ Kernel space │ -128 TB (or -16 EB with 5-level)
│ (direct mapping) │
0xFFFF888000000000 ├─────────────────────┤
│ Physical memory map │ 64 TB
│ (page_offset_base) │
0xFFFF800000000000 ├─────────────────────┤
│ vmalloc/ioremap │ 8 TB
0xFFFF000000000000 ├─────────────────────┤
│ KASAN shadow │ 8 TB (if enabled)
│ ... │
0x00007FFFFFFFFFFF ├─────────────────────┤
│ User space │ 128 TB (47-bit)
│ (mmap, heap, stack) │
0x0000000000000000 └─────────────────────┘
Direct Mapping
/* Linux maps all physical memory directly into kernel virtual space */
/* This is the "direct mapping" or "linear mapping" */
/* Convert between physical and virtual addresses */
#define __pa(x) ((unsigned long)(x) - PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))
/* PAGE_OFFSET on x86_64 is 0xFFFF888000000000 */
/* This means physical address 0 maps to 0xFFFF888000000000 */
/* Physical address 0x1000 maps to 0xFFFF888000001000 */
/* etc. */
/* Example usage in kernel code */
struct page *page = alloc_page(GFP_KERNEL);
void *vaddr = page_address(page); /* Kernel virtual address */
phys_addr_t paddr = page_to_phys(page); /* Physical address */
/* Or equivalently */
void *vaddr2 = __va(paddr);
phys_addr_t paddr2 = __pa(vaddr);
KASLR (Kernel Address Space Layout Randomization)
# KASLR randomizes the kernel load address at boot time
# This makes exploiting kernel vulnerabilities harder
# Check if KASLR is enabled
$ cat /proc/cmdline | grep -o nokaslr
# (no output = enabled)
# Disable KASLR (for debugging)
# Add "nokaslr" to kernel command line in GRUB
# Check randomized addresses
$ sudo cat /proc/kallsyms | head -5
ffffffff81000000 T _text # Changes every boot with KASLR
ffffffff81001000 T startup_64
ffffffff81001000 T secondary_startup_64
CPU Feature Detection
/* Linux detects CPU features at boot and stores them */
/* Check for CPUID features */
static inline bool cpu_has_sse2(void)
{
return boot_cpu_has(X86_FEATURE_SSE2);
}
static inline bool cpu_has_avx2(void)
{
return boot_cpu_has(X86_FEATURE_AVX2);
}
static inline bool cpu_has_avx512f(void)
{
return boot_cpu_has(X86_FEATURE_AVX512F);
}
/* CPU feature flags in /proc/cpuinfo */
$ grep flags /proc/cpuinfo | head -1
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov
pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe
syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good
nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64
monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1
sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand
lahf_lm cpuid_fault epb invpcid_single ssbd ibrs ibpb stibp
tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1
avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
md_clear flush_l1d
x86-Specific Kernel Features
Spectre/Meltdown Mitigations
# Check active mitigations
$ cat /sys/devices/system/cpu/vulnerabilities/meltdown
Mitigation: PTI
$ cat /sys/devices/system/cpu/vulnerabilities/spectre_v1
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
$ cat /sys/devices/system/cpu/vulnerabilities/spectre_v2
Mitigation: Retpolines, IBPB: conditional, IBRS_FW, STIBP: conditional, RSB filling
# Full mitigation status
$ grep . /sys/devices/system/cpu/vulnerabilities/*
Performance Counters
# x86 has hardware performance counters (PMU)
# Access via perf tool
$ perf stat -e cycles,instructions,cache-misses,branch-misses -- ./program
Performance counter stats for './program':
1,234,567,890 cycles
987,654,321 instructions # 0.80 insn per cycle
1,234,567 cache-misses
234,567 branch-misses
# Specific x86 PMU events
$ perf list | grep -i "intel"
cpu/event=0x3c,umask=0x00/ [Kernel PMU event]
cpu/event=0xc0,umask=0x00/ [Kernel PMU event]
...
Linux Kernel x86 Code Organization
arch/x86/
├── boot/ — Boot code (bzImage creation)
├── entry/ — Syscall and interrupt entry points
├── events/ — Performance monitoring (perf)
├── hyperv/ — Hyper-V support
├── include/ — x86-specific headers
├── kernel/ — Core kernel (process, signal, time)
├── kvm/ — KVM virtualization (VMX/SVM)
├── lib/ — x86-optimized library routines
├── math-emu/ — FPU emulation
├── mm/ — Memory management
├── net/ — x86-specific networking (BPF JIT)
├── pci/ — PCI support
├── platform/ — Platform-specific code
├── power/ — ACPI/power management
├── tools/ — x86 userspace tools
├── um/ — User-Mode Linux
├── vdso/ — vDSO (virtual dynamic shared object)
├── video/ — Video/framebuffer support
├── xen/ — Xen hypervisor support
├── Kconfig — x86 configuration options
├── Kconfig.debug — Debug configuration
└── Makefile — Build rules
References and Further Reading
-
Intel 64 and IA-32 Architectures Software Developer Manuals: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
-
AMD64 Architecture Programmer’s Manual: https://www.amd.com/en/support/tech-docs
-
x86_64 System V ABI: https://gitlab.com/x86-psABIs/x86-64-ABI
-
Linux kernel x86 documentation: https://www.kernel.org/doc/html/latest/arch/x86/
-
OSDev Wiki — x86-64: https://wiki.osdev.org/X86-64
-
“Intel 64 and IA-32 Architectures Optimization Reference Manual”
-
Linux kernel arch/x86 source: https://elixir.bootlin.com/linux/latest/source/arch/x86
-
“Understanding the Linux Virtual Memory Manager” by Mel Gorman
-
x86 instruction reference: https://www.felixcloutier.com/x86/
Related Topics
- Calling Conventions — x86_64 System V ABI details
- Memory Models — x86 TSO memory ordering
- Building the Kernel — building for x86
- Key Kernel Subsystems — subsystems using x86-specific code