Build System

Aegis uses a monolithic GNU Make build system with a single top-level Makefile that orchestrates kernel compilation, userspace program builds, rootfs image construction, and ISO packaging. The build is designed for cross-compilation on a Linux host targeting x86_64 bare-metal.

A parallel ARM64 build lives at kernel/arch/arm64/Makefile — a self-contained invocation (make -C kernel/arch/arm64) that produces build/aegis-arm64.elf and, via make image, a Linux arm64 Image format kernel suitable for qemu-system-aarch64 -kernel and Raspberry Pi 5 firmware loading. The arm64 build reuses the same shared sources under kernel/core/, kernel/fs/, kernel/net/, kernel/syscall/, etc., and cross-compiles the Rust capability library to aarch64-unknown-none. tools/build-pi5-image.sh wraps the arm64 build to produce a ready-to-flash FAT32 SD card layout.

v1 note: The build system is v1 and evolving alongside the OS. Contributions are welcome – file issues or propose changes at exec/aegis.

Toolchain Requirements

x86_64 (primary)

Tool Binary Purpose
Cross GCC x86_64-elf-gcc (or x86_64-linux-gnu-gcc) Kernel C compilation (freestanding)
Cross LD x86_64-elf-ld Kernel linking
NASM nasm x86_64 assembly (boot stub, ISR stubs, context switch, syscall entry)
Cargo (nightly) cargo +nightly Rust capability library (kernel/cap/)
musl libc Built from source Dynamic C library for userspace programs
objcopy x86_64-elf-objcopy Binary blob embedding (initrd)
Limine Vendored in tools/limine/ Bootloader (BIOS + UEFI); host tool built from tools/limine/limine.c
xorriso xorriso ISO construction (xorriso -as mkisofs)
mtools + dosfstools mmd, mcopy, mkfs.fat FAT ESP image creation
debugfs /sbin/debugfs ext2 rootfs image manipulation
sgdisk /usr/sbin/sgdisk GPT disk image partitioning

ARM64 (parallel build)

Tool Binary Purpose
Cross GCC aarch64-elf-gcc or aarch64-linux-gnu-gcc Kernel C compilation + inline AArch64 assembly
Cross LD aarch64-elf-ld Kernel linking
Cargo (nightly) cargo +nightly with aarch64-unknown-none target Rust capability library cross-compile
dtc dtc Device tree source manipulation (optional)
QEMU qemu-system-aarch64 ARM64 kernel testing

No NASM — AArch64 assembly uses GNU as via aarch64-elf-gcc. No GRUB — the arm64 kernel is built as a Linux arm64 Image format file (64-byte header + flat binary), loaded directly by QEMU’s -kernel or by Raspberry Pi firmware reading config.txt. Pi firmware blobs are fetched by tools/build-pi5-image.sh from github.com/raspberrypi/firmware and cached locally.

Compiler Flags

The kernel is compiled with a strict freestanding configuration:

CFLAGS = \
    -ffreestanding -nostdlib -nostdinc \
    -isystem $(GCC_INCLUDE) \
    -mcmodel=kernel \
    -fno-pie -fno-pic \
    -mno-red-zone -mno-mmx -mno-sse -mno-sse2 \
    -fno-stack-protector \
    -fno-omit-frame-pointer \
    -g \
    -Wall -Wextra -Werror \
    -I<kernel subdirectories>

Key flag rationale:

  • -nostdinc + -isystem $(GCC_INCLUDE): Excludes all system headers, then adds back only GCC’s own freestanding headers (stdint.h, stddef.h, etc.)
  • -mcmodel=kernel: Generates code assuming execution in the higher-half virtual address space (above 0xFFFFFFFF80000000)
  • -mno-red-zone: Required for x86_64 kernels; interrupt handlers use stack space below RSP that the red zone would otherwise reserve
  • -mno-mmx -mno-sse -mno-sse2: No floating-point or SIMD in kernel code
  • -fno-stack-protector: No stack canary (no runtime support for __stack_chk_fail)
  • -fno-omit-frame-pointer: Preserves frame pointers for reliable stack traces
  • -Werror: All warnings are build failures

Assembly is compiled with NASM in ELF64 mode:

ASFLAGS = -f elf64

Kernel Source Organization

The kernel source is divided into subsystem directories, each producing its own set of object files:

kernel/
├── arch/x86_64/    # Architecture: boot, GDT, IDT, PIC, PIT, LAPIC, IOAPIC,
│                   # SMP, TLB, syscall entry, PCIe, ACPI, serial, VGA, keyboard,
│                   # mouse, SMAP/SMEP
├── core/           # main.c, printk, random, capability policy
├── cap/            # Rust capability validation library (Cargo project)
├── mm/             # Physical/virtual memory management, KVA, VMA
├── sched/          # Scheduler, wait queues
├── proc/           # Process management, ELF loader
├── syscall/        # Syscall dispatch + per-category handlers (io, memory,
│                   # process, exec, identity, cap, time, file, dir, meta,
│                   # signal, socket, random, disk, futex)
├── fs/             # VFS, ext2, initrd, ramfs, procfs, memfd, console, pipe,
│                   # block device, GPT, poll
├── tty/            # TTY, PTY
├── signal/         # Signal delivery
├── drivers/        # NVMe, xHCI, USB HID, USB mouse, virtio-net, RTL8169,
│                   # framebuffer, ramdisk
└── net/            # Ethernet, IP, UDP, TCP, sockets, UNIX sockets, epoll

Assembly sources in kernel/arch/x86_64/:

File Purpose
boot.asm Multiboot2 entry, 32-bit trampoline, GDT setup, jump to 64-bit
isr.asm Interrupt service routine stubs (pushes error code, calls C handler)
ctx_switch.asm Context switch: saves/restores register state between tasks
syscall_entry.asm SYSCALL/SYSRET entry point, user-kernel transition
ap_trampoline.asm SMP application processor startup trampoline (real mode to long mode)

Primary Make Targets

Build Targets

Target Command Description
all make Builds build/aegis.elf (kernel binary only)
iso make iso Full ISO: kernel + rootfs + ESP + Limine bootloader (BIOS + UEFI)
test-iso make test-iso Text-mode ISO for automated testing (Limine test config, timeout 0)
disk make disk GPT disk image wrapping the rootfs (for NVMe testing)
rootfs make rootfs ext2 rootfs image only
build-musl make build-musl Build musl libc as a shared library
curl_bin make curl_bin Build BearSSL + curl (external dependency)

Run Targets

Target Command Description
run make run Boot ISO in QEMU with q35, std VGA, serial on stdio
run-fb make run-fb Boot ISO in QEMU with q35, virtio-vga (framebuffer)

Both run targets automatically attach NVMe if build/disk.img exists, add xHCI + USB keyboard, and configure the isa-debug-exit device for programmatic shutdown.

Debug Targets

Target Command Description
gdb make gdb Launch QEMU with GDB server on :1234, then attach GDB
sym make sym ADDR=0x... Resolve a kernel address to source file + line via addr2line
trace make trace [TRACE_CPU=… TRACE_SECS=…] Boot under TCG with QEMU exception/reset logging to diagnose a kernel that dies with no serial output; captures faults + serial to build/qemu-trace.log / qemu-serial.log. See Observability & Debugging.

Test Targets

Target Command Description
test make test Build ISO, run the Rust integration test suite (tests/)
test-q35 make test-q35 Build ISO + disk, run tests with AEGIS_PRESET=q35
install-test make install-test Run installer test via Vortex stack

Clean

make clean

Removes the build/ directory, cleans all user program sub-Makefiles, and runs cargo clean on the Rust capability library.

The kernel is linked with a custom linker script at tools/linker.ld:

OUTPUT_FORMAT("elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)

PHYS_BASE = 0x100000;
KERN_VMA  = 0xFFFFFFFF80000000;

Memory Layout

Physical Address Space:
┌──────────────────────────┐ 0x100000 (1 MB)
│ .multiboot               │  ← Must be in first 8 KB (Multiboot2 spec)
│ .text.boot               │  ← 32-bit boot stub, runs at physical addresses
├──────────────────────────┤
│                          │
│  (identity mapped 1:1    │
│   during early boot)     │
│                          │
└──────────────────────────┘

Virtual Address Space (higher half):
┌──────────────────────────┐ 0xFFFFFFFF80000000 + offset
│ .text                    │  ← Kernel code (AT physical)
│ .rodata                  │  ← Read-only data, embedded blobs
│ .data                    │  ← Initialized data
│ .bss                     │  ← Zero-initialized data
│ _kernel_end              │  ← End marker (virtual address)
└──────────────────────────┘

The .multiboot and .text.boot sections run at physical addresses (VMA = LMA). All remaining sections are relocated to the higher half: VMA starts at KERN_VMA + offset, while LMA remains physical. The boot stub sets up page tables to map both the identity region (for the trampoline) and the higher-half region before jumping to main().

x86_64-elf-ld -T tools/linker.ld -nostdlib -o build/aegis.elf \
    <all object files> \
    kernel/cap/target/x86_64-unknown-none/release/libcap.a

The Rust capability library (libcap.a) is linked directly into the kernel ELF alongside the C object files.

The kernel embeds its own address-to-name symbol table so panic and stackshot backtraces print function+0x<offset> live on serial (see Observability & Debugging). This makes the final link a two-pass build:

  1. Link once with weak, empty ksym_* fallback arrays (kernel/core/ksym.c).
  2. tools/gen-ksyms.sh runs nm -n on that image and emits build/ksyms.c — sorted {address, name} arrays — which is then compiled.
  3. Relink with build/ksyms.o; its strong symbols override the weak fallbacks.

A single extra pass is correct because .text is laid out before .rodata in tools/linker.ld: the generated table is const (.rodata), placed after all code, so embedding it never shifts a function address — the addresses captured in pass 1 remain valid in the relink. If build/ksyms.c is ever absent, the weak fallback leaves ksym_count == 0 and the kernel still links (backtraces fall back to hex).

Initrd / Binary Blob Embedding

Aegis embeds a minimal set of programs directly into the kernel ELF as binary blobs in .rodata. This provides the initial userspace programs available before the rootfs is mounted.

Blob Build Process

Source ELF → build/blobs/<name>.bin → objcopy → build/blobs/<name>.o → linked into kernel
  1. Copy: The source ELF is copied to build/blobs/<name>.bin
  2. Convert: objcopy converts the raw binary into an ELF object with the data placed in .rodata (read-only)
  3. Link: The object file is linked into the kernel

Embedded Blobs

Blob Source Notes
login user/bin/login/login.elf Login manager (dynamically linked)
vigil user/bin/vigil/vigil Init system (dynamically linked)
shell user/bin/shell/shell.elf Minimal shell (statically linked)
init user/bin/vigil/vigil Alias for vigil (init process)
echo Built from user/bin/echo/main.c Static build via musl-gcc -static
cat Built from user/bin/cat/main.c Static build via musl-gcc -static
ls Built from user/bin/ls/main.c Static build via musl-gcc -static

The echo, cat, and ls blobs are built as minimal static binaries (musl-gcc -static -O2 -s) directly from their single-file sources, bypassing the normal dynamic-linking build path.

Userspace Program Builds

musl libc

All dynamically-linked userspace programs depend on musl libc 1.2.5, built from source by tools/build-musl.sh:

bash tools/build-musl.sh

Output goes to build/musl-dynamic/:

  • usr/lib/libc.so – shared library
  • lib/ld-musl-x86_64.so.1 – dynamic linker (symlink to libc.so)
  • usr/bin/musl-gcc – GCC wrapper with patched specs file

The script auto-detects stale builds (wrong architecture) and rebuilds as needed.

Simple User Programs

Programs with no extra library dependencies are declared in a single list:

SIMPLE_USER_PROGS = \
    ls cat echo pwd uname clear true false wc grep sort \
    mkdir touch rm cp mv whoami ln chmod chown readlink \
    shutdown reboot login stsh httpd nettest polltest

A macro generates a Make rule for each: user/bin/<name>/<name>.elf depends on musl and is built by running make -C user/bin/<name>.

Programs with Extra Dependencies

Programs requiring additional libraries have explicit dependency declarations:

Program Dependencies
lumen (compositor) libglyph, libcitadel, musl
bastion (display manager) libglyph, libauth, musl
installer libinstall, musl
gui-installer libglyph, libinstall, musl

External Builds

Program Build Script Dependencies
BearSSL tools/build-bearssl.sh None
curl tools/build-curl.sh BearSSL
Rune (text editor) tools/build-rune.sh Rust toolchain

Rust Capability Library

The capability validation subsystem is written in Rust and compiled as a static library:

# x86_64 build (top-level Makefile)
cargo +nightly build --release \
    --target x86_64-unknown-none \
    --manifest-path kernel/cap/Cargo.toml

# AArch64 build (kernel/arch/arm64/Makefile)
cargo +nightly build --release \
    --target aarch64-unknown-none \
    --manifest-path kernel/cap/Cargo.toml

Output: kernel/cap/target/<triple>/release/libcap.a

The *-unknown-none targets produce freestanding binaries with no standard library, suitable for linking into the kernel. The Rust source has zero arch-specific code, zero #[cfg(target_arch)] branches, and no inline asm — the same lib.rs cross-compiles cleanly to both targets with rust-toolchain.toml listing them as installed targets.

Rootfs Image Construction

The rootfs is an ext2 filesystem image built by tools/build-rootfs.sh. The process has three phases:

1. Create Empty Image

dd if=/dev/zero of=build/rootfs.img bs=512 count=<P1_SECTORS>
mke2fs -t ext2 -F -L aegis-root build/rootfs.img

This creates a 44 MiB ext2 filesystem (single source of truth: P1_SECTORS in tools/build-rootfs.sh and the Makefile disk target).

2. Copy Skeleton Directory

The rootfs/ directory in the source tree is replicated into the image verbatim. This provides the base directory structure and configuration files (e.g., /etc/vigil/ service definitions, /etc/passwd, etc.).

3. Install Binaries from Manifest

The rootfs.manifest file is the single source of truth for what goes into the rootfs:

# Format: SOURCE DEST
build/musl-dynamic/usr/lib/libc.so  /lib/libc.so
build/musl-dynamic/usr/lib/libc.so  /lib/ld-musl-x86_64.so.1
user/bin/shell/shell.elf            /bin/sh
user/bin/vigil/vigil                /bin/vigil
...

Rules:

  • Everything under /bin/ and /lib/ is automatically chmod 0755
  • Parent directories are created on demand
  • Missing source files generate a warning but do not fail the build

Assets

The rootfs build also installs optional assets:

  • Wallpaper/logos: Converted from PNG to raw format via Python scripts, installed to /usr/share/
  • TTF fonts: Copied from assets/*.ttf to /usr/share/fonts/
  • Kernel binary: Copied to /boot/aegis.elf for installed-system boot
  • CA certificates: tools/cacert.pem installed to /etc/ssl/certs/ca-certificates.crt

ISO Construction

The ISO is assembled with xorriso -as mkisofs and uses Limine for both BIOS (El Torito CD boot) and UEFI booting:

build/isodir/
├── boot/
│   ├── aegis-stripped.elf  ← Kernel binary (stripped; full ELF kept for make sym)
│   ├── rootfs.img          ← ext2 root filesystem (Multiboot2 module ramdisk0)
│   ├── esp.img             ← EFI System Partition (Multiboot2 module ramdisk1)
│   └── limine/
│       ├── limine.conf         ← Generated by tools/gen-limine-conf.sh
│       ├── limine-bios.sys
│       ├── limine-bios-cd.bin  ← BIOS El Torito boot image
│       └── limine-uefi-cd.bin  ← UEFI El Torito boot image
└── EFI/BOOT/
    ├── BOOTX64.EFI    ← Limine UEFI binary
    └── BOOTIA32.EFI   ← Limine UEFI-IA32 binary

Limine Configuration

A single script, tools/gen-limine-conf.sh, generates limine.conf for all four build modes:

Mode Command Kernel Arguments Description
live make iso boot=graphical quiet Full GUI: Bastion greeter + Lumen desktop
test make test-iso boot=text quiet Text-mode, timeout 0 (for automated tests)
installer-test make installer-test-iso boot=graphical quiet bastion_autologin=root GUI with autologin for GUI installer tests
installed (written to ESP) boot=graphical quiet Installed system boot entry (reads kernel from FAT ESP)

The kernel is always loaded via Multiboot2. Live ISOs pass two modules (rootfs.img + esp.img). Installed system entries pass no modules — the kernel mounts the ext2 root from NVMe itself. No gfxmode directive is used: Limine always provides a 32bpp linear framebuffer via the Multiboot2 framebuffer tag.

Nuclear clean before every ISO build:

git clean -fdx --exclude=references --exclude=.worktrees
make clean
make iso

git clean -fdx removes untracked files; make clean removes build/. Both are required. Never trust an incremental build for ISO creation.

EFI System Partition

The ESP image (build/esp.img) is a FAT32 filesystem containing:

  • EFI/BOOT/BOOTX64.EFI – Limine UEFI-x64 binary (vendored)
  • EFI/BOOT/BOOTIA32.EFI – Limine UEFI-IA32 binary (vendored)
  • boot/aegis-stripped.elf – Stripped kernel (for installed-system boot)
  • limine.conf – Installed-system boot configuration

This ESP is embedded in the ISO and written to the NVMe disk during installation for UEFI boot. The installed system boots the kernel from the FAT ESP because Limine’s built-in ext2 driver cannot read Aegis’s 1 KiB-block rootfs.

Disk Image

The disk image is a GPT-partitioned raw image for NVMe testing:

make disk
┌─────────────────────────────────────┐
│ GPT Header                          │
├─────────────────────────────────────┤ Sector 34
│ Partition 1: aegis-root             │
│ Type: A3618F24-0C76-4B3D-0001-...   │
│ Sectors 34–122879                   │
│ Contains: ext2 rootfs               │
├─────────────────────────────────────┤ Sector 122880
│ Partition 2: aegis-swap             │
│ Type: A3618F24-0C76-4B3D-0002-...   │
│ Remaining space                     │
├─────────────────────────────────────┤
│ GPT Footer                          │
└─────────────────────────────────────┘

The rootfs partition uses a custom Aegis-specific GPT type GUID. The ext2 rootfs image is dd‘d into partition 1 at sector offset 2048.

Build Dependencies Graph

                    ┌─────────────┐
                    │  make iso   │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌──────────┐ ┌──────────┐ ┌──────────┐
        │aegis.elf │ │rootfs.img│ │ esp.img  │
        └────┬─────┘ └────┬─────┘ └──────────┘
             │            │
     ┌───────┼───────┐    │
     ▼       ▼       ▼    ▼
  ┌──────┐┌──────┐┌────┐┌──────────────────────┐
  │Kernel││ ASM  ││Rust││  rootfs.manifest     │
  │  .c  ││stubs ││cap ││  + rootfs/ skeleton  │
  │files ││      ││lib ││  + all user programs │
  └──────┘└──────┘└────┘└──────────┬───────────┘
                                   │
                          ┌────────┼────────┐
                          ▼        ▼        ▼
                     ┌────────┐┌──────┐┌────────┐
                     │ Simple ││ Libs ││External│
                     │ progs  ││glyph ││ curl   │
                     │(musl)  ││auth  ││bearssl │
                     └───┬────┘└──┬───┘└────────┘
                         │        │
                         ▼        ▼
                    ┌──────────────────┐
                    │   musl libc      │
                    │ (build-musl.sh)  │
                    └──────────────────┘

Adding a New Userspace Program

  1. Create user/bin/<name>/ with a Makefile and source files
  2. If the program has no extra library dependencies, add it to SIMPLE_USER_PROGS in the top-level Makefile
  3. If it has library dependencies, add an explicit rule (see lumen or bastion as examples)
  4. Add the output binary to rootfs.manifest:
    user/bin/<name>/<name>.elf    /bin/<name>
    
  5. Run make iso – the manifest-driven rootfs builder handles the rest