A minimal bare-metal OS for AArch64 , built with pure Clang + LLD .
Designed to run on Termux (Android) — no sudo, no Linux PC needed.
crix/
├── arch/
│ └── vectors.S AArch64 exception vector table (EL1 SPx + EL0)
├── boot/
│ ├── boot.c UEFI bootloader → boota64.efi (PE32+ AArch64)
│ └── efi.h Hand-rolled UEFI types (no gnu-efi dependency)
├── fs/
│ ├── fat32.c FAT32 driver (read + write, mounted at /mnt)
│ ├── vfs.c Virtual filesystem (ramfs + FAT32 mount points)
│ └── virtio_blk.c VirtIO block device driver (QEMU virt)
├── include/
│ ├── dynlink.h ELF64 dynamic linking types, AArch64 relocs, API
│ ├── elf_loader.h ELF64 loader types and result struct
│ ├── mm.h PMM / VMM / slab types and function declarations
│ ├── string.h Kernel string, UART, and formatting declarations
│ └── vfs.h VFS types, inodes, file handles, and declarations
├── lib/
│ └── string.c String utils, memory ops, UART driver, fmt helpers
├── mm/
│ ├── pmm.c Buddy physical memory manager (~509 MiB free)
│ ├── slab.c Slab allocator (9 size classes: 8–2048 bytes)
│ └── vmm.c Page tables + MMU (identity map + higher-half)
├── user/
│ ├── lib/
│ │ └── libmath.c Shared library — 9 exported math/string functions
│ ├── cmdd.c EL0 command daemon — primary user-space shell
│ ├── hello.c Minimal EL0 demo (writes + exits)
│ ├── testso.c PIE binary exercising libmath.so via dynamic linking
│ └── user.ld User linker script (0x41000000, two PT_LOAD segs)
├── dynlink.c Kernel-side ELF dynamic linker (so_load/activate/patch)
├── elf_loader.c ELF64 loader: ET_EXEC + ET_DYN (PIE) support
├── kernel.c Kernel init, scheduler, syscall dispatch (30 calls)
├── kernel.ld Kernel linker script (kernel_main at 0x40200000)
└── Makefile
OVMF (UEFI) → boota64.efi → loads KXCZ at 0x40200000 → kernel_main()
→ SCTLR / VBAR / GIC / timer init
→ PMM (buddy) → VMM (MMU) → slab → VirtIO → FAT32 → VFS
→ scheduler init → task 0 ("kernel") starts
→ shell_run() → exec /mnt/bin/cmdd [EL0 ERET]
→ cmdd _start() → readline loop → SVC #0 for all kernel services
x8
Name
Args (x0–x5)
Return (x0)
1
SYS_WRITE
fd, buf, len
bytes written
2
SYS_EXIT
code
—
3
SYS_READ
fd, buf, len
char
4
SYS_GETPID
—
pid
7
SYS_UPTIME
—
ms since boot
9
SYS_EXEC
path, pathlen
0 / -1
10
SYS_GETCWD
buf, bufsz
bytes written
11
SYS_CHDIR
path, pathlen
0 / -1
12
SYS_LISTDIR
path,plen,idx,nbuf,nsz,tp
namelen / -1
13
SYS_SPAWN
type (0=counter,1=finite)
task_id / -1
14
SYS_KILL
task_id
0 / -1
15
SYS_MKDIR
path, pathlen
0 / -1
16
SYS_REMOVE
path, pathlen
0 / -1
17
SYS_WRITEFILE
path,plen,data,dlen
0 / -1
18
SYS_MEMINFO
buf, bufsz
bytes written
19
SYS_TASKS
buf, bufsz
bytes written
20
SYS_SYSINFO
buf, bufsz
bytes written
21
SYS_STAT
path,plen,buf,bufsz
bytes / -1
22
SYS_READFILE
path,plen,buf,bufsz
bytes / -1
23
SYS_HEXDUMP
path,plen,buf,bufsz
bytes / -1
24
SYS_REBOOT
—
—
25
SYS_POWEROFF
—
—
27
SYS_MOUNT
buf, bufsz
bytes written
28
SYS_PMM
buf, bufsz
bytes written
29
SYS_SLAB
buf, bufsz
bytes written
30
SYS_MEMMAP
buf, bufsz
bytes written
pkg update -y && pkg upgrade -y
pkg install -y clang lld llvm mtools parted qemu-system-aarch64-headless
unzip crix.zip && cd crix
make # build everything
make run # launch in QEMU
Expected build output:
[OK] OVMF : /data/data/com.termux/files/usr/share/qemu/edk2-aarch64-code.fd
[BOOT] boota64.efi
[AS] arch/vectors.o
[CC] lib/string.o
[CC] kernel.o
[CC] elf_loader.o
[CC] mm/pmm.o mm/vmm.o mm/slab.o
[CC] fs/virtio_blk.o fs/fat32.o fs/vfs.o
[LD] kernel.elf
[BIN] kxcz
[CMDD] cmdd.elf
[USER] hello.elf
[IMG] crix.img
✓ Build complete → crix.img
ESP path
Contents
/EFI/BOOT/BOOTAA64.EFI
UEFI bootloader
/KXCZ
Kernel flat binary
/bin/cmdd
Command daemon (primary shell)
/bin/hello
Minimal EL0 demo
/lib/libmath.so
Shared library (ET_DYN)
/bin/testso
PIE test binary (ET_DYN)
Target
Description
make
Build everything → crix.img
make run
Build + launch QEMU
make run-debug
Launch QEMU paused, GDB on :1234
make inspect
Show symbols / section headers
make inspect-so
Detailed .so / PIE relocation dump
make clean
Remove all build artifacts
Address range
Region
0x40000000–0x401FFFFF
UEFI / bootloader
0x40200000–0x402FFFFF
Kernel image (kxcz)
0x40300000–0x5FFFFFFF
PMM free pool (~509 MiB)
0x41000000–0x411FFFFF
User code (cmdd text+rodata)
0x41200000–0x413FFFFF
User data+BSS (cmdd RW)
0x41400000
User stack top (SP_EL0)
0x43000000–0x431FFFFF
Shared library window (SO)
0xFFFFFF8040200000+
Kernel higher-half (TTBR1)
Shared object (.so) support — v0.7.0
Crix OS now includes a kernel-side ELF dynamic linker that can load
position-independent shared libraries (ET_DYN) and link them into a PIE
test binary at runtime. No ld.so is needed in user space — the kernel
does all the work before ERET to EL0.
File
Role
include/dynlink.h
ELF64 structures, AArch64 reloc constants, API
dynlink.c
so_load, so_activate, so_resolve_sym, so_patch_got
user/lib/libmath.c
Shared math library (9 exported functions)
user/testso.c
PIE test binary — exercises all libmath functions
ESP path
Contents
/lib/libmath.so
Shared library (-fPIC -shared)
/bin/testso
PIE test binary (-pie)
kernel_main / task_spawn_user("testso"):
1. fat32_read("/lib/libmath.so") → so_load()
Parses ET_DYN ELF, allocates phys pages, copies PT_LOAD segments,
applies RELATIVE relocations, extracts dynsym/dynstr.
2. fat32_read("/bin/testso") → elf_load() (ET_DYN path)
Rebases PIE at USER_BASE_VA, fills L3 page tables.
3. so_patch_got(so, exec_elf, exec_pages_pa, USER_BASE_VA)
Walks the exec's RELA.DYN + RELA.PLT; resolves each
GLOB_DAT / JUMP_SLOT / ABS64 symbol against libmath.so's
dynsym table; patches the GOT/PLT entries in physical memory.
4. so_activate(so, L2_table)
Wires the SO's L3 into the user page table at L2[24]
(VA 0x43000000, 2 MiB window).
5. elf_activate() → ERET to EL0 entry point.
AArch64 relocation types handled
Constant
Value
Action
R_AARCH64_RELATIVE
1027
*slot = base + addend
R_AARCH64_GLOB_DAT
1025
*slot = sym_va + addend
R_AARCH64_JUMP_SLOT
1026
*slot = sym_va
R_AARCH64_ABS64
257
*slot = sym_va + addend
task_spawn_user() in kernel.c must be updated to call the dynamic
linker when executing testso. Minimal integration:
#include "include/dynlink.h"
#include "include/elf_loader.h"
// 1. Load the shared library
SharedLib so ;
uint8_t * so_buf = fat32_read ("/lib/libmath.so" , & so_sz );
so_load (so_buf , so_sz , & so );
// 2. Load the PIE binary
uint8_t * elf_buf = fat32_read ("/bin/testso" , & elf_sz );
ElfLoadResult res = elf_load (elf_buf , elf_sz );
// 3. Patch GOT (resolves all JUMP_SLOT / GLOB_DAT entries)
so_patch_got (& so , (Elf64_Ehdr * )elf_buf , res .pages_pa , USER_BASE_VA );
// 4. Map the .so into the user address space
extern uint64_t * user_l2 ; // L2 table for TTBR0
so_activate (& so , user_l2 );
// 5. Map the executable and ERET
elf_activate (& res );
Verify on host (before running in QEMU)
make inspect-so # dump symbols and relocations
llvm-readelf -l testso.elf | grep LOAD # confirm ET_DYN
llvm-nm -D libmath.so | grep " T " # 9 exported symbols
llvm-readelf -r testso.elf | grep JUMP # PLT slots to patch