Skip to content

Initrd and boot

miroslaw edited this page Sep 29, 2026 · 1 revision

Initrd and Boot

Boot sequence

  1. GRUB loads exyde-kernel.elf via Multiboot2 and jumps to _start.
  2. The assembly entry sets up a bootstrap identity map and calls kmain(magic, mb_info).
  3. kmain:
    • zeroes .bss, initialises the arch layer, checks Multiboot2 magic
    • parses the memory map, reserves low memory and the kernel image
    • initialises PMM, VMM, heap
    • reserves HEAP_BOOT_RESERVE in the heap
    • runs kernel self-tests (pmm, vmm, heap, handles)
    • initialises the scheduler, registers the timer IRQ, enables interrupts
    • runs sync, channel, ring-3 syscall, and userspace self-tests
    • enters the idle hlt loop

The userspace self-test is where init is spawned. See below.

The initrd

The initrd is a cpio newc archive (SVR4 without CRC, magic 070701). It contains:

bin/console
bin/echo
bin/exy-vfs
bin/test

The kernel does not parse it. It maps the archive read-only into init's address space at INITRD_VA = USER_VA_BASE + 0x02000000 and passes the base and size to init through environment variables:

EXYDE_INITRD_BASE=0x...
EXYDE_INITRD_SIZE=0x...

init reads these with getenv, validates the magic, and walks the archive with initrd_find("bin/<name>", &data, &size). It then hands the blob to SPAWN_ELF.

The cpio newc format

Each entry:

offset   size   field
0        6      "070701"
6        8      c_ino            (hex ASCII, 8 chars)
14       8      c_mode
22       8      c_uid
30       8      c_gid
38       8      c_nlink
46       8      c_mtime
54       8      c_filesize
62       8      c_devmajor
70       8      c_devminor
78       8      c_rdevmajor
86       8      c_rdevminor
94       8      c_namesize
102      8      c_chksum

Total header: 110 bytes.

Layout of one entry in the stream:

[header 110 B][name (namesize bytes, NUL-terminated)][pad to 4]
[file body (filesize bytes)][pad to 4]

The padding applies to the cumulative offset, not the length. Both data_off = align4(name_off + namesize) and next_off = align4(data_off + filesize) use the cumulative form. An earlier version of the parser used name_off + align4(namesize), which was off by two bytes for the first entry and made the kernel see an invalid ELF header. That bug surfaced as -ENOMEM from SPAWN_ELF.

The archive ends with an entry whose name is TRAILER!!!.

Building the archive

tools/mkinitrd.sh:

  1. Copies build/{test,echo,exy-vfs,console}.elf into build/initrd-stage/bin/<name>.
  2. Runs find . -type f -printf '%P\0' | LC_ALL=C sort -z | cpio --null -o -H newc > build/initrd.
  3. Removes the staging directory.

The sort -z is what makes the archive reproducible for a given set of inputs.

Embedding into the kernel

src/kernel/Makefile runs objcopy -I binary -O elf64-x86-64 on both build/init.elf and build/initrd, producing object files whose symbols are path-mangled:

build/init.elf  ->  _binary_build_init_elf_{start,end}
build/initrd    ->  _binary_build_initrd_{start,end}

include/exyde/init_elf.h and include/exyde/initrd.h expose these as exyde_init_elf / exyde_init_elf_size and exyde_initrd / exyde_initrd_size.

Spawning from the archive

Inside init.c:

static exyde_handle_t spawn_from_initrd(const char *path,
                                        const char *name,
                                        exyde_handle_t cap) {
    const void *data; size_t size;
    if (initrd_find(path, &data, &size) != 0)
        return EXYDE_HANDLE_INVALID;
    return exyde_spawn_elf(data, size, name, cap);
}

All service spawns go through this helper. The kernel never looks at the archive.

Clone this wiki locally