The kernel-test machinery in tests/user-qemu.mk is x86-multiboot-
specific in several ways that don't generalise to aarch64. The
shortest path that mirrors x86's "boot through a real bootloader
the way users actually boot" philosophy is u-boot:
* tests/test-%.iso bundles the kernel + module into a GRUB-
bootable ISO via grub-mkrescue. GRUB's arm64-efi `linux`
command refuses plain-image kernels (requires
CONFIG_(U)EFI_STUB) which gnumach is not, so the multiboot ISO
path doesn't translate. Replace it on aarch64 with u-boot
running under QEMU: a FAT image bundles gnumach + the test
module + a per-test boot.scr that uses u-boot's `fdt mknod` to
inject the multiboot,module DTB nodes documented in
aarch64/BOOTING. Same convention real-hardware u-boot users
follow; gnumach reads it through the same
load_boot_modules_from_dtb() path as production.
* QEMU's per-arch command differs: x86 uses qemu-system-{i386,
x86_64} + -cdrom, aarch64 uses qemu-system-aarch64 + -M virt +
-bios u-boot.bin + -drive file=test.img,if=virtio. Factor the
boot-delivery flags out of run-qemu.sh.template as a new
QEMU_BOOT_ARGS sed substitution; user-qemu.mk sets it per
HOST_<arch> block. Behaviour for x86 is byte-identical (the
only template change is the `-cdrom <path>` literal being
replaced by the QEMU_BOOT_ARGS placeholder that resolves back
to `-cdrom <path>`).
* Two new per-test build rules feed the aarch64 image:
tests/boot-%.scr (the u-boot script, made by sed + mkimage)
and tests/test-%.img (the FAT image, made by mkfs.vfat +
mcopy). Module load address is fixed at 0x50000000
(RAM_BASE + 256 MB) to leave the single-segment vm_page heap
a usable 256 MB; multi-segment heap is a follow-up that would
lift this constraint and let users place modules anywhere.
* QEMU -m sizing: x86 uses -m 2047 (32-bit low-memory upper
bound). aarch64's pmap_bootstrap maps a single 1 GB L1 block
in TTBR1 so anything past 1 GB faults; cap at -m 512 until the
kernel grows multi-block mappings.
* The userland test binaries link against MIG-generated stubs
for the per-arch <mach/<arch>/mach_<arch>.defs> interface.
Add a HOST_aarch64 generate_mig_client(mach/aarch64,
mach_aarch64) call alongside the existing ix86/x86_64 ones,
and extend MIG_GEN_CC to pick up the right .user.c per host.
* The freestanding test runtime includes a per-arch memory-ops
file (memcpy/memset/memcmp). i386 has its hand-tuned asm
version; aarch64 has a plain-C one in aarch64/aarch64/
strings.c (already imported by the kernel-port series).
Factor as ARCH_STRINGS_C, selected by HOST_aarch64.
UBOOT_BIN is exported by the parent build system's Nix dev shell
for the aarch64 cross-target (points at pkgs.ubootQemuAarch64's
u-boot.bin). Linux-only — nixpkgs's u-boot derivation doesn't
build on darwin (Makefile already errors on darwin hosts).
tests/Makefrag.am drops tests/test-multiboot from the TESTS list
on aarch64 — that one is a grub-file --is-x86-multiboot validator
and doesn't apply to a kernel that boots via the arm64 image
header.
USER_TESTS list is unchanged; all 11 of those C sources are
arch-agnostic at the source level (they call Mach via the
kernel_trap() macro from <mach/syscall_sw.h>, which dispatches
per-arch automatically — mach/aarch64/syscall_sw.h was already
present in the existing aarch64/include/ tree).
---
tests/Makefrag.am | 13 ++++-
tests/run-qemu.sh.template | 2 +-
tests/uboot.script.template | 71 +++++++++++++++++++++++
tests/user-qemu.mk | 110 +++++++++++++++++++++++++++++++++---
4 files changed, 183 insertions(+), 13 deletions(-)
create mode 100644 tests/uboot.script.template
diff --git a/tests/Makefrag.am b/tests/Makefrag.am
index ef5616a6..d690adb8 100644
--- a/tests/Makefrag.am
+++ b/tests/Makefrag.am
@@ -30,13 +30,20 @@ export GNUMACH
include tests/user-qemu.mk
-TESTS += \
- tests/test-multiboot \
- $(USER_TESTS)
+# tests/test-multiboot is a host-side grub-file validator that checks
+# the produced ISO is multiboot1-spec-compliant. Multiboot1 is x86-
+# only by design, so the validator is meaningless on aarch64 (which
+# boots via the arm64 image header / linux+initrd through GRUB's
+# arm64-efi target instead).
+if !HOST_aarch64
+TESTS += tests/test-multiboot
+endif
+TESTS += $(USER_TESTS)
EXTRA_DIST += \
tests/README \
tests/grub.cfg.single.template \
+ tests/uboot.script.template \
tests/run-qemu.sh.template \
tests/include/syscalls.h \
tests/include/testlib.h \
diff --git a/tests/run-qemu.sh.template b/tests/run-qemu.sh.template
index a7ec426f..28c25936 100644
--- a/tests/run-qemu.sh.template
+++ b/tests/run-qemu.sh.template
@@ -17,7 +17,7 @@
set -e
-cmd="QEMU_BIN QEMU_OPTS -cdrom tests/test-TESTNAME.iso"
+cmd="QEMU_BIN QEMU_OPTS QEMU_BOOT_ARGS"
log="tests/test-TESTNAME.raw"
echo "temp log $log"
diff --git a/tests/uboot.script.template b/tests/uboot.script.template
new file mode 100644
index 00000000..fb4c3fc5
--- /dev/null
+++ b/tests/uboot.script.template
@@ -0,0 +1,71 @@
+# Copyright (C) 2026 Free Software Foundation
+#
+# This program is free software ; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation ; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY ; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with the program ; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+# u-boot script: boots gnumach with a single bootstrap module via
+# the multiboot,module DTB convention documented in aarch64/BOOTING.
+# Loaded by u-boot's distro_bootcmd from boot.scr on virtio0:1.
+#
+# This script mirrors what a real-hardware u-boot user would type
+# manually: load kernel + module from storage, hand-build the
+# multiboot,module child of /chosen in the DTB, hand off via booti.
+# Tests should fail the same way real hardware would if any step
+# regresses.
+
+# Kernel goes well above u-boot's own data area and the DTB.
+setenv kernel_addr 0x42000000
+
+# Load files from the boot device's FAT partition (virtio 0:1).
+load virtio 0:1 ${kernel_addr} gnumach
+load virtio 0:1 MODULE_ADDR module-TESTNAME
+
+# u-boot resolves the DTB load address into ${fdtcontroladdr} at start-
+# up. Reserve headroom for the new child node + properties.
+fdt addr ${fdtcontroladdr}
+fdt resize 1024
+
+# Multiboot,module convention from aarch64/BOOTING. Two-cell
+# address/size so the reg property covers a 64-bit module address.
+fdt set /chosen \#address-cells <2>
+fdt set /chosen \#size-cells <2>
+fdt mknod /chosen module@MODULE_ADDR
+fdt set /chosen/module@MODULE_ADDR compatible "multiboot,kernel"
"multiboot,module"
+fdt set /chosen/module@MODULE_ADDR reg <0 MODULE_ADDR 0 MODULE_SIZE>
+
+# bootargs payload mirrors tests/grub.cfg.single.template's x86
+# module line — a multi-word boot-script string so kern/bootstrap.c
+# takes the boot_script_task_create path (which threads
+# ${host-port} / ${device-port} / $(task-create) / $(task-resume)
+# through the boot-script interpreter) rather than the broken
+# single-word bootstrap_exec_compat path. Use single quotes so
+# u-boot's hush parser doesn't expand the ${...} / $(...) tokens —
+# they need to reach /chosen/module/bootargs verbatim for the
+# kernel boot-script parser to find and substitute them.
+fdt set /chosen/module@MODULE_ADDR bootargs 'module-TESTNAME ${host-port}
${device-port} $(task-create) $(task-resume)'
+
+# Force the DTB low. u-boot's default booti behaviour relocates
+# the DTB to the highest available memory before entering the
+# kernel — but gnumach's aarch64 pmap excludes the DTB region from
+# the vm_page heap (heap_start is bumped above the DTB), and the
+# single-segment heap is already capped below the lowest module.
+# A relocated-high DTB pushes heap_start past the module → heap
+# becomes empty → vm_page panics during bootstrap. Pin the DTB
+# below the kernel's load area so it lands in the heap-excluded
+# kernel image region (which is already lost to the heap) instead
+# of stealing usable heap above the module.
+setenv fdt_high 0x42000000
+
+# Hand off — `booti` takes <kernel> <initrd|"-"> <dtb>.
+booti ${kernel_addr} - ${fdtcontroladdr}
diff --git a/tests/user-qemu.mk b/tests/user-qemu.mk
index 857dbaac..a55d4f7b 100644
--- a/tests/user-qemu.mk
+++ b/tests/user-qemu.mk
@@ -86,6 +86,9 @@ endif
if HOST_x86_64
$(eval $(call generate_mig_client,mach/x86_64,mach_i386))
endif
+if HOST_aarch64
+$(eval $(call generate_mig_client,mach/aarch64,mach_aarch64))
+endif
# NOTE: keep in sync with the rules above
MIG_GEN_CC = \
@@ -99,8 +102,19 @@ MIG_GEN_CC = \
$(MIG_OUTDIR)/mach.user.c \
$(MIG_OUTDIR)/mach_host.user.c \
$(MIG_OUTDIR)/mach_port.user.c \
- $(MIG_OUTDIR)/task_notify.server.c \
- $(MIG_OUTDIR)/mach_i386.user.c
+ $(MIG_OUTDIR)/task_notify.server.c
+
+# Per-arch machine_interface stub: keep this in sync with the
+# arch-specific generate_mig_client calls above.
+if HOST_ix86
+MIG_GEN_CC += $(MIG_OUTDIR)/mach_i386.user.c
+endif
+if HOST_x86_64
+MIG_GEN_CC += $(MIG_OUTDIR)/mach_i386.user.c
+endif
+if HOST_aarch64
+MIG_GEN_CC += $(MIG_OUTDIR)/mach_aarch64.user.c
+endif
#
# compilation of user space tests and utilities
@@ -131,8 +145,16 @@ TESTSRC_TESTLIB= \
$(srcdir)/tests/testlib.c \
$(srcdir)/tests/testlib_thread_start.c
+# The testlib's per-arch memcpy/memset/memcmp: i386's hand-tuned asm
+# version vs aarch64's plain-C one in aarch64/aarch64/strings.c.
+if HOST_aarch64
+ARCH_STRINGS_C = $(srcdir)/aarch64/aarch64/strings.c
+else
+ARCH_STRINGS_C = $(srcdir)/i386/i386/strings.c
+endif
+
SRC_TESTLIB= \
- $(srcdir)/i386/i386/strings.c \
+ $(ARCH_STRINGS_C) \
$(srcdir)/kern/printf.c \
$(srcdir)/kern/strings.c \
$(srcdir)/util/atoi.c \
@@ -167,16 +189,28 @@ tests/module-%: $(srcdir)/tests/test-%.c $(SRC_TESTLIB)
$(MACH_TESTINSTALL)
#
# Avoid removal of module-% files after building the ISO file
#
-.PRECIOUS: tests/module-%
+.PRECIOUS: tests/module-% tests/boot-%.scr tests/test-%.img
#
# packaging of qemu bootable image and test runner
#
GNUMACH_ARGS = console=com0
-QEMU_OPTS = -m 2047 -nographic -no-reboot -boot d
+QEMU_OPTS = -nographic -no-reboot -boot d
QEMU_GDB_PORT ?= 1234
+# Memory size depends on what the kernel's bootstrap pmap can map.
+# x86 builds use 2047 MB historically (the upper bound of the 32-bit
+# "low" memory gnumach manages directly). aarch64's pmap_bootstrap
+# maps a single 1 GB L1 block in TTBR1, so vm_page's allocator faults
+# on memory beyond that — keep aarch64 well under 1 GB until the
+# kernel grows multi-block mappings.
+if HOST_aarch64
+QEMU_OPTS += -m 512
+else
+QEMU_OPTS += -m 2047
+endif
+
if HOST_ix86
QEMU_BIN = qemu-system-i386
QEMU_OPTS += -cpu pentium3-v1
@@ -185,10 +219,67 @@ if HOST_x86_64
QEMU_BIN = qemu-system-x86_64
QEMU_OPTS += -cpu core2duo-v1
endif
+if HOST_aarch64
+QEMU_BIN = qemu-system-aarch64
+QEMU_OPTS += -M virt -cpu cortex-a72
+endif
if enable_smp
QEMU_OPTS += -smp 2
endif
+# Per-arch boot delivery. x86 boots from a multiboot ISO via
+# -cdrom. aarch64 boots through u-boot running under QEMU: the
+# test FAT image contains gnumach, the test module, and a boot.scr
+# that uses u-boot's `fdt mknod` to inject the multiboot,module DTB
+# nodes gnumach's load_boot_modules_from_dtb() reads. This is the
+# same convention real-hardware u-boot users follow per
+# aarch64/BOOTING — same kernel code path, same DTB shape on entry.
+#
+# UBOOT_BIN is exported by the Nix dev shell for HOST_aarch64; it
+# points at the u-boot.bin nixpkgs ships in pkgs.ubootQemuAarch64.
+if HOST_aarch64
+TEST_DEPS = tests/test-%.img
+QEMU_BOOT_ARGS = -bios $(UBOOT_BIN) -drive
file=tests/test-TESTNAME.img,format=raw,if=virtio
+else
+TEST_DEPS = tests/test-%.iso
+QEMU_BOOT_ARGS = -cdrom tests/test-TESTNAME.iso
+endif
+
+# Module load address on aarch64: RAM_BASE + 256 MB. pmap_bootstrap
+# maps a single 1 GB L1 block in TTBR1 and vm_page_load_heap caps
+# the heap below the lowest module's address, so memory above the
+# module is lost to the allocator (single-segment carve-out).
+# Placing the module at 256 MB leaves the heap a usable 256 MB —
+# enough for the vm_page bootstrap array + kernel image. Multi-
+# segment heap is a follow-up that would lift this constraint.
+AARCH64_MODULE_ADDR = 0x50000000
+
+# Per-test u-boot script: substitutes the test name, module address,
+# and module size into the template, then wraps via mkimage as a
+# u-boot legacy script image (which u-boot's distro_bootcmd auto-
+# locates on virtio0:1 at boot).
+tests/boot-%.scr: $(srcdir)/tests/uboot.script.template tests/module-%
+ < $(srcdir)/tests/uboot.script.template \
+ sed -e "s|TESTNAME|$*|g" \
+ -e "s|MODULE_ADDR|$(AARCH64_MODULE_ADDR)|g" \
+ -e "s|MODULE_SIZE|0x$$(printf '%x' $$(stat -c %s
tests/module-$*))|g" \
+ >tests/boot-$*.cmd
+ mkimage -A arm64 -T script -C none -d tests/boot-$*.cmd $@
+ rm -f tests/boot-$*.cmd
+
+# Per-test FAT image: 16 MB disk with a single FAT partition (MBR
+# from sfdisk; mtools writes the FAT inside at the 1 MB offset).
+# u-boot's bootflow scanner only finds boot.scr on partitioned
+# disks, hence the MBR — a bare FAT volume is silently skipped.
+tests/test-%.img: tests/module-% $(GNUMACH) tests/boot-%.scr
+ rm -f $@
+ truncate -s 16M $@
+ printf 'label: dos\n2048,, c, *\n' | sfdisk -q $@
+ mformat -i $@@@1M -v GNUMACH
+ mcopy -i $@@@1M $(GNUMACH) ::/gnumach
+ mcopy -i $@@@1M tests/module-$* ::/module-$*
+ mcopy -i $@@@1M tests/boot-$*.scr ::/boot.scr
+
tests/test-%.iso: tests/module-% $(GNUMACH)
$(srcdir)/tests/grub.cfg.single.template
rm -rf $(builddir)/tests/isofiles-$*
mkdir -p $(builddir)/tests/isofiles-$*/boot/grub/
@@ -202,10 +293,11 @@ tests/test-%.iso: tests/module-% $(GNUMACH)
$(srcdir)/tests/grub.cfg.single.temp
grub-mkrescue -o $@ $(builddir)/tests/isofiles-$*
rm -rf $(builddir)/tests/isofiles-$*
-tests/test-%: tests/test-%.iso $(srcdir)/tests/run-qemu.sh.template
- < $(srcdir)/tests/run-qemu.sh.template \
- sed -e "s|TESTNAME|$(subst tests/test-,,$@)|g" \
- -e "s/QEMU_OPTS/$(QEMU_OPTS)/g" \
+tests/test-%: $(TEST_DEPS) $(srcdir)/tests/run-qemu.sh.template
+ < $(srcdir)/tests/run-qemu.sh.template \
+ sed -e 's|QEMU_BOOT_ARGS|$(QEMU_BOOT_ARGS)|g' \
+ -e "s|TESTNAME|$(subst tests/test-,,$@)|g" \
+ -e "s/QEMU_OPTS/$(QEMU_OPTS)/g" \
-e "s/QEMU_BIN/$(QEMU_BIN)/g" \
-e "s/TEST_START_MARKER/$(TEST_START_MARKER)/g" \
-e "s/TEST_SUCCESS_MARKER/$(TEST_SUCCESS_MARKER)/g" \
--
2.54.0