ajhahn.de
← all repos

Repository

FlashOS

Bare-metal AArch64 kernel for Raspberry Pi 4 and QEMU, written in Flash, Zig and Assembly.

Flash · updated Jun 2026

  • aarch64
  • armv8-a
  • bare-metal
  • embedded-systems
  • kernel
  • low-level
  • mmu
  • operating-system
  • qemu
  • raspberry-pi-4
  • scheduler
  • systems-programming
  • zig

Languages

  • Flash 53.9%
  • Markdown 21.2%
  • Zig 12.5%
  • Assembly 7.5%
  • Shell 3.9%
  • YAML 0.6%
  • Python 0.4%

Readme

FlashOS

AArch64 bare-metal kernel for the Raspberry Pi 4B and QEMU -M virt

CI Coverage Version Zig 0.16.0 aarch64-elf License

Documentation · Setup · Port · Versioning · Changelog · License

English · Deutsch


FlashOS booting on a Raspberry Pi into the fsh shell

The boot above is a captured serial console of FlashOS booting on real Raspberry Pi 4B hardware to the login: prompt; the trailing fsh session — help, ls, and sysinfo — replays the shell's real output at a readable cadence, before a final reboot loops the demo back to the boot.

About

FlashOS is a bare-metal AArch64 kernel that boots on Raspberry Pi 4B hardware and under QEMU. The kernel core is written in Flash — a systems language that transpiles to Zig — with the boot path, exception vectors, and context switch in AArch64 assembly. The build is driven entirely by build.zig, which transpiles the .flash modules through a pinned flashc. The current release ships with a complete uniprocessor process lifecycle (fork, exec, exit, wait, kill), leak-free across stress cycles, exercised by an in-kernel [TEST]/[PASS]/[FAIL] harness and a host-side unit test suite.

Specifications

Hardware Raspberry Pi 4 Model B (BCM2711)
Architecture AArch64 (ARMv8-A)
Languages Flash (transpiled to Zig) + AArch64 assembly
Toolchain flashc (pinned) + Zig 0.16.0 +aarch64-elf binutils
Targets RPi 4B hardware,qemu-system-aarch64 -M raspi4b, and qemu-system-aarch64 -M virt

Features

  • Two-stage boot. EL3 armstub configures the GIC and erets into the kernel at EL1 (Pi). On QEMU -M virt, boot.S does the EL3→EL1 drop itself.
  • Dual-target build. -Dboard=rpi4b or -Dboard=virt switches the per-board driver bag (uart, gpio, timer, irq), the linker script, and the boot quirks at comptime.
  • Four-level MMU. Identity map for early bring-up, linear-high map for the kernel, demand-allocated user pages with per-region flags (text RX, data/heap/stack RW+UXN).
  • Priority round-robin scheduler with timer-driven preemption.
  • Process lifecycle. fork / exec / exit / wait / kill, zombie reap path, leak-free across stress cycles.
  • ELF64 loader. sys_execve resolves a path through the VFS, streams each PT_LOAD segment into a freshly built address space with the right permissions, and eagerly maps the top stack page before copying the argv block onto the new user stack.
  • Userland mini-libc (flibc). SVC wrappers, printf over sys_writeConsole, bump allocator over brk / sbrk, fork / wait / exit / execve. Linked into ELF demos by the build, kept under user_space/lib/flibc/.
  • Heap via sys_brk / sys_sbrk. Pages are demand-allocated by the page-fault path inside [HEAP_BASE, brk); shrinks unmap and free.
  • Region-aware page-fault dispatch. do_data_abort classifies by user VA region (heap / stack / stack-guard / text / wild) and panics-and-zombies on out-of-region access; the parent's sys_wait reaps the offender so the harness keeps running.
  • Stack guard. A 1-page unmapped region below the legal stack range turns runaway recursion into a [KERN] stack overflow diagnostic instead of memory corruption.
  • Unified file descriptors. A single tagged fds table per task (console / pipe / file) behind one read / write / close / dup2 ABI; fd 0/1/2 are pre-installed console slots, fork inherits the table and execve preserves it, so a shell can hand a child redirected stdio. Anonymous pipes (sys_pipe) ride the same table.
  • Interactive shell (fsh). A userland REPL at /bin/fsh over a mini-libc (flibc): a readline line editor with TAB completion (double-TAB lists candidates), a tokenizer with a single | pipe stage, in-process built-ins (cd / pwd / exit / logout / help / free / whoami / reboot), a Unix-style #/$ privilege prompt, and fork + execvp (/bin/<name> resolution) for externals — plus /bin/echo, /bin/cat, /bin/ls (the stateless sys_readdir consumer), /bin/grep (literal line search), /bin/cp / /bin/mv / /bin/rm (FAT32 file management over the create/unlink/rename syscalls), /bin/meminfo, /bin/forkbomb (a capped leak probe), /bin/sysinfo (a key/value system summary), /bin/cpuinfo (CPU temperature + clock), /bin/uptime (time since boot), /bin/less (a full-screen pager), /bin/edit (a full-screen text editor), /bin/clear (a screen wipe), and /bin/passwd. Reads /etc/fshrc at startup; sys_chdir gives each task a working directory. The coreutils use fixed-size stack/static buffers; the userland heap (brk/sbrk behind flibc's bump malloc) has its first consumer in /bin/edit's growable buffer.
  • Process identity, login & permissions. Every task carries real + effective uid/gid (inherited across fork, preserved across execve) behind a getuid/setuid-family ABI, and every file carries mode/uid/gid metadata enforced at the open/write/exec syscall boundary (-EACCES, root bypasses). Boot runs /bin/login as a session supervisor: the kernel verifies the password with PBKDF2-HMAC-SHA256 + a constant-time compare (sys_authenticate — the KDF never leaves the kernel), then login forks a child that drops privilege and execs the user's shell; exit returns to the login: prompt. Passwords live in a writable /mnt/shadow on the SD card (protected to 0600 root:root by a FAT32 permission overlay, with the read-only initramfs seed as the always-bootable fallback) and are changed with passwd / sys_passwd — fresh kernel-minted salt, splice-safe in-place rewrite. Password echo is suppressed through SYS_SET_CONSOLE_MODE. The seed accounts use fixed public salts (build reproducibility); rotated records get random salts.
  • Syscalls dispatched via svc and an indexed table — see Documentation §5.
  • USB-C gadget console. The Pi's USB-C port enumerates as a CDC-ACM serial device (BCM2711 DWC2 OTG — Full-Speed, polled, slave/PIO): a single C-to-C cable to a Mac carries both power and the interactive fsh console (/dev/tty.usbmodem…, no driver install). User/shell output switches to USB when enumerated and falls back to the Mini-UART otherwise.
  • Two UARTs. Mini-UART (UART1) for the console fallback + kernel diagnostics, dedicated PL011 for an out-of-band trace channel.
  • Kernel symbol table generated by a two-pass populate-syms step and consumed by the function-entry tracer (runtime intact, but currently inert — Zig has no -fpatchable-function-entry=2 equivalent yet).
  • In-kernel test harness ([TEST]/[PASS]/[FAIL] + tally, 30 scenarios) plus a host-side zig build test suite (468 host tests across 41 modules).

Quick start

Install the toolchain:

brew install zig aarch64-elf-binutils qemu

FlashOS's source modules are written in Flash and transpiled to Zig at build time by flashc. Build the pinned compiler once — build.zig looks for it at ~/Flash/zig-out/bin/flashc-stage1 by default (override with -Dflashc=<path>):

git clone https://github.com/ajhahnde/Flash.git ~/Flash
git -C ~/Flash checkout "$(grep -oE '[0-9a-f]{40}' flash-toolchain.lock)"
( cd ~/Flash && zig build stage1 )   # → ~/Flash/zig-out/bin/flashc-stage1

Build everything for the Pi (kernel8.img + armstub8.bin land in zig-out/):

zig build                   # default: -Dboard=rpi4b

Or build for QEMU -M virt (no armstub):

zig build -Dboard=virt

Run the kernel under QEMU:

zig build -Dboard=rpi4b run        # raspi4b machine (Pi 4 model)
zig build -Dboard=virt  run-virt   # generic ARMv8 virt machine

Run host-side unit tests (page allocator + ELF parser):

zig build test

For the full hardware flow (two-pass build with symbol-table population and an interactive deploy prompt):

./build.sh

See Setup for the SD-card layout, firmware files, and serial-console setup.

Build steps

Step What it does
zig build (or -Dboard=rpi4b) Default — Pi:kernel8.img + armstub8.bin
zig build -Dboard=virt virt:kernel8.img only (no armstub)
zig build kernel Kernel image only
zig build armstub (rpi4b only) Armstub only
zig build populate-syms Regenerate src/symbol_area.S from the linked ELF
zig build deploy (rpi4b only) Copy artefacts + RPi firmware to $SD_BOOT
zig build -Dboard=rpi4b run Boot under qemu-system-aarch64 -M raspi4b
zig build -Dboard=virt run-virt Boot under qemu-system-aarch64 -M virt
zig build -Dboard=virt test-virt Boot virt, watchdog asserts the boot reaches the fsh prompt
zig build -Dboard=rpi4b test-rpi4b Boot raspi4b, watchdog asserts the boot reaches the fsh prompt
zig build -Dboard=virt iso Build a GRUB-EFI rescue ISO (virt only)
zig build test Host-side unit tests (468 tests, 41 modules)
zig build clean Remove .zig-cache/ and zig-out/

The default optimisation mode is ReleaseSmall. Override with -Doptimize=ReleaseSafe (or Debug, ReleaseFast).

Repository layout

src/                kernel core (Flash + AArch64 assembly)
src/board/<name>/   per-board driver bag (rpi4b / virt) + linker script
user_space/         PID 1 image + in-kernel test harness
user_space/lib/flibc/  userland mini-libc for ELF demos
lib/                shared kernel↔user constants (syscall IDs)
tools/              hand-rolled ELF demos (hello, stackbomb, flibc_demo)
tests/              host-side unit tests
armstub/            EL3 → EL1 bootstrap shim (Pi only)
scripts/            symbol-table generation, iso, QEMU test watchdog,
                    Pi-baseline verifier
assets/             logo and visual assets
build.zig           the only build entry point
build.sh            two-pass build orchestrator + deploy prompt
flash-toolchain.lock  pinned flashc revision (Flash→Zig transpiler)
config.txt          RPi 4 firmware configuration

A deeper walk-through of each subsystem is in Documentation.

Versioning

v[MAJOR].[MINOR].[PATCH]. Per-tag notes live on the releases page.

AI assistance

The prose docs in this repo (README, DOCUMENTATION, CHANGELOG, PORT) are LLM-drafted under my review. They're kept honest by the build, not by trust: the OS is verified by booting it, not by describing it.

  • Boots to a login shell on QEMU virt and Raspberry Pi 4B from the same kernel ABI
  • -Dboot-selftest=true runs the in-kernel [TEST] harness at PID 1 before the login prompt — process, filesystem, memory-fault, and device scenarios, each bracketed by free-page checkpoints to surface leaks
  • The kernel is written in Flash and transpiled to Zig via the sibling flashc compiler — pinned in flash-toolchain.lock

If a doc claims a subsystem works, the boot path is what exercises it.

The docs are also kept current by an automated drift check that keeps the contract values quoted across them — version, boot-contract numbers, ABI constants — in sync with the live tree, so a stale copy is caught rather than shipped.

Source code (src/*.flash, the Zig drivers, the AArch64 assembly) is authored by me.

License

Apache License, Version 2.0. See License.

See also

  • Flash — a systems language and Zig transpiler.
  • eeco — self-maintaining workflow ecosystem.
  • the-way-out — top-down pixel-art escape-room shooter.
  • Theria — 2.5D MOBA built in Godot 4.

Next: Documentation →

Recent commits

Files