Repository
FlashOS
Bare-metal AArch64 kernel for Raspberry Pi 4 and QEMU, written in Flash, Zig and Assembly.
- 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
AArch64 bare-metal kernel for the Raspberry Pi 4B and QEMU -M virt
Documentation · Setup · Port · Versioning · Changelog · License
English · Deutsch
The boot above is a captured serial console of FlashOS booting on real Raspberry Pi 4B hardware to the
login:prompt; the trailingfshsession —help,ls, andsysinfo— replays the shell's real output at a readable cadence, before a finalrebootloops 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.Sdoes the EL3→EL1 drop itself. - Dual-target build.
-Dboard=rpi4bor-Dboard=virtswitches 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_execveresolves 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,printfoversys_writeConsole, bump allocator overbrk/sbrk,fork/wait/exit/execve. Linked into ELF demos by the build, kept underuser_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_abortclassifies by user VA region (heap / stack / stack-guard / text / wild) and panics-and-zombies on out-of-region access; the parent'ssys_waitreaps 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 overflowdiagnostic instead of memory corruption. - Unified file descriptors. A single tagged
fdstable per task (console/pipe/file) behind oneread/write/close/dup2ABI; fd 0/1/2 are pre-installed console slots,forkinherits the table andexecvepreserves 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/fshover a mini-libc (flibc): areadlineline 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, andfork+execvp(/bin/<name>resolution) for externals — plus/bin/echo,/bin/cat,/bin/ls(the statelesssys_readdirconsumer),/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/fshrcat startup;sys_chdirgives each task a working directory. The coreutils use fixed-size stack/static buffers; the userland heap (brk/sbrkbehind flibc's bumpmalloc) 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 acrossexecve) behind agetuid/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/loginas 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;exitreturns to thelogin:prompt. Passwords live in a writable/mnt/shadowon the SD card (protected to0600 root:rootby a FAT32 permission overlay, with the read-only initramfs seed as the always-bootable fallback) and are changed withpasswd/sys_passwd— fresh kernel-minted salt, splice-safe in-place rewrite. Password echo is suppressed throughSYS_SET_CONSOLE_MODE. The seed accounts use fixed public salts (build reproducibility); rotated records get random salts. - Syscalls dispatched via
svcand 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
fshconsole (/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-symsstep and consumed by the function-entry tracer (runtime intact, but currently inert — Zig has no-fpatchable-function-entry=2equivalent yet). - In-kernel test harness (
[TEST]/[PASS]/[FAIL]+ tally, 30 scenarios) plus a host-sidezig build testsuite (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
virtand Raspberry Pi 4B from the same kernel ABI -Dboot-selftest=trueruns 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
flashccompiler — pinned inflash-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.
Recent commits
-
62f80caci: refresh the Pi-HW baseline for v0.7.0 Jun 2026 -
146e15frelease: FlashOS v0.7.0 — full-screen text editor Jun 2026 -
0a9d568ci: refresh the Pi-HW baseline for v0.6.0 and resolve flashc in the verify script Jun 2026 -
88ea222release: FlashOS v0.6.0 — FAT32 file management & coreutils Jun 2026 -
10164ccfeat: FAT32 file create/unlink/rename and grep/cp/mv/rm coreutils Jun 2026 -
b16de80release: FlashOS v0.5.0 — hardware monitoring & mailbox metrics Jun 2026 -
b0d131arefactor: isolate the AArch64 ISA core under arch/aarch64/ Jun 2026 -
f440c9dci: build qemu-system-aarch64 11.0.1 from source for the raspi4b machine Jun 2026 -
9e55c98ci: run on ubuntu-24.04 so QEMU has the raspi4b machine type Jun 2026 -
5cce7eefix: format the rpi4b test disk as -c 1 so it is valid FAT32 Jun 2026 -
f7475a8ci: install mtools for the rpi4b FAT32 test-disk build Jun 2026 -
7ff1df3fix: make_test_disk.sh POSIX-portable so dash on CI can run it Jun 2026 -
10c1f8adocs: update copyright holder to ajhahnde Jun 2026 -
4aae31dci: freeze the QEMU virt board; rpi4b becomes the boot gate Jun 2026 -
829d038build: regenerate the kernel symbol table Jun 2026
Files
-
.github
-
workflows
-
-
armstub
-
docs
-
firmware
-
lib
-
scripts
-
src
-
board
- block_dev.flash
- board.flash
- console.flash
- elf.flash
- execve.flash
- fat32_backend.flash
- fat32.flash
- fdtable.flash
- file.zig
- fork.flash
- generic_timer.flash
- hwrng.flash
- initramfs_backend.flash
- initramfs.flash
- kernel.flash
- klog_ring.flash
- mailbox.flash
- mm_user.flash
- overlay.flash
- page_alloc.flash
- path.flash
- perm.flash
- pipe.flash
- pwfile.flash
- sched.flash
- sdhci_cmd.flash
- sha256.flash
- shadow.flash
- start.zig
- symbol_area.S
- sys.flash
- task_layout.flash
- usb_descriptors.flash
- usb_tx_ring.flash
- user_layout.flash
- utilc.flash
- vfs.zig
- wait_queue.flash
-
-
tools
- argv_echo_linker.ld
- argv_echo.flash
- cat.flash
- clear.flash
- coreutil_linker.ld
- cp.flash
- cpuinfo.flash
- dmesg.flash
- echo.flash
- edit.flash
- flibc_demo_linker.ld
- flibc_demo.flash
- forkbomb.flash
- fsh_linker.ld
- gen_shadow.zig
- grep_match.flash
- grep.flash
- hello_linker.ld
- hello.flash
- initramfs.S
- less.flash
- login.flash
- ls.flash
- meminfo.flash
- mv.flash
- passwd.flash
- pid1_linker.ld
- rm.flash
- stackbomb.flash
- sysinfo.flash
- uptime.flash
- .gitattributes
- .gitignore
- .zigversion
- build.sh
- build.zig
- build.zig.zon
- CHANGELOG.md
- config.txt
- DOCUMENTATION.md
- flash-toolchain.lock
- flashos.env.zsh
- LICENSE.md
- PORT.md
- README.md
- SETUP.md
- VERSIONING.md