| name | osdev-architecture |
| description | Architecture-specific OS development for x86-64, ARM, and RISC-V. MSRs, CPUID, control registers, CPU modes, exception models, and device trees. Use when writing arch-specific kernel code, detecting CPU features, or porting across architectures. |
| origin | MCC |
CPU Architecture for OS Development
Complete reference for writing architecture-specific kernel code across x86-64, ARM (AArch64), and RISC-V. Covers the registers, instructions, and mechanisms that differ between architectures and that every kernel must interact with.
When to Use
- Writing boot code or early kernel init that touches control registers
- Detecting CPU features via CPUID before enabling them
- Reading or writing MSRs (syscall setup, APIC, NX enable)
- Porting kernel code between x86-64, ARM, and RISC-V
- Setting up exception/interrupt vectors on a new architecture
- Configuring syscall entry points (SYSCALL/SVC/ECALL)
- Parsing device trees on ARM or RISC-V platforms
x86-64 Essentials
Control Registers
Control registers configure fundamental CPU behavior. You cannot enable paging, enforce write-protection, or activate security features without setting the right CR bits.
| Register | Key Bits | Purpose |
|---|
| CR0 | PE (bit 0) | Protected Mode enable -- must be set before PG |
| CR0 | PG (bit 31) | Paging enable |
| CR0 | WP (bit 16) | Write Protect -- enforces read-only pages even for ring 0 (needed for copy-on-write) |
| CR0 | NE (bit 5) | Numeric Error -- use native FPU error reporting instead of external interrupt |
| CR3 | bits [51:12] | Physical address of PML4 table (page table base) |
| CR4 | PSE (bit 4) | Page Size Extension -- enables 4MB pages in 32-bit mode |
| CR4 | PAE (bit 5) | Physical Address Extension -- required before entering long mode |
| CR4 | SMEP (bit 20) | Supervisor Mode Execution Prevention -- blocks kernel executing user pages |
| CR4 | SMAP (bit 21) | Supervisor Mode Access Prevention -- blocks kernel reading user pages |
| CR4 | FSGSBASE (bit 16) | Enables RDFSBASE/WRFSBASE instructions for fast FS/GS access |
Why CR0.WP matters: Without WP, ring 0 can silently write through read-only page mappings. This breaks copy-on-write and makes it impossible to catch kernel bugs that corrupt read-only data. Set WP=1 early in boot.
Model Specific Registers (MSRs)
MSRs are a secondary register space accessed with dedicated instructions. Each MSR has a 32-bit address and holds a 64-bit value.
Access pattern:
; Read MSR: ECX = MSR address, result in EDX:EAX
mov ecx, 0xC0000080 ; IA32_EFER
rdmsr ; EDX:EAX = value
; Write MSR: ECX = MSR address, EDX:EAX = value to write
mov ecx, 0xC0000080
or eax, (1 << 0) | (1 << 8) | (1 << 11) ; SCE + LME + NXE
wrmsr
Key MSRs every kernel needs:
| Address | Name | Purpose |
|---|
| 0xC0000080 | IA32_EFER | LME (bit 8): Long Mode Enable. NXE (bit 11): No-Execute Enable. SCE (bit 0): SYSCALL Enable |
| 0xC0000081 | IA32_STAR | SYSCALL segment selectors (bits 47:32 = kernel CS, bits 63:48 = user CS) |
| 0xC0000082 | IA32_LSTAR | SYSCALL entry point -- RIP loaded from here on SYSCALL |
| 0xC0000084 | IA32_FMASK | RFLAGS mask -- bits set here are cleared on SYSCALL (mask IF to disable interrupts) |
| 0x0000001B | IA32_APIC_BASE | Local APIC base address and enable bit |
| 0xC0000100 | IA32_FS_BASE | FS segment base (used for thread-local storage) |
| 0xC0000101 | IA32_GS_BASE | GS segment base (used for per-CPU data in kernel) |
| 0xC0000102 | IA32_KERNEL_GS_BASE | Swapped with GS_BASE on SWAPGS instruction |
Why FMASK matters: On SYSCALL, the CPU does not automatically disable interrupts. If you do not mask IF in FMASK, an interrupt can fire before your syscall handler saves state, corrupting the user stack pointer.
CPUID
CPUID is the only way to discover what features the CPU supports. Without checking CPUID first, enabling a feature that does not exist causes a #UD (undefined instruction) or #GP fault.
Usage pattern: Load EAX with the leaf number (and ECX with subleaf if needed), execute CPUID, read results from EAX/EBX/ECX/EDX.
; Check if CPUID is supported (toggle EFLAGS.ID bit 21)
; Then query features:
mov eax, 1 ; Leaf 1: basic features
cpuid
; ECX and EDX now contain feature flags
test edx, (1 << 5) ; EDX bit 5 = MSR support
test ecx, (1 << 21) ; ECX bit 21 = x2APIC
Critical leaves:
| EAX (Leaf) | Purpose | Key Results |
|---|
| 0 | Max leaf + vendor string | EBX:EDX:ECX = vendor ID ("GenuineIntel", "AuthenticAMD") |
| 1 | Feature flags | EDX: FPU, MSR, PAE, APIC, SSE, SSE2. ECX: SSE3, SSSE3, SSE4.1/4.2, x2APIC, XSAVE, AVX |
| 7 (ECX=0) | Extended features | EBX: SMEP, SMAP, AVX2, RDSEED. ECX: UMIP, PKU. EDX: IBRS, STIBP, L1D_FLUSH |
| 0x80000001 | Extended features 2 | EDX: NX/XD, 1GB pages, SYSCALL. ECX: LAHF/SAHF in long mode |
| 0x80000008 | Address sizes | EAX[7:0]: physical address bits. EAX[15:8]: virtual address bits |
Rule: Always check CPUID leaf 0 first to get the maximum supported leaf. Querying a leaf beyond the maximum returns undefined data (often the last valid leaf's data).
See references/x86-64-msrs.md for full MSR table, CPUID leaf details, control register bitmaps, and inline assembly wrappers.
ARM Overview (AArch64)
ARM uses a fundamentally different privilege model from x86. Instead of rings, ARM defines Exception Levels that the CPU transitions between on exceptions and explicit return instructions.
Exception Levels
| Level | Purpose | Analogy |
|---|
| EL0 | User applications | Ring 3 |
| EL1 | OS kernel | Ring 0 |
| EL2 | Hypervisor | VMX root |
| EL3 | Secure Monitor (TrustZone) | SMM |
Key difference from x86: On ARM, you enter a higher exception level on exception and return to a lower one with ERET. On x86, interrupts and syscalls stay in ring 0 -- the ring change happens via gate descriptors, not a level number.
The CPU boots at the highest implemented EL and must drop down. If firmware starts at EL3, it configures TrustZone, drops to EL2 for hypervisor init, which drops to EL1 for the kernel.
System Registers
ARM system registers are accessed with MRS (read) and MSR (write) instructions. Unlike x86 MSRs which are in a flat numeric namespace, ARM system registers have hierarchical names.
| Register | Purpose |
|---|
| SCTLR_EL1 | System control: MMU enable (M bit), caching (C, I bits), alignment check (A bit), WXN (write-implies-XN) |
| TTBR0_EL1 | Translation Table Base Register 0 -- user space page tables |
| TTBR1_EL1 | Translation Table Base Register 1 -- kernel page tables |
| TCR_EL1 | Translation Control Register -- granule size, address space size, cacheability |
| VBAR_EL1 | Vector Base Address Register -- base of exception vector table |
| MAIR_EL1 | Memory Attribute Indirection Register -- defines memory types referenced by PTEs |
| ESR_EL1 | Exception Syndrome Register -- on exception entry, describes cause |
| FAR_EL1 | Fault Address Register -- faulting virtual address |
TTBR0 vs TTBR1 split: ARM gives user and kernel separate page table base registers. TTBR0 covers the lower address range (user), TTBR1 covers the upper range (kernel). On context switch, only TTBR0 changes. This is cleaner than x86's single CR3 with global-page workarounds.
Exception Handling
The vector table at VBAR_EL1 has 16 entries (4 exception types x 4 source contexts):
| Exception Type | From Current EL (SP0) | From Current EL (SPx) | From Lower EL (AArch64) | From Lower EL (AArch32) |
|---|
| Synchronous | +0x000 | +0x200 | +0x400 | +0x600 |
| IRQ | +0x080 | +0x280 | +0x480 | +0x680 |
| FIQ | +0x100 | +0x300 | +0x500 | +0x700 |
| SError | +0x180 | +0x380 | +0x580 | +0x780 |
Each entry has 128 bytes (32 instructions) for the initial handler. Branch to full handler code from there.
Device Tree
ARM systems (unlike x86 with ACPI) describe hardware topology through a Device Tree Blob (DTB). The bootloader passes the DTB physical address in register x0 at kernel entry.
The DTB describes: memory regions, interrupt controllers (GIC), UARTs, timers, bus topology, and all platform devices. Without parsing the DTB, your kernel cannot find any hardware.
See references/arm-exceptions.md for vector table setup code, exception entry/return sequences, system register access patterns, and boot sequence details.
RISC-V Overview
RISC-V takes a minimalist approach: a small mandatory base plus optional extensions. The privilege architecture is simpler than both x86 and ARM.
Privilege Modes
| Mode | Abbreviation | Purpose |
|---|
| Machine | M-mode | Firmware (OpenSBI). Mandatory, always present. Highest privilege |
| Supervisor | S-mode | OS kernel. Optional but required for Unix-like OS |
| User | U-mode | Applications. Optional but required for process isolation |
Typical boot path: M-mode firmware (OpenSBI) initializes hardware, sets up trap delegation, then jumps to S-mode kernel entry. The kernel never runs in M-mode.
Control and Status Registers (CSRs)
CSRs are accessed with dedicated instructions: CSRRW (read-write), CSRRS (read-set), CSRRC (read-clear), and immediate variants.
M-mode CSRs (firmware):
| CSR | Address | Purpose |
|---|
| mstatus | 0x300 | Global interrupt enable (MIE), previous mode (MPP), etc. |
| mtvec | 0x305 | Trap vector base address (M-mode) |
| mepc | 0x341 | Exception program counter (return address for MRET) |
| mcause | 0x342 | Trap cause: bit 63 = interrupt flag, bits [62:0] = exception code |
| medeleg | 0x302 | Exception delegation -- which exceptions delegate to S-mode |
| mideleg | 0x303 | Interrupt delegation -- which interrupts delegate to S-mode |
S-mode CSRs (kernel):
| CSR | Address | Purpose |
|---|
| sstatus | 0x100 | Supervisor status: SIE (interrupt enable), SPP (previous privilege) |
| stvec | 0x105 | Trap vector base address (S-mode) |
| sepc | 0x141 | Exception PC for supervisor traps |
| scause | 0x142 | Trap cause (same format as mcause) |
| stval | 0x143 | Trap value (faulting address for page faults, etc.) |
| satp | 0x180 | Supervisor Address Translation and Protection -- page table base + mode (Sv39/Sv48/Sv57) |
| sie | 0x104 | Supervisor interrupt enable bits |
| sip | 0x144 | Supervisor interrupt pending bits |
Trap Handling
Traps in RISC-V are vectored through stvec (S-mode) or mtvec (M-mode). Two modes:
- Direct (stvec[1:0] = 0): All traps jump to the base address
- Vectored (stvec[1:0] = 1): Interrupts jump to base + 4*cause, exceptions jump to base
The mcause/scause register tells you what happened. The high bit distinguishes interrupts from exceptions:
| Cause Code | Type | Meaning |
|---|
| 0 | Exception | Instruction address misaligned |
| 2 | Exception | Illegal instruction |
| 5 | Exception | Load access fault |
| 8 | Exception | Environment call from U-mode (syscall) |
| 12 | Exception | Instruction page fault |
| 13 | Exception | Load page fault |
| 15 | Exception | Store page fault |
| 1 (interrupt) | Interrupt | Supervisor software interrupt |
| 5 (interrupt) | Interrupt | Supervisor timer interrupt |
| 9 (interrupt) | Interrupt | Supervisor external interrupt |
SBI (Supervisor Binary Interface)
SBI is RISC-V's equivalent of BIOS calls or ARM's PSCI. The kernel running in S-mode calls firmware in M-mode through ECALL for operations it cannot perform directly:
- Timer: Set timer comparator (SBI_SET_TIMER)
- IPI: Send inter-processor interrupts (SBI_SEND_IPI)
- Console: Early debug output (SBI_CONSOLE_PUTCHAR) -- legacy, use UART for production
- HSM: Hart State Management -- start/stop/suspend harts
OpenSBI is the standard M-mode firmware that implements SBI. The kernel calls SBI by placing the extension ID in a7, function ID in a6, and arguments in a0-a5, then executing ECALL.
See references/riscv-privilege.md for full CSR tables, trap delegation setup, SBI call reference, SATP format, and boot sequence.
Architecture Comparison
| Feature | x86-64 | ARM (AArch64) | RISC-V |
|---|
| Privilege levels | Ring 0-3 (typically 0 and 3) | EL0-EL3 | M/S/U modes |
| Page table base | CR3 (single) | TTBR0 + TTBR1 (split user/kernel) | SATP (single) |
| Page table format | 4-level (PML4), 5-level (PML5) | 4-level (4KB granule) | Sv39 (3), Sv48 (4), Sv57 (5) |
| Syscall mechanism | SYSCALL/SYSRET (MSR-configured) | SVC instruction (to EL1) | ECALL (to S-mode or M-mode) |
| Interrupt vector | IDT (256 entries) | VBAR vector table (16 entries) | stvec/mtvec (direct or vectored) |
| Feature detection | CPUID instruction | ID registers (ID_AA64MMFR0_EL1, etc.) | misa CSR + device tree |
| Hardware discovery | ACPI (mostly) | Device Tree (DTB) | Device Tree (DTB) |
| Firmware interface | UEFI/BIOS | PSCI / SMC calls | SBI (ECALL to M-mode) |
| NX/XD | PTE bit 63 (requires EFER.NXE) | PXN/UXN/XN in PTE | Page permission bits |
| Kernel execution prevention from user pages | SMEP (CR4 bit 20) | PXN bit in PTE | Not directly equivalent |
Common Pitfalls
-
Querying CPUID without checking max leaf. Leaf 7 does not exist on older CPUs. Always check leaf 0 first. Querying unsupported leaves returns stale data, not a fault.
-
Writing MSRs without reading first. WRMSR writes the full 64-bit value. If you only want to set one bit, RDMSR first, OR in your bit, then WRMSR. Otherwise you zero out every other field.
-
Forgetting SWAPGS on syscall entry. On x86-64, SYSCALL does not change GS. If your kernel uses GS for per-CPU data, you must SWAPGS immediately on entry and again on return. Missing this means you read user GS, not kernel GS.
-
Not masking IF in IA32_FMASK. SYSCALL does not disable interrupts. If an interrupt fires before you save RSP, the interrupt handler uses the user stack. Set bit 9 in FMASK.
-
ARM: Not setting VBAR_EL1 before enabling interrupts. VBAR defaults to 0 on reset. If you enable interrupts before setting VBAR, the CPU jumps to address 0 on the first interrupt.
-
ARM: Forgetting ISB after system register writes. ARM system register writes are not guaranteed to take effect until an instruction synchronization barrier (ISB). After writing SCTLR_EL1, TTBR0_EL1, or VBAR_EL1, always issue ISB.
-
RISC-V: Not delegating traps from M-mode. Without setting medeleg/mideleg, all traps go to M-mode. Your S-mode kernel will never see page faults or timer interrupts unless firmware delegates them.
-
RISC-V: Confusing SATP modes. Sv39 uses 3 levels, Sv48 uses 4. Setting the wrong mode in satp.MODE gives you either truncated or invalid address translation. Check the platform supports your chosen mode.
-
Assuming CPUID features are the same on AMD and Intel. Leaf 0x80000001 has different bit assignments between vendors. Always check the vendor string from leaf 0 first.
-
Using RDTSC for timing without checking invariant TSC. The TSC frequency changes with CPU frequency unless the invariant TSC feature (CPUID 0x80000007 EDX bit 8) is present. Use it for timestamps only when invariant.
Related Skills
osdev-paging -- page table setup, PTE flags, TLB management
osdev-interrupts -- IDT/GIC/PLIC setup, interrupt handlers
osdev-boot-sequence -- BIOS/UEFI boot, entering long mode
osdev-cpu-tables -- GDT, TSS setup on x86
osdev-security -- SMEP, SMAP, NX enable, KPTI
osdev-process-scheduling -- context switch, syscall return paths