Multi-Architecture Support in Forest OS
Forest OS is a hobby operating system kernel designed to run on multiple CPU architectures from a single, unified codebase. This page covers how the kernel organizes architecture-specific code, the cross-architecture abstraction layer, and the details of each supported architecture.
Supported Architectures
Forest OS currently supports five architecture targets:
| Architecture | ARCH value | Bits | ISA | Page Table | Example Target |
|---|---|---|---|---|---|
| x86 (32-bit) | 32 |
32 | IA-32 (i686) | 2-level paging | QEMU i440FX |
| x86_64 (64-bit) | 64 |
64 | AMD64 | 4-level (PML4) | QEMU Q35/qemu64 |
| ARM32 | arm |
32 | ARMv7-A (Cortex-A15) | Short-descriptor (L1+L2) | QEMU virt, Raspberry Pi 2/3 |
| AArch64 | aarch64 |
64 | ARMv8-A (Cortex-A53) | 4-level (L0→L3) | QEMU virt, Raspberry Pi 3/4 |
| RISC-V 64 | riscv64 |
64 | RV64GC | Sv39 (3-level) | QEMU virt |
All architectures use 4 KB pages and are little-endian.
Code Organization
The kernel source tree splits architecture-specific code into two main areas:
1. The Cross-Architecture Abstraction Layer (src/arch/)
This directory contains headers and C files that are compiled for every architecture. The build system selects the correct architecture-specific implementations at compile time using preprocessor conditionals.
Key files:
arch.h— The single entry point for all architecture definitions. Detects the target architecture via compiler predefined macros (__x86_64__,__aarch64__,__arm__,__riscv, etc.) and definesARCH_X86_32,ARCH_X86_64,ARCH_ARM32,ARCH_ARM64,ARCH_RISCV64. Provides abstract types (arch_word_t,arch_paddr_t,arch_vaddr_t), abstract CPU state (arch_cpu_state_t— a union of per-arch register frames), and inline operations (arch_get_sp(),arch_halt(),arch_enable_irq(),arch_disable_irq(),arch_cpu_relax()).
arch_ops.c— Implementsarch_init()(per-arch early init),arch_get_name(),arch_get_page_size()(always 4096),arch_supports_feature()(lazy feature detection with caching), and platform UART stubs for early boot console output.
barrier.h— Unified memory barrier interface:arch_mb()(full barrier),arch_rmb()(read barrier),arch_wmb()(write barrier),arch_compiler_barrier(), and ARM-specificarch_dsb()/arch_isb().
platform.h— Platform/board detection and MMIO base addresses. Auto-detects platform from architecture. Defines MMIO bases for QEMU x86, QEMU ARM virt, Raspberry Pi 3, and Raspberry Pi 4.
2. Architecture-Specific Directories
Each architecture has its own directory with full implementations:
src/arm32/ 31 files — boot, exceptions, context switch, MMU, GIC, UART, timer, etc.
src/aarch64/ 34 files — boot, exceptions, context switch, MMU, GICv3, UART, FPU, UEFI, etc.
src/riscv64/ 27 files — boot, trap handling, context switch, MMU, CLINT, PLIC, UEFI, etc.
src/x86_64/ 2 files — signal.c, smp.c (most x86 code is in src/ directly)
Additionally, each architecture has a thin header in src/arch/{arch}/:
src/arch/x86_32/arch_x86_32.h — GDT, IDT, TSS, EFLAGS, CR0-CR4, CPUID, port I/O
src/arch/x86_64/arch_x86_64.h — 64-bit GDT/IDT/TSS, RFLAGS, MSRs, SYSCALL/SYSRET, 4-level paging
src/arch/arm32/arch_arm32.h — CPSR, CP15 accessors, MMU short-descriptor, cache/TLB ops
src/arch/aarch64/arch_aarch64.h — Exception levels, system registers, GICv3 ICC, 4-level paging
src/arch/riscv64/arch_riscv64.h — CSRs, Sv39 page tables, TLB/cache maintenance
These headers are pulled in automatically by arch.h based on the detected architecture — you never include them directly.
The Cross-Architecture Abstraction Layer
The abstraction layer provides a unified API that kernel subsystems use without caring which architecture they're running on. Here's how it works:
Architecture Detection
arch.h uses compiler-predefined macros to set exactly one ARCH_* flag to 1 and all others to 0:
#if defined(__x86_64__) || defined(_M_X64)
# define ARCH_X86_64 1
# define ARCH_BITS 64
# define ARCH_NAME "x86_64"
#elif defined(__aarch64__) || defined(_M_ARM64)
# define ARCH_ARM64 1
# define ARCH_BITS 64
# define ARCH_NAME "aarch64"
// ... etc for each architecture
Convenience macros like ARCH_IS_X86, ARCH_IS_ARM, ARCH_IS_64BIT, and ARCH_IS_32BIT are derived from these.
Abstract CPU State
Each architecture defines its own CPU state structure, but the kernel sees only arch_cpu_state_t:
// x86_64: r15-r8, rdi-rbx, rdx-rcx, rax, interrupt frame
// ARM32: r0-r12, sp, lr, pc, cpsr, spsr
// AArch64: x0-x29, x30 (lr), sp, pc, spsr_el1, elr_el1, esr_el1, far_el1
// RISC-V: ra, gp, tp, t0-t6, s0-s11, a0-a7, sstatus, sepc, scause, stval
Inline Operations
Architecture-neutral inline functions compile to a single instruction per architecture:
| Operation | x86/x86_64 | ARM32 | AArch64 | RISC-V |
|---|---|---|---|---|
arch_get_sp() |
mov %rsp, %0 |
mov %0, sp |
mov %0, sp |
mv %0, sp |
arch_halt() |
hlt |
wfi |
wfi |
wfi |
arch_enable_irq() |
sti |
cpsie i |
msr daifclr, #2 |
csrsi sstatus, 0x2 |
arch_disable_irq() |
cli |
cpsid i |
msr daifset, #2 |
csrci sstatus, 0x2 |
arch_cpu_relax() |
pause |
yield |
yield |
rep.nop |
Memory Barriers
arch_mb(); // Full barrier: x86=mfence, ARM=dmb sy, RISC-V=fence rw,rw
arch_rmb(); // Read barrier: x86=lfence, ARM=dmb ishld, RISC-V=fence r,r
arch_wmb(); // Write barrier: x86=sfence, ARM=dmb ishst, RISC-V=fence w,w
Feature Detection
arch_supports_feature() lazily detects CPU capabilities at runtime and caches the result:
- x86/x86_64: Uses CPUID leaves 1 and 7 to detect FPU, SSE2, AVX-512, TSX, AES-NI, VMX, SMP (APIC)
- ARM32: Reads MPIDR (SMP), ID_ISAR0 (FPU), MVFR1 (NEON), SCTLR bits
- AArch64: Reads MPIDR_EL1, ID_AA64PFR0_EL1 (SVE, EL2), ID_AA64ISAR0_EL1 (AES, CRC32)
- RISC-V: Feature detection is not yet implemented in
arch_ops.c(the function exists but nodetect_features_riscv64()is present — this is a TODO)
Feature flags include: ARCH_FEAT_FPU, ARCH_FEAT_SIMD, ARCH_FEAT_MMU, ARCH_FEAT_SMP, ARCH_FEAT_ATOMIC64, ARCH_FEAT_NEON, ARCH_FEAT_SVE, ARCH_FEAT_AVX512, ARCH_FEAT_TSX, ARCH_FEAT_CRYPTO, ARCH_FEAT_CRC32, ARCH_FEAT_VIRT.
x86 (32-bit) Specifics
Toolchain: i686-forestos-gcc (Forest OS cross-compiler) Linker flags: -m elf_i386 NASM flags: -f elf386
Key features:
- Paging: 2-level page directory/table (1024 PDEs × 1024 PTEs = 4 GB VA space)
- Interrupts: 8-byte IDT gate descriptors, PIC or APIC
- Boot: Multiboot-compliant, loaded at 1 MB by BIOS/GRUB
- GDT/IDT/TSS: Full IA-32 protected mode structures defined in
arch_x86_32.h - Port I/O:
inb/outb/inw/outw/inl/outlinline functions - Syscalls: Via
int 0x80orsysenter/sysexit
Compiler flags: -m32 -march=i386 -mtune=i386, with -mno-red-zone -mno-sse -mno-sse2 -mfpmath=387 for kernel code.
x86_64 (64-bit) Specifics
Toolchain: x86_64-forestos-gcc (Forest OS cross-compiler, required) Linker flags: -m elf_x86_64 NASM flags: -f elf64
Key features:
- Long mode: 64-bit paging with 4-level page tables (PML4 → PDP → PD → PT), 48-bit virtual address space
- SYSCALL/SYSRET: Fast syscall via MSR configuration (LSTAR, STAR, SFMAP)
- MSR access:
rdmsr/wrmsrfor EFER, FS_BASE, GS_BASE, SYSCALL MSRs - GS base / SWAPGS: Per-CPU kernel data via GS.base swap on syscall entry
- NX bit: Execute Disable via EFER.NXE
- SMEP/SMAP: Supervisor Mode Execution/Access Prevention (detected via CPUID leaf 7)
- 5-level paging: Optional LA57 support (gated by
ENABLE_5LEVEL_PAGING) - Interrupts: 16-byte IDT entries with IST (Interrupt Stack Table) support
- TSS: 64-bit TSS with rsp0 (ring-0 stack) and ist1-ist7
Compiler flags: -m64 -march=x86-64 -mcmodel=kernel, with -mno-red-zone -mcmodel=kernel -mno-mmx -mno-sse -mno-sse2 -mno-80387 -msoft-float.
The x86_64/ directory contains signal.c and smp.c. Most x86-specific code lives in the main src/ directory (boot, interrupt stubs, GDT/IDT/PIC/APIC drivers).
ARM32 Specifics
Toolchain: arm-none-eabi-gcc or arm-linux-gnueabi-gcc Linker flags: -m armelf Compiler flags: -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=softfp -marm
Key features:
- Boot (
boot.S): Starts in SVC mode, masks interrupts, installs VBAR, sets up per-mode stacks (SVC: 8KB, IRQ: 4KB, FIQ: 1KB, ABT: 2KB, UND: 1KB), zeroes BSS, copies .data, initializes PL011 UART, callskernel_main(dtb_addr). - Exception vectors: 8-entry table with
LDR PC, [PC, #offset]trampolines. Each handler saves full context viaSAVE_CONTEXTmacro (pushes r0-r12, lr, spsr). - MMU: VMSAv7 short-descriptor format. L1 table (4096 entries × 4 bytes = 16 KB) with 1 MB section descriptors or L2 page table pointers. L2 tables (256 entries × 4 bytes = 1 KB) with 4 KB small page descriptors. Domain-based access control.
- Context switch (
context_switch.S): Saves/restores callee-saved registers (r4-r11, lr) and VFP/NEON state (s16-s31, fpscr). User-mode entry vialdmfd sp!, {pc}^which atomically restores PC and CPSR. - Interrupt controller: GIC (Generic Interrupt Controller) — see
gic.c/gic.h - UART: PL011 MMIO UART at
0x09000000(QEMU virt) - Timer: ARM Generic Timer or BCM system timer
- VFP/NEON: Lazy save/restore in context switch and exception handlers
- Syscalls: SWI (Supervisor Call) with Linux ARM EABI convention — syscall number in r7, args in r0-r6
Platform support: QEMU virt (cortex-a15), Raspberry Pi 2/3 (BCM2837).
AArch64 (ARM 64-bit) Specifics
Toolchain: aarch64-linux-gnu-gcc or aarch64-none-elf-gcc Linker flags: -m aarch64elf Compiler flags: -march=armv8-a
Key features:
- Exception levels: EL0 (user), EL1 (kernel), EL2 (hypervisor), EL3 (secure monitor). Boot code handles EL3→EL2→EL1 transitions.
- Boot (
boot.S): Parks secondary CPUs, determines current EL, configures HCR_EL2 (EL1 is AArch64), sets up SCTLR_EL1, enables FP/SIMD via CPACR_EL1, installs vector table at VBAR_EL1, zeroes BSS, sets up 16-byte-aligned kernel stack, callskernel_main(dtb_addr). - Exception vectors (
exceptions.S): 16-entry table (2 KB aligned, each entry 128 bytes). Four groups: Current EL with SP0, Current EL with SPx, Lower EL (AArch64), Lower EL (AArch32). Syscalls detected via ESR_EL1.EC = 0x15 (SVC from AArch64). - MMU: 4-level page table (L0→L1→L2→L3), 4 KB granule, 48-bit VA. TTBR0 for user space (0x0000_0000_0000_0000 – 0x0000_FFFF_FFFF_FFFF), TTBR1 for kernel (0xFFFF_0000_0000_0000 – 0xFFFF_FFFF_FFFF_FFFF). MAIR_EL1 for memory attributes (Normal WB, Device nGnRnE, Normal NC).
- Context switch (
context_switch.S): Saves/restores x19-x30, sp_el0, and page table registers (sctlr_el1, ttbr0_el1, tcr_el1, mair_el1). TLB flush only when ttbr0 changes. NEON/FP state saved/restored viafpu_save()/fpu_restore(). User-mode entry viaeret. - Interrupt controller: GICv3 with system register CPU interface (ICC_SRE_EL1, ICC_IAR1_EL1, ICC_EOIR1_EL1). SGIs via ICC_SGI1R_EL1.
- UART: PL011 MMIO UART
- UEFI boot:
uefi_boot.c/uefi_boot.Sfor UEFI boot path - Syscalls: Linux AArch64 convention — x8=syscall number, x0-x7=args, x0=return value
Platform support: QEMU virt (cortex-a53), Raspberry Pi 3/4.
RISC-V 64 Specifics
Toolchain: riscv64-unknown-elf-gcc or riscv64-linux-gnu-gcc Linker flags: -m elf64lriscv Compiler flags: -march=rv64gc -mabi=lp64d -mcmodel=medany
Key features:
- Privilege modes: M-mode (machine), S-mode (supervisor), U-mode (user). Kernel runs in S-mode.
- Boot (
boot.S): OpenSBI firmware loads kernel at0x80200000in M-mode. Boot code parks secondary harts, configures mstatus (MPP=supervisor, MPIE=1), sets mepc to S-mode entry, executesecallto invoke OpenSBI runtime which transitions to S-mode. S-mode code zeroes BSS, sets up stack, callskernel_main(dtb_addr). - Trap handling (
trap.S): Uses Direct mode (stvec = base address). Singletrap_vectorentry point decodes scause: bit 63 = interrupt, code 8 = ecall from U-mode (syscall), others = exception. Full register save/restore viasave_regs/restore_regsmacros (272-byte frame: all GPRs + sstatus, sepc, scause, stval). - MMU: Sv39 (3-level page table, 39-bit VA, 4 KB granule). Virtual addresses sign-extended from bit 38. Kernel at
0xFFFFFF80_80000000, user space up to0x0000003F_FFFFFFFF(256 GB). SATP register for mode/ASID/PPN. TLB flush viasfence.vma. - Context switch (
context_switch.S): Saves/restores s0-s11, ra, sp (112-byte frame). User-mode entry viasretwith sstatus.SPP=0 (return to U-mode), sepc = user entry point. - Interrupt controller: PLIC (Platform-Level Interrupt Controller) — see
plic.c/plic.h - Timer: CLINT (Core-Local Interruptor) — see
clint.c/clint.h, provides machine timer and software interrupts - UART: Memory-mapped UART
- UEFI boot:
uefi_boot.c/uefi_boot.Sfor UEFI boot path - Syscalls: Linux RISC-V convention — a7=syscall number, a0-a5=args, a0=return value
Platform support: QEMU virt with OpenSBI firmware.
Architecture-Specific Assembly Code
Each architecture has hand-written assembly for the low-level kernel paths that cannot be written in C:
Boot Sequences
| Architecture | Entry point | First C function | Key steps |
|---|---|---|---|
| x86 (32-bit) | _start (boot.asm) |
kernel_main |
Multiboot header, GDT, IDT, paging, BSS clear |
| x86_64 | _start (boot64.asm) |
kernel_main |
Long mode transition, 4-level paging, BSS clear |
| ARM32 | _start → reset_handler (boot.S) |
kernel_main(dtb) |
SVC mode, VBAR, per-mode stacks, PL011 init, BSS clear, .data copy |
| AArch64 | _start (boot.S) |
kernel_main(dtb) |
EL3→EL2→EL1, HCR_EL2, CPACR_EL1, VBAR_EL1, BSS clear |
| RISC-V | _start → smode_entry (boot.S) |
kernel_main(dtb) |
M-mode config, ecall to OpenSBI, S-mode, BSS clear |
Context Switching
All architectures save callee-saved registers to the kernel stack and switch stack pointers. The implementations differ in what they save:
- ARM32: r4-r11, lr (via
stmfd/ldmfd), plus VFP s16-s31 and fpscr - AArch64: x19-x30, sp_el0, plus page table registers (sctlr_el1, ttbr0_el1, tcr_el1, mair_el1), plus NEON/FP via
fpu_save()/fpu_restore() - RISC-V: s0-s11, ra, sp (via
sd/ld) - x86/x86_64: Managed via TSS ring-0 stack switching and ISR stubs (not a separate context_switch.S)
Exception/Interrupt Handling
- ARM32: 8-entry vector table, each entry is
LDR PC, [PC, #offset]. Handlers save context, call C dispatchers, restore and return vialdmfd sp!, {pc}^. - AArch64: 16-entry vector table (2 KB aligned, 128 bytes each). Syscalls detected by ESR_EL1.EC field. Returns via
eret. - RISC-V: Single trap entry decodes scause. Syscalls (ecall from U-mode, code 8) dispatch to
riscv64_syscall_handle(). Returns viasret. - x86/x86_64: Interrupt stubs in
.asm/.sfiles push error codes and interrupt numbers, jump to common ISR handler.
User-Mode Entry
All architectures provide task_start_usermode_asm() and enter_usermode_asm():
- ARM32: Sets USR-mode SP (banked, must switch modes), builds SVC return frame, executes
ldmfd sp!, {pc}^ - AArch64: Sets sp_el0, loads entry into elr_el1, configures spsr_el1 for EL0t, executes
eret - RISC-V: Sets sepc to user entry, configures sstatus (SPP=0, SPIE=1), saves user sp in sscratch, executes
sret
Build Targets
The Makefile provides convenient per-architecture build targets:
make build32 # x86 32-bit (BIOS)
make build64 # x86 64-bit (BIOS)
make buildarm # ARM32
make buildaarch64 # AArch64 (BIOS)
make buildaarch64-uefi # AArch64 (UEFI)
make buildriscv64 # RISC-V 64 (BIOS)
make buildriscv64-uefi # RISC-V 64 (UEFI)
make buildall # All combinations
Each target invokes make with the appropriate ARCH= and BOOT_MODE= overrides. The build system generates architecture-specific output directories:
build/32bit-bios-debug/
build/64bit-uefi-release/
build/arm-bios-debug/
build/aarch64-bios-debug/
build/riscv64-bios-debug/
QEMU run targets are also available:
make run32 # QEMU q35, 32-bit
make run64 # QEMU q35, 64-bit
make runarm # QEMU virt, cortex-a15
make runaarch64 # QEMU virt, cortex-a53
Cross-Compilation Setup
Toolchain Detection
The build system (build/toolchain.mk) auto-detects available cross-compilers:
- x86/x86_64: Requires the Forest OS cross-toolchain (
i686-forestos-gcc/x86_64-forestos-gcc) built fromforestos-toolchain/. A fallback host toolchain is available for 64-bit when the cross-compiler is unavailable. - ARM32: Auto-detects
arm-none-eabi-gcc,arm-linux-gnueabi-gcc, orarm-linux-gnueabihf-gcc - AArch64: Auto-detects
aarch64-linux-gnu-gccoraarch64-none-elf-gcc - RISC-V: Auto-detects
riscv64-unknown-elf-gcc,riscv64-linux-gnu-gcc, orriscv64-elf-gcc
Architecture Flags
| ARCH | ARCH_FLAGS | ARCH_LDFLAGS |
|---|---|---|
| 32 | -m32 -march=i386 -mtune=i386 |
-m elf_i386 |
| 64 | -m64 -march=x86-64 -mcmodel=kernel |
-m elf_x86_64 |
| arm | -march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=softfp -marm |
-m armelf |
| aarch64 | -march=armv8-a |
-m aarch64elf |
| riscv64 | -march=rv64gc -mabi=lp64d -mcmodel=medany |
-m elf64lriscv |
Kernel Source Selection
build/kernel-sources.mk selects architecture-specific source files based on ARCH:
ifeq ($(ARCH),arm)
ARCH_EXTRA_OBJECTS := $(ARM32_OBJECTS)
else ifeq ($(ARCH),aarch64)
ARCH_EXTRA_OBJECTS := $(AARCH64_OBJECTS)
else ifeq ($(ARCH),riscv64)
ARCH_EXTRA_OBJECTS := $(RISCV64_OBJECTS)
else ifeq ($(ARCH),64)
ARCH_EXTRA_OBJECTS := $(X86_64_OBJECTS)
endif
The src/arch/*.c shared objects are always linked regardless of architecture.
Linker Scripts
Each architecture has its own linker script:
- x86 32-bit:
src/link.ld - x86 64-bit:
src/link64.ld - ARM32:
src/arm32/link.ld - AArch64:
src/aarch64/link.ld - RISC-V 64:
src/riscv64/link.ld(BIOS) orsrc/riscv64/link_uefi.ld(UEFI)
Current Status
| Architecture | Boot | MMU | Interrupts | Context Switch | Syscalls | SMP | UEFI | Status |
|---|---|---|---|---|---|---|---|---|
| x86 (32-bit) | Complete | Complete | Complete | Complete | Complete | Partial | Yes | Working |
| x86_64 | Complete | Complete | Complete | Complete | Complete | Partial | Yes | Working |
| ARM32 | Complete | Complete | Complete (GIC) | Complete | Complete | In progress | No | Working |
| AArch64 | Complete | Complete | Complete (GICv3) | Complete | Complete | In progress | Yes | Working |
| RISC-V 64 | Complete | Complete | Complete (PLIC) | Complete | Complete | In progress | Yes | In progress |
Known TODOs
- RISC-V:
arch_supports_feature()has nodetect_features_riscv64()implementation yet — feature detection always returns 0. - SMP: All architectures have
smp.cfiles but multi-core bring-up is in progress across the board. - x86 32-bit: Most x86-specific code lives in
src/directly rather thansrc/x86_32/, making it less cleanly separated than the ARM/RISC-V ports. - Signal handling: x86_64, ARM32, AArch64, and RISC-V all have
signal.c— this is one of the more complete cross-arch subsystems. - Sound drivers: Each architecture has its own
sound_{arch}.cfor architecture-specific audio hardware support.
This page covers the Fern kernel's multi-architecture support. For the build system configuration, see Build System. For memory management details, see Memory Management.