Early in my low-level programming journey, I tried to write a tiny assembly routine that directly accessed a hardware port, expecting it to just work like any other instruction. It didn’t — the CPU raised a protection fault and the OS killed my process instantly. That was my first real encounter with privilege levels, the hardware-enforced hierarchy that decides what a piece of running code is and isn’t allowed to do. This post walks through what privilege levels are, how they’re implemented on x86 and ARM, and why they matter even if you never write an operating system yourself.
What Are Privilege Levels?
Privilege levels (also called protection rings, or exception levels on ARM) are hardware-enforced tiers of access control built into the CPU. Code running at a higher privilege level can do things that code at a lower privilege level cannot — access arbitrary memory, execute special instructions, configure hardware, and handle interrupts directly.
This exists because an operating system needs to protect itself, and other processes, from a buggy or malicious user program. Without hardware-enforced privilege separation, any program could overwrite kernel memory, disable interrupts, or directly manipulate hardware in ways that would destabilize the entire system.
x86 Protection Rings
x86 defines four privilege rings, though in practice almost all modern general-purpose operating systems only use two of them:
| Ring | Typical Use | Common Usage Today |
|---|---|---|
| Ring 0 | Kernel mode — full hardware access | Used by the OS kernel |
| Ring 1 | Device drivers (historically) | Rarely used in modern OSes |
| Ring 2 | Device drivers (historically) | Rarely used in modern OSes |
| Ring 3 | User mode — restricted access | Used by all normal applications |
flowchart TD
R0["Ring 0 - Kernel Mode (highest privilege)"] --> R1[Ring 1 - rarely used]
R1 --> R2[Ring 2 - rarely used]
R2 --> R3["Ring 3 - User Mode (lowest privilege)"]
Most modern OSes (Linux, Windows, macOS) collapse this into a simple two-tier model: Ring 0 for the kernel, Ring 3 for everything else, because the intermediate rings offered diminishing security benefit for the added complexity.
How the CPU Tracks Current Privilege Level
On x86, the Current Privilege Level (CPL) is stored in the low 2 bits of the CS (code segment) register, and it’s checked automatically by the hardware every time a privileged instruction or memory access is attempted.
; A privileged instruction - only legal at Ring 0
cli ; Clear Interrupt Flag (disable interrupts) - requires Ring 0
; Executing CLI from Ring 3 user-mode code causes a #GP (General Protection Fault)
ARM Exception Levels
ARM’s AArch64 architecture uses a conceptually similar but differently named system: Exception Levels (EL0–EL3).
| Exception Level | Typical Use |
|---|---|
| EL0 | User applications (least privileged) |
| EL1 | Operating system kernel |
| EL2 | Hypervisor (virtualization) |
| EL3 | Secure Monitor / TrustZone firmware (most privileged) |
flowchart TD
EL3["EL3 - Secure Monitor / TrustZone (highest privilege)"] --> EL2["EL2 - Hypervisor"]
EL2 --> EL1["EL1 - Operating System Kernel"]
EL1 --> EL0["EL0 - User Applications (lowest privilege)"]
This model explicitly bakes in a virtualization tier (EL2) and a security/firmware tier (EL3), reflecting ARM’s widespread use in mobile devices, embedded security, and cloud virtualization scenarios where these extra layers are genuinely useful.
How Privilege Transitions Happen
Moving between privilege levels isn’t something you can do with an ordinary jump instruction — it requires specific, hardware-controlled mechanisms:
x86: Interrupts, Syscalls, and Task Gates
; Transition from Ring 3 to Ring 0 via syscall instruction
mov rax, 1 ; syscall number
syscall ; CPU switches CS to a Ring 0 code segment, jumps to kernel handler
The SYSCALL instruction (or historically INT 0x80) is one of the very few sanctioned doorways from Ring 3 into Ring 0. The CPU automatically updates the CPL as part of this transition, and only jumps to a kernel-configured entry point — user code cannot specify an arbitrary destination address for this privilege escalation.
ARM: SVC and Exception Entry
; Transition from EL0 to EL1 via supervisor call
MOV X8, #64 ; syscall number
SVC #0 ; CPU switches exception level EL0 -> EL1, jumps to configured vector
Just like x86, the destination of this transition (the exception vector table) is configured by the kernel ahead of time, not chosen by the calling user-mode code.
What Privileged Instructions Actually Look Like
| Category | Example x86 Instructions | Example ARM Instructions |
|---|---|---|
| Interrupt control | CLI, STI | MSR DAIFSet, MSR DAIFClr |
| Memory management | MOV CR3, reg (page table base) | MSR TTBR0_EL1, reg |
| Privilege/exception config | LGDT, LIDT | MSR VBAR_EL1, reg |
| Halt/power management | HLT | WFI (Wait For Interrupt) |
Attempting any of these from an insufficiently privileged level results in a hardware fault — on x86 this is typically a General Protection Fault (#GP); on ARM it results in a synchronous exception that the OS’s exception handler must service (often terminating the offending process).
Internal Flow: What Happens on a Privilege Violation
sequenceDiagram
participant App as User Program (Ring 3 / EL0)
participant CPU as CPU Protection Hardware
participant Kernel as Kernel (Ring 0 / EL1)
App->>CPU: Attempt privileged instruction (e.g. CLI, MSR TTBR0_EL1)
CPU->>CPU: Check current privilege level against required level
CPU->>Kernel: Privilege violation detected - raise fault/exception
Kernel->>Kernel: Fault handler inspects the offending instruction/context
Kernel->>App: Typically terminates the process (e.g. SIGSEGV/SIGILL)
Practical Use Cases
- Operating system and kernel development: Understanding privilege transitions is fundamental to writing bootloaders, kernels, and hypervisors.
- Virtualization: Hypervisors rely on ARM’s EL2 or x86’s VMX root mode to intercept and manage guest OS privilege behavior transparently.
- Security research and exploit mitigation: Many privilege-escalation vulnerabilities are precisely about tricking the kernel into executing attacker-controlled code at a higher privilege level than intended.
- Embedded/TrustZone development: ARM’s EL3/TrustZone is used to isolate secure firmware (e.g., handling cryptographic keys) from the normal-world OS, even if the OS itself is compromised.
Debugging Privilege-Related Issues
$ dmesg | grep -i "general protection"
$ dmesg | grep -i "segfault"
On Linux, privilege violations from user-mode code typically surface as SIGSEGV (segmentation fault) or SIGILL (illegal instruction), logged in the kernel ring buffer. Kernel-mode privilege bugs (much rarer to debug, since they crash the whole system) require kernel debuggers like kgdb or hardware JTAG debug probes.
Comparison: x86 Rings vs. ARM Exception Levels
| Aspect | x86 Protection Rings | ARM Exception Levels |
|---|---|---|
| Number of levels | 4 defined, typically 2 used | 4 defined (EL0-EL3), all commonly used in modern systems |
| Virtualization support | Separate VMX root/non-root mode overlay | Built directly into the EL model via EL2 |
| Security/firmware tier | Handled via System Management Mode (SMM), somewhat separate | Explicit EL3/TrustZone tier |
| Terminology | “Rings,” Current Privilege Level (CPL) | “Exception Levels,” current EL tracked in PSTATE |
Common Mistakes
- Assuming user-mode code can execute privileged instructions “if it’s careful” — the check is enforced entirely in hardware, not convention.
- Confusing ARM’s exception levels with x86 rings one-to-one; ARM’s model explicitly separates hypervisor (EL2) and secure firmware (EL3) concepts that x86 handles differently (via VMX and SMM respectively).
- In kernel/driver development, forgetting that certain memory-mapped registers or instructions require a specific privilege level, leading to faults that are hard to diagnose without understanding the underlying protection model.
Best Practices
- When writing low-level or OS-adjacent assembly, always check the target architecture’s manual for which privilege level a given instruction requires.
- Use well-documented syscall/SVC interfaces to request privileged operations rather than attempting direct privileged instructions from user mode.
- In virtualization or security-sensitive contexts, understand exactly which exception level or ring your code is expected to run at, and design fault handlers accordingly.
FAQs
Can user-mode code ever execute privileged instructions? No — the CPU hardware checks the current privilege level before executing certain instructions and blocks the attempt with a fault if the check fails, regardless of how the instruction was reached.
Do all four x86 rings get used in modern systems? Rarely — almost all modern operating systems use only Ring 0 and Ring 3, treating Rings 1 and 2 as legacy/unused.
What’s the difference between EL2 and EL3 on ARM? EL2 is for hypervisors managing virtual machines; EL3 is for the most privileged secure firmware/monitor code, often used for TrustZone-based secure world isolation, which is a different concern from virtualization.
Summary and Key Takeaways
- Privilege levels are hardware-enforced tiers that restrict which instructions and memory a running program can access.
- x86 uses protection rings (0–3, with 0 and 3 dominant in practice); ARM uses Exception Levels (EL0–EL3), explicitly including hypervisor and secure firmware tiers.
- Transitioning between privilege levels requires specific, kernel-controlled mechanisms like
SYSCALL/SVC, not ordinary jumps. - Understanding privilege levels is essential for OS development, virtualization, security research, and diagnosing low-level faults.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3 (Protection, Rings, and Privilege chapters)
- AMD64 Architecture Programmer’s Manual, Volume 2 (System Programming)
- Arm® Architecture Reference Manual for A-profile architecture (Exception Levels chapter)
- Arm TrustZone documentation (Security and EL3 overview)
