ajhahn.de
← FlashOS
Markdown 306 lines
<div align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="assets/flashos_logo_dark.png">
    <img src="assets/flashos_logo_light.png" alt="FlashOS" width="420">
  </picture>

<h3>AArch64 bare-metal kernel for the Raspberry Pi 4B and QEMU <code>-M virt</code></h3>

<p>
    <a href="https://github.com/ajhahnde/FlashOS/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/ajhahnde/FlashOS/test.yml?branch=main&style=flat-square&label=ci" alt="CI"></a>
    <a href="https://codecov.io/gh/ajhahnde/FlashOS"><img src="https://img.shields.io/codecov/c/github/ajhahnde/FlashOS?style=flat-square&label=coverage" alt="Coverage"></a>
    <img src="https://img.shields.io/badge/version-v0.7.0-f59e0b?style=flat-square" alt="Version">
    <img src="https://img.shields.io/badge/zig-0.16.0-lightgrey?style=flat-square" alt="Zig 0.16.0">
    <img src="https://img.shields.io/badge/target-aarch64--elf-lightgrey?style=flat-square" alt="aarch64-elf">
    <img src="https://img.shields.io/badge/license-Apache--2.0-lightgrey?style=flat-square" alt="License">
  </p>

<p>
    <a href="DOCUMENTATION.md"><b>Documentation</b></a> ·
    <a href="SETUP.md"><b>Setup</b></a> ·
    <a href="PORT.md"><b>Port</b></a> ·
    <a href="VERSIONING.md"><b>Versioning</b></a> ·
    <a href="CHANGELOG.md"><b>Changelog</b></a> ·
    <a href="LICENSE.md"><b>License</b></a>
  </p>

<p>
    <b>English</b> ·
    <a href="docs/de/README.md">Deutsch</a>
  </p>
</div>

---

<p align="center">
  <img src="assets/boot_demo.gif" alt="FlashOS booting on a Raspberry Pi into the fsh shell" width="780">
</p>

> 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](https://github.com/ajhahnde/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 `eret`s 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](DOCUMENTATION.md#5-syscalls--exceptions).
- **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:

```bash
brew install zig aarch64-elf-binutils qemu
```

FlashOS's source modules are written in
[Flash](https://github.com/ajhahnde/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>`):

```bash
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/`):

```bash
zig build                   # default: -Dboard=rpi4b
```

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

```bash
zig build -Dboard=virt
```

Run the kernel under QEMU:

```bash
zig build -Dboard=rpi4b run        # raspi4b machine (Pi 4 model)
```

```bash
zig build -Dboard=virt  run-virt   # generic ARMv8 virt machine
```

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

```bash
zig build test
```

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

```bash
./build.sh
```

See [Setup](SETUP.md) 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

```text
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](DOCUMENTATION.md).

## Versioning

`v[MAJOR].[MINOR].[PATCH]`. Per-tag notes live on the
[releases page](https://github.com/ajhahnde/FlashOS/releases).

## 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](LICENSE.md).

## See also

- **[Flash](https://github.com/ajhahnde/Flash)** — a systems language and Zig transpiler.
- **[eeco](https://github.com/ajhahnde/eeco)** — self-maintaining workflow ecosystem.
- **[the-way-out](https://github.com/ajhahnde/the-way-out)** — top-down pixel-art escape-room shooter.
- **[Theria](https://github.com/ajhahnde/Theria)** — 2.5D MOBA built in Godot 4.


---

[Next: Documentation →](DOCUMENTATION.md)