Kernel build and image pipeline
kit can build freestanding kernels from C and assembly and turn the linked
result into the flat load images and initramfs archives a bare-metal or
QEMU-driven boot needs. This is a thin policy layer on top of the existing
toolchain: the same compile, link, and object machinery used for hosted
programs, plus three things a kernel author specifically needs — a richer
linker-script subset, an image emitter (kit image / objcopy -O binary), and
a cpio packager (kit cpio).
kit deliberately stops at the artifact boundary. It does not generate startup code, set up page tables, stacks, TLS, or privilege-mode entry, and it does not implement any boot protocol. Kernel startup and boot-protocol compliance are the author's responsibility; kit produces a deterministic ELF, a byte-exact flat image, and a well-formed archive, and nothing about how they are loaded or run.
Related: DRIVER.md (the multi-call binary and tool registry),
LINK.md (symbol resolution, layout, relocation, linker scripts),
OBJ.md (the object/image model the emitter reads), and
RUNTIME.md (the freestanding headers, compiler-rt helpers, and the
TLS contract — note there is no crt0).
Freestanding targets and the build shape
The supported freestanding ELF targets are:
x86_64-none-elfaarch64-none-elfriscv64-none-elfriscv32-none-elf
A *-none-elf triple (EI_OSABI STANDALONE) resolves to a non-PIE ELF target by
default and puts the driver and linker into freestanding mode, which turns on
the strict validation described below.
There is no separate kit kernel command. The compile/link front doors are the
ordinary build verbs (see DRIVER.md):
build-objcompiles a source set (C / asm / wasm) to one object; multiple sources combine into a single relocatable object viald -r.build-execompiles a source set in memory and links it — together with any.o/.ainputs — into an executable, with no intermediate files.
Both accept the freestanding and link flags a kernel needs, and build-exe
accepts the common direct linker flags (not only the -Wl, escape hatch). A
representative kernel link:
kit build-exe -target x86_64-none-elf \
-ffreestanding -nostdlib -nostartfiles -static -no-pie \
-mcmodel=kernel -mno-red-zone \
-ffunction-sections -fdata-sections \
-T kernel.ld -e _start \
-Wl,--gc-sections \
--map kernel.map --symbols kernel.sym \
-o kernel.elf \
boot.S kernel.c mm.c
The flags that matter for kernel code, and that kit honors rather than merely accepts:
-ffreestanding/-fhostedselect freestanding vs hosted assumptions (whether sysroot-hosted profiles may engage).-nostdinc,-nostdlib,-nodefaultlibs,-nostartfilesprecisely control include paths and runtime/CRT/libc insertion. kit never invents a startup object for a freestanding kernel.-static,-no-pie,-fno-pic,-fno-pieproduce static non-PIE, ET_EXEC-style code unless PIC/PIE is requested explicitly.-mcmodel=...selects the code model.-mno-red-zonedisables the x86-64 SysV red zone — it changes backend frame selection, it is not just parsed.-mgeneral-regs-onlyprevents accidental SIMD/FP codegen where the privilege-mode context does not save those registers.-fno-builtinand the stack-protector flags constrain what the frontend may synthesize.-ffunction-sections/-fdata-sectionscompose with--gc-sectionsand scriptKEEP(...).
-Ttext / -Tdata / -Tbss and --section-start=.name=addr place a
freestanding image at a fixed load address from the command line, and work
through both build-exe and ld.
Strict freestanding validation
A freestanding executable link (triggered by a *-none-elf input, or forced by
the same freestanding_strict trigger from either build-exe or ld) rejects,
by default, anything that implies a dynamic loader: DSO inputs, dynamic
interpreter paths, dynamic sections and PLT/GOT imports, unresolved symbols, and
cross-input target / object-format mismatches. --no-undefined makes unresolved
references an error explicitly; --allow-undefined is the escape hatch.
Linker-script subset
Kernel layouts need a structured GNU-ld-compatible script subset larger than the
default SECTIONS-only form. The script is parsed by a hand-written
recursive-descent parser (src/link/link_script.c) into a structured
KitLinkScript; the linker accepts only that structured form, and ld parses
-T text into it. Unsupported directives are rejected with a diagnostic rather
than silently ignored. See LINK.md for how scripted layout replaces
the default permission-bucket placement.
The supported constructs:
MEMORYwithORIGIN/LENGTHand attributes, with region-overflow diagnostics, and output-section placement into regions.- Load-memory placement via
AT(expr)/AT> REGION, so VMA/LMA-split kernels are representable. PHDRS, the:phdrsection attribute, and segment flags, so a kernel controls its program headers and segment permissions. Multiple sections naming one:phdrcoalesce into a single PT_LOAD (perms unioned); a section listing several phdrs appears under each.PROVIDE,PROVIDE_HIDDEN, andHIDDENsymbol definitions; top-level and in-section assignments;. = exprdot moves, which apply at their textual position interleaved with the section walk.ASSERT(expr, "message").EXTERN(symbol)as a GC root / undefined-symbol declaration.- Input-section patterns including
EXCLUDE_FILEand the alignment helpersALIGN/BLOCK. - Section fills (
=0x...andFILL(...)), which lay a repeating big-endian pattern across holes. NOLOADoutput sections: PROGBITS content is forced to occupy no file bytes, and relocations into a NOLOAD section are skipped rather than written through a null buffer.OUTPUT_ARCH/OUTPUT_FORMATas validation directives (mismatches are reported; they do not drive target selection).
KEEP(...) roots continue to interact correctly with --gc-sections.
Link-side side outputs
The linker produces deterministic side files, available from both ld and
build-exe, so a kernel build can audit its own layout:
--map FILEwrites a link map: target triple and output kind, entry symbol and address, memory regions and usage, program headers, output sections with VMA / LMA / file offset / size / alignment, input contributions, linker-defined symbols, discarded sections, and unresolved symbols. Input paths are normalized to basenames so no absolute host path leaks in.--symbols FILEwrites post-link absolute symbols;--symbols-format=nm|jsonselects the format.--cref FILEwrites a cross-reference table (including imported/undefined symbols).--print-memory-usagesummarizes per-MEMORY-region usage (overflow-safe for high-half regions).
These are linker-owned, not image-owned: the image metadata sidecar (below) is a
report about the image transform, not a substitute for --map. Supporting
policy flags include --defsym name=expr (can satisfy an otherwise-undefined
reference), --orphan-handling=place|warn|error|discard, and --fatal-warnings.
Image emission
Two front doors turn a linked ELF into a flat load image, both backed by one
lower-level emitter (include/kit/image.h, implemented in src/obj/image.c):
objcopy -O binarystays a minimal object-transformer path:kit objcopy -O binary kernel.elf kernel.binkit imageowns image-building policy:kit image --format bin kernel.elf -o kernel.bin
The emitter consumes already-opened object/image state plus the original input bytes (see OBJ.md for the linked-image view it reads). It never reads the filesystem itself, chooses a boot protocol, or invokes an emulator.
Formats
bin— flat binary derived from loadable segments (the default) or selected sections.rom— fixed-size flat binary: requires--pad-to, fills unused bytes deterministically, and fails when the payload exceeds the requested size.sections— concatenate--section NAME(repeatable) in declared order; missing sections are rejected and the address metadata is made explicit so a concatenation is not mistaken for a loadable memory image.elf— normalize / strip / split-debug. This is deliberately delegated toobjcopy+strip; the emitter returns a diagnostic pointing at those tools rather than duplicating the object-rewrite machinery. The--strip-debug/--split-debug/--keep-symbolsknobs are only meaningful with--format elf.
Segment-based emission reads loadable ranges from the program headers, sorts by the selected address kind, detects overlaps, fills or rejects holes per policy, and preserves bytes exactly as they would be loaded. The main flag groups:
- Selection:
--from segments|sections,--segment PT_LOAD(repeatable),--only-section/--remove-section/--section(all repeatable). - Addressing:
--addr vaddr|paddr|lma,--base ADDR,--bias N. - Layout:
--fill BYTE,--fail-on-holes,--max-hole SIZE,--align,--pad-to,--max-size(sizes acceptK/M/Gsuffixes). - Validation:
--require-entry,--require-symbol NAME,--require-section NAME,--no-dynamic— checked against the opened object before any bytes are written. - Reporting:
--metadata FILEwrites a deterministic JSON sidecar (stable key order, no host paths, no timestamps) describing target, object format, entry, build id, the selection that actually contributed bytes, base/bias/fill policy, payload/output sizes, and warnings.
Flat-kernel Image header (arm64 / riscv64)
QEMU's -kernel path on arm64 and riscv consumes the flat Linux Image format:
a raw loadable binary prefixed with a fixed 64-byte header the loader reads to
place and size the image. The first 32 bytes are common to both arches: code0
/ code1 (the entry branch; "MZ" low half when EFI), a u64 text_offset
(load offset from a 2 MiB-aligned base), a u64 image_size (the in-memory
footprint including BSS), and a u64 flags (bit 0 endianness; on arm64 bits
1-2 page size, bit 3 placement). The tails differ: arm64 carries magic = "ARM\x64" at offset 56; riscv64 carries a version u32 (currently 0x2) at 32
and magic2 = "RSC\x05" at 56.
kit supports this two ways:
Author-owned, pass-through. A kernel's own
head.Scan emit the 64-byte header (placingcode0/code1and declaringimage_sizefrom linker symbols).kit image --format bin/objcopy -O binarycopy the PT_LOAD bytes verbatim, so the leading header survives byte-exact and the result is a loadableImage. kit needs nothing extra for this path.kit-synthesized. The awkward field for authors is
image_size, the in-memory span including BSS, which kit already knows from each loadable segment'svsize. So--image-header[=arm64|riscv|auto](on--format binorrom;autoinfers the arch from the object's machine) synthesizes the header. It overlays the first segment rather than prepending: the author's first 8 bytes (thecode0/code1entry branch) are preserved and kit fills the 56-byte metadata tail, so the output stays the same size as a plainbin. The author's first segment must reserve those 56 bytes.image_sizeis the in-memory span including BSS, computed fromvsizeover all selected loadable segments (BSS-onlyfile_size == 0segments count toward the span but contribute no bytes).
Because flags encodes boot semantics, header synthesis is an explicit opt-in,
never a default, and the boot-semantic fields are surfaced as explicit options
(--image-text-offset, --image-endian, --image-page-size [arm64 only,
4k/16k/64k]) rather than invented — they error if given without
--image-header. kit fills the magic/version and image_size deterministically
and chooses no boot policy.
kit cpio — initramfs archives
The Linux kernel unpacks its initramfs from a cpio -H newc archive (magic
070701, or 070702 for the CRC variant), optionally compressed; the
early-microcode convention is just an uncompressed cpio concatenated ahead of
the compressed main archive. This is an archive format, not a boot protocol — a
sibling of ar — so kit cpio lives in the byte-utility tool family rather than
in the image emitter. The newc codec is driver-local (driver/cmd/cpio.c,
mirroring tar.c's stateless shape) since only this one tool consumes it; the
driver has no -Isrc. kit packages and inspects the archive; it does not build,
mount, or boot it.
kit cpio -o -F initramfs.cpio -z init etc/ # create, gzip-compressed
kit cpio -t -F initramfs.cpio.gz # list (auto-detects gzip)
kit cpio -i -F initramfs.cpio # extract
What it supports:
- SVR4
newconly (-H newcdefault,-H crcfor the070702checksum variant). The legacybin/odcformats are out of scope. - Create (
-o), list (-t), and extract (-i) over regular files, directories, and symlinks. Special/device nodes are out of scope. - Deterministic output: members sorted by archived path (a stable sort that
is a valid DFS, each directory before its children), normalized metadata
(uid/gid 0, mtime 0, sequential inode, mode = type | perms keyed on the source
executable bit), a closing
TRAILER!!!record, and a 512-byte tail pad. Identical inputs yield byte-identical output. - Concatenation: the reader continues past a
TRAILER!!!, skips inter-segment zero padding, and resumes on the next magic, so early-init segments (built bycatof 512-padded archives) list and extract cleanly; trailing non-cpio data is noted, not fatal. - Compression as a create-time flag (
--compress=gzip|lz4, with short-zfor gzip and--lz4), via the publickit/compress.hcodecs. An initramfs is just a compressed newc archive, so there is no separateinitramfstool. gzip and lz4 only; a zstd/xz magic or--compress=zstd|xzgets a specific diagnostic rather than pretend support. On read,-dand always-on auto-detection round-trip a compressed archive with no separate step. - Safety: absolute names and
..components are refused on both create and extract, so a crafted archive cannot escape the destination directory.
The tool is gated in driver/main.c behind KIT_TOOL_CPIO_ENABLED, alongside
the other archive utilities.