Build System
Aegis OS build system architecture: toolchain, Make targets, rootfs construction, and ISO packaging
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 (above0xFFFFFFFF80000000)-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.
Kernel Link Process
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().
Final Link Command
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.
Two-Pass Symbol-Table Link
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:
- Link once with weak, empty
ksym_*fallback arrays (kernel/core/ksym.c). tools/gen-ksyms.shrunsnm -non that image and emitsbuild/ksyms.c— sorted{address, name}arrays — which is then compiled.- 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
- Copy: The source ELF is copied to
build/blobs/<name>.bin - Convert:
objcopyconverts the raw binary into an ELF object with the data placed in.rodata(read-only) - 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 librarylib/ld-musl-x86_64.so.1– dynamic linker (symlink tolibc.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 automaticallychmod 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/*.ttfto/usr/share/fonts/ - Kernel binary: Copied to
/boot/aegis.elffor installed-system boot - CA certificates:
tools/cacert.peminstalled 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 -fdxremoves untracked files;make cleanremovesbuild/. 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
- Create
user/bin/<name>/with aMakefileand source files - If the program has no extra library dependencies, add it to
SIMPLE_USER_PROGSin the top-level Makefile - If it has library dependencies, add an explicit rule (see
lumenorbastionas examples) - Add the output binary to
rootfs.manifest:user/bin/<name>/<name>.elf /bin/<name> - Run
make iso– the manifest-driven rootfs builder handles the rest