commit f415cd6cd9859e7ec49c82cf5ac4c368b073d242
parent eab8230948b78b2c4f18922b1ad5956a72e2c392
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 14:53:51 -0700
test: per-arch none-elf kernel-link fixtures + byte-golden map/sym + negatives
Extend test/buildcmds/run.sh (test-driver-build) with the remaining
KERNEL.md "Tests and fixtures" coverage:
- Per-arch host-side end-to-end kernel-link fixtures for all four
first-pass freestanding targets (x86_64 / aarch64 / riscv64 / riscv32
none-elf): compile a minimal C + asm kernel set via build-exe
(-ffreestanding -nostdlib -static -no-pie -T kernel.ld -e _start, plus
-mno-red-zone for x86_64), emit --map / --symbols, convert to a flat
binary with kit image --format bin, and assert link success, non-empty
image, byte-identical reproducibility (ELF + map + sym + image across
two runs), and freestanding strict validation (--no-dynamic
--require-entry). No qemu/emulator required.
- Byte-golden --map / --symbols for a fully-deterministic aarch64-none-elf
kernel link (addresses pinned via the kernel.ld text base; goldens carry
no host paths). Committed under test/buildcmds/golden/, diffed with
same_file. Regenerate with KIT_REGEN_GOLDEN=1.
- Negative tests: region overflow (a MEMORY region too small for its
sections is rejected with 'overflows MEMORY region', via build-exe and
ld) and discarded sections (a /DISCARD/-routed section and a
--gc-sections-collected function-section both appear in the map's
discarded list).
All 225 build-driver cases pass.
Diffstat:
3 files changed, 239 insertions(+), 0 deletions(-)
diff --git a/test/buildcmds/golden/aarch64_kernel.map b/test/buildcmds/golden/aarch64_kernel.map
@@ -0,0 +1,38 @@
+target aarch64-elf
+output exe
+entry _start 0x80000000
+
+segments
+ [1] r-x off=0x4000 vaddr=0x80000000 filesz=0x60 memsz=0x60 align=0x4
+ [2] r-- off=0x8000 vaddr=0x80000060 filesz=0x4 memsz=0x4 align=0x4
+ [3] rw- off=0xc000 vaddr=0x80000064 filesz=0x4 memsz=0x4 align=0x4
+ [4] rw- off=0x10000 vaddr=0x80000068 filesz=0x0 memsz=0x4 align=0x4
+
+sections
+ [1] .text seg=1 off=0x4000 vaddr=0x80000000 lma=0x80000000 size=0x4 align=0x1
+ [2] .text seg=1 off=0x4004 vaddr=0x80000004 lma=0x80000004 size=0x5c align=0x4
+ [3] .rodata seg=2 off=0x8000 vaddr=0x80000060 lma=0x80000060 size=0x4 align=0x4
+ [4] .data seg=3 off=0xc000 vaddr=0x80000064 lma=0x80000064 size=0x4 align=0x4
+ [5] .bss seg=4 off=0x10000 vaddr=0x80000068 lma=0x80000068 size=0x4 align=0x4
+
+discarded
+
+unresolved
+
+symbols
+0000000000000000 N _GLOBAL_OFFSET_TABLE_
+0000000000000000 N __dso_handle
+0000000000000000 N __fini_array_end
+0000000000000000 N __fini_array_start
+0000000000000000 N __init_array_end
+0000000000000000 N __init_array_start
+0000000000000000 N __preinit_array_end
+0000000000000000 N __preinit_array_start
+0000000000000000 A __tbss_size
+0000000000000000 N __tdata_end
+0000000000000000 N __tdata_start
+0000000080000000 T _start
+0000000080000004 T kernel_main
+0000000080000060 R kernel_ro
+0000000080000064 D kernel_var
+0000000080000068 B kernel_bss
diff --git a/test/buildcmds/golden/aarch64_kernel.sym b/test/buildcmds/golden/aarch64_kernel.sym
@@ -0,0 +1,16 @@
+0000000000000000 N _GLOBAL_OFFSET_TABLE_
+0000000000000000 N __dso_handle
+0000000000000000 N __fini_array_end
+0000000000000000 N __fini_array_start
+0000000000000000 N __init_array_end
+0000000000000000 N __init_array_start
+0000000000000000 N __preinit_array_end
+0000000000000000 N __preinit_array_start
+0000000000000000 A __tbss_size
+0000000000000000 N __tdata_end
+0000000000000000 N __tdata_start
+0000000080000000 T _start
+0000000080000004 T kernel_main
+0000000080000060 R kernel_ro
+0000000080000064 D kernel_var
+0000000080000068 B kernel_bss
diff --git a/test/buildcmds/run.sh b/test/buildcmds/run.sh
@@ -478,5 +478,190 @@ fi
# A same-arch freestanding link still succeeds (no false positive).
run_ok fs-same-arch-ok "$KIT" ld -o fsok.elf fs_x64.o
+# ===========================================================================
+# Per-arch freestanding kernel-link fixtures (KERNEL.md acceptance criteria)
+# ===========================================================================
+#
+# A HOST-SIDE (no qemu/emulator) end-to-end kernel-link per first-pass
+# freestanding target: x86_64 / aarch64 / riscv64 / riscv32 none-elf. Each
+# exercises the documented acceptance flow:
+# - compile a tiny C + asm kernel set with `build-exe -ffreestanding
+# -nostdlib -static -no-pie -T kernel.ld -e _start`,
+# - emit a deterministic `--map` and `--symbols` side file,
+# - convert the linked ELF to a flat binary with `kit image --format bin`,
+# - assert the link succeeds, the image is non-empty, the link is byte-for-byte
+# reproducible across two runs (ELF + map + symbols), and freestanding strict
+# validation accepts it (no dynamic artifacts: --no-dynamic + --require-entry).
+#
+# The kernel source per arch is minimal: an asm `_start` self-loop stub (fine to
+# never run), plus a C function touching .rodata / .data / .bss. The linker
+# script pins .text at a fixed VMA so addresses are deterministic.
+
+# Shared linker script: a fixed text base makes every emitted address stable.
+cat > kernel.ld <<'LDEOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x80000000;
+ .text : { *(.text*) }
+ .rodata : { *(.rodata*) }
+ .data : { *(.data*) }
+ .bss : { *(.bss*) }
+}
+LDEOF
+
+# Shared C kernel translation unit: one function plus .rodata / .data / .bss.
+cat > kernel_main.c <<'CEOF'
+int kernel_var = 42; /* .data */
+int kernel_bss; /* .bss */
+const int kernel_ro = 7; /* .rodata */
+int kernel_main(void) { return kernel_var + kernel_ro + kernel_bss; }
+CEOF
+
+# kernel_link ARCH ASMBODY [EXTRA_CFLAGS...] : build + map/symbols + image +
+# determinism + strict-validation for one none-elf target. The asm body is the
+# instruction(s) for an `_start` self-loop stub.
+kernel_link() {
+ _ka_arch=$1; _ka_asm=$2; shift 2
+ _ka_t="${_ka_arch}-none-elf"
+ printf '.globl _start\n.section .text\n_start:\n %s\n' "$_ka_asm" \
+ > "kstart_${_ka_arch}.s"
+
+ # build-exe: C + asm freestanding kernel link with a pinned script + entry.
+ run_ok "kern-${_ka_arch}-link" "$KIT" build-exe -target "$_ka_t" \
+ -ffreestanding -nostdlib -static -no-pie "$@" \
+ -T kernel.ld -e _start \
+ --map "kern_${_ka_arch}.map" --symbols "kern_${_ka_arch}.sym" \
+ "kstart_${_ka_arch}.s" kernel_main.c -o "kern_${_ka_arch}.elf"
+ assert_file_exists "kern-${_ka_arch}-elf" "kern_${_ka_arch}.elf"
+ assert_file_exists "kern-${_ka_arch}-map" "kern_${_ka_arch}.map"
+ assert_file_exists "kern-${_ka_arch}-sym" "kern_${_ka_arch}.sym"
+ # The pinned _start address shows up in both the map and the symbols file.
+ contains "kern-${_ka_arch}-map-entry" "kern_${_ka_arch}.map" \
+ "entry _start 0x80000000"
+ contains "kern-${_ka_arch}-sym-start" "kern_${_ka_arch}.sym" \
+ "0000000080000000 T _start"
+
+ # Flat binary image: succeeds and is non-empty.
+ run_ok "kern-${_ka_arch}-image" "$KIT" image --format bin \
+ "kern_${_ka_arch}.elf" -o "kern_${_ka_arch}.bin"
+ if [ -s "kern_${_ka_arch}.bin" ]; then ok "kern-${_ka_arch}-image-nonempty"
+ else not_ok "kern-${_ka_arch}-image-nonempty"; fi
+
+ # Freestanding strict validation accepts it: a static, entried, non-dynamic
+ # ELF passes --no-dynamic + --require-entry.
+ run_ok "kern-${_ka_arch}-no-dynamic" "$KIT" image --no-dynamic \
+ --require-entry --format bin "kern_${_ka_arch}.elf" \
+ -o "kern_${_ka_arch}.nd.bin"
+
+ # Reproducibility: a second identical link is byte-identical (ELF + map +
+ # symbols + image).
+ run_ok "kern-${_ka_arch}-link2" "$KIT" build-exe -target "$_ka_t" \
+ -ffreestanding -nostdlib -static -no-pie "$@" \
+ -T kernel.ld -e _start \
+ --map "kern_${_ka_arch}.2.map" --symbols "kern_${_ka_arch}.2.sym" \
+ "kstart_${_ka_arch}.s" kernel_main.c -o "kern_${_ka_arch}.2.elf"
+ run_ok "kern-${_ka_arch}-image2" "$KIT" image --format bin \
+ "kern_${_ka_arch}.2.elf" -o "kern_${_ka_arch}.2.bin"
+ same_file "kern-${_ka_arch}-elf-reproducible" \
+ "kern_${_ka_arch}.elf" "kern_${_ka_arch}.2.elf"
+ same_file "kern-${_ka_arch}-map-reproducible" \
+ "kern_${_ka_arch}.map" "kern_${_ka_arch}.2.map"
+ same_file "kern-${_ka_arch}-sym-reproducible" \
+ "kern_${_ka_arch}.sym" "kern_${_ka_arch}.2.sym"
+ same_file "kern-${_ka_arch}-image-reproducible" \
+ "kern_${_ka_arch}.bin" "kern_${_ka_arch}.2.bin"
+}
+
+# x86_64 also disables the SysV red zone (kernel codegen requirement).
+kernel_link x86_64 "jmp _start" -mno-red-zone
+kernel_link aarch64 "b _start"
+kernel_link riscv64 "j _start"
+kernel_link riscv32 "j _start"
+
+# ===========================================================================
+# Byte-golden --map / --symbols for a fully-deterministic kernel link
+# ===========================================================================
+#
+# The grep-assertions above (and in the kernel-flag section) prove fields are
+# present; this locks the *exact* emitted bytes against a checked-in golden so a
+# layout/format regression is caught. We pin one arch (aarch64-none-elf, the
+# reference backend) with the address-pinning kernel.ld above. The map/symbols
+# carry no host paths (the writer uses basenames; this fixture's links contribute
+# no input= lines at all), so a raw byte-golden is directory-independent.
+golden_dir="$repo_root/test/buildcmds/golden"
+run_ok be-golden-link "$KIT" build-exe -target aarch64-none-elf \
+ -ffreestanding -nostdlib -static -no-pie \
+ -T kernel.ld -e _start \
+ --map golden.map --symbols golden.sym \
+ kstart_aarch64.s kernel_main.c -o golden.elf
+if [ "${KIT_REGEN_GOLDEN:-0}" = "1" ]; then
+ # Opt-in regeneration: refresh the committed goldens from this run.
+ mkdir -p "$golden_dir"
+ cp golden.map "$golden_dir/aarch64_kernel.map"
+ cp golden.sym "$golden_dir/aarch64_kernel.sym"
+ echo "regenerated goldens in $golden_dir" >&2
+fi
+same_file be-golden-map "$golden_dir/aarch64_kernel.map" golden.map
+same_file be-golden-sym "$golden_dir/aarch64_kernel.sym" golden.sym
+
+# ===========================================================================
+# Negative tests: region overflow + discarded-section map reporting
+# ===========================================================================
+
+# ---- region overflow: a MEMORY region too small for its sections is rejected.
+cat > overflow.ld <<'LDEOF'
+ENTRY(_start)
+MEMORY {
+ TINY (rwx) : ORIGIN = 0x80000000, LENGTH = 8
+}
+SECTIONS {
+ .text : { *(.text*) } > TINY
+ .data : { *(.data*) } > TINY
+}
+LDEOF
+run_fail be-region-overflow "$KIT" build-exe -target aarch64-none-elf \
+ -ffreestanding -nostdlib -static -no-pie -T overflow.ld -e _start \
+ kstart_aarch64.s kernel_main.c -o overflow.elf
+contains be-region-overflow-diag "$work/be-region-overflow.err" \
+ "overflows MEMORY region"
+# Via ld too: the same script over a precompiled object is rejected.
+run_ok be-region-overflow-obj "$KIT" build-obj -target aarch64-none-elf \
+ -ffreestanding -nostdlib kernel_main.c -o ovf_main.o
+run_ok be-region-overflow-start "$KIT" build-obj -target aarch64-none-elf \
+ -ffreestanding -nostdlib kstart_aarch64.s -o ovf_start.o
+run_fail ld-region-overflow "$KIT" ld -nostdlib -static -no-pie \
+ -T overflow.ld ovf_start.o ovf_main.o -o ld-overflow.elf
+contains ld-region-overflow-diag "$work/ld-region-overflow.err" \
+ "overflows MEMORY region"
+
+# ---- discarded sections: a /DISCARD/-removed section appears in the map's
+# discarded list (positive assertion on the discarded-sections map field).
+cat > discard.ld <<'LDEOF'
+ENTRY(_start)
+SECTIONS {
+ . = 0x80000000;
+ .text : { *(.text*) }
+ .data : { *(.data*) }
+ /DISCARD/ : { *(.rodata*) }
+}
+LDEOF
+run_ok be-discard "$KIT" build-exe -target aarch64-none-elf \
+ -ffreestanding -nostdlib -static -no-pie -T discard.ld -e _start \
+ --map discard.map kstart_aarch64.s kernel_main.c -o discard.elf
+# The map's discarded list (between the `discarded` and `unresolved` headers)
+# names .rodata, the section routed into /DISCARD/.
+awk '/^discarded/{f=1;next} /^unresolved/{f=0} f' discard.map > discard.list
+contains be-discard-rodata discard.list ".rodata"
+
+# ---- discarded via --gc-sections: a GC-dropped function-section shows up in
+# the discarded list. kernel_main is unreferenced from _start (the asm stub just
+# self-loops), so with -ffunction-sections + --gc-sections it is collected.
+run_ok be-gc-discard "$KIT" build-exe -target aarch64-none-elf \
+ -ffreestanding -nostdlib -static -no-pie --gc-sections \
+ -ffunction-sections -fdata-sections -T kernel.ld -e _start \
+ --map gc.map kstart_aarch64.s kernel_main.c -o gc.elf
+awk '/^discarded/{f=1;next} /^unresolved/{f=0} f' gc.map > gc.list
+contains be-gc-discard-fn gc.list ".text.kernel_main"
+
kit_summary build-driver
kit_exit