iso: boot both firmwares, from a disc with a real hierarchy
The disc carried one file -- boot/schos.bin -- with 8.3 uppercase names, one El Torito entry, and no way to boot a UEFI machine. Now: /boot/schos.bin BIOS entry, unchanged: no-emulation, 1+STAGE2_SECTORS /boot/efiboot.img UEFI entry: a FAT image built with mtools /EFI/BOOT/BOOTX64.EFI the loader, also visible as a file /bin /etc /usr ... the staged sysroot as an ordinary tree Rock Ridge and Joliet restore real names, permissions and symlinks; -cache-inodes matters more than it looks, because the sysroot is a toybox multicall binary with a couple of hundred hard links to it and without inode caching each link becomes its own copy. -isohybrid-gpt-basdat adds a GPT so the .iso can be dd'd to a stick and booted by UEFI. That is UEFI only and deliberately so: the BIOS isohybrid trick needs syslinux's isohdpfx.bin, and even with it this loader reads the kernel back over ATAPI, which a stick presented as a hard disk cannot serve. usbstick.img is what that is for. The FAT image carries the kernel as well as the loader, because BOOTX64.EFI opens \schos\kernel on the volume it was loaded from, and for a UEFI boot that is the FAT image rather than the ISO 9660 tree. Without it the loader halted with EFI_NOT_FOUND. Also fixes `make iso` and `make ramroot`, which were already broken: fs-ram.img was still built by the v1 mkfs while the kernel accepts only SchFS as root, so the ramdisk was a format it could not mount and it panicked with "schfs root: no SchFS volume on the root device". It is a mkschfs image now, which needed --exclude in mkschfs -- dropping the exclude list was not an option, because the ramdisk is linked into the kernel binary and what goes in it is a licensing question. RAMDISK_NINODES is gone with the v1 image: SchFS has no preallocated inode table. Verified: BIOS ISO boots to a shell and runs commands over serial (241 files in /bin, /etc/motd reads back). UEFI ISO boots to a prompt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e4df779048
commit
61b35a1f71
3 changed files with 188 additions and 14 deletions
27
Makefile
27
Makefile
|
|
@ -291,11 +291,9 @@ CFLAGS_SYS = $(CFLAGS) -I./kernel/sys
|
|||
# -----------------------------------------------------------------------
|
||||
RAMDISK_BLOCKS ?= 6000
|
||||
|
||||
# Inodes for the ramdisk root. The inode table is preallocated, so mkfs's
|
||||
# default of 12000 cost 1501 of those 6000 blocks -- a quarter of the image, to
|
||||
# describe about 260 files. 2000 is still an order of magnitude of headroom and
|
||||
# costs 251 blocks.
|
||||
RAMDISK_NINODES ?= 2000
|
||||
# RAMDISK_NINODES used to live here, sizing mkfs's preallocated inode table.
|
||||
# The ramdisk is a SchFS image now (see $(BUILD)/fs-ram.img) and SchFS allocates
|
||||
# object ids as it goes, so there is no table to size and nothing to tune.
|
||||
|
||||
# Kept out of the ramdisk root.
|
||||
#
|
||||
|
|
@ -444,8 +442,12 @@ iso:
|
|||
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
|
||||
$(MAKE) RAMROOT=1 schos.iso
|
||||
|
||||
schos.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel
|
||||
STAGE2_SECTORS=$(STAGE2_SECTORS) sh tools/posix/mkiso.sh $@
|
||||
# The UEFI loader is a dependency now: the disc carries a second El Torito
|
||||
# entry pointing at a FAT image that holds it, so one disc boots both firmwares.
|
||||
# sysroot-install because the disc also carries the tree as a real hierarchy.
|
||||
schos.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel \
|
||||
$(BUILD)/uefi/BOOTX64.EFI sysroot-install
|
||||
STAGE2_SECTORS=$(STAGE2_SECTORS) SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@
|
||||
|
||||
# Boot the ISO the way a machine booting off a disc would: no hard disk at
|
||||
# all, so a kernel that still secretly depends on one fails here rather than
|
||||
|
|
@ -525,10 +527,17 @@ $(BUILD)/kernel/kernel: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel
|
|||
# The ramdisk root image, and the object wrapping it for the linker. Built from
|
||||
# the same staged sysroot as the main image, just capped at RAMDISK_BLOCKS --
|
||||
# mkfs's content dedup means the whole userland still fits in a few MB.
|
||||
$(BUILD)/fs-ram.img: $(MKFS) $(BUILD)/sysroot.stamp
|
||||
# Built with mkschfs, not mkfs.
|
||||
#
|
||||
# SchFS is the only root the kernel will mount -- it panics in forkret() when
|
||||
# handed anything else -- so a v1 image here produced "schfs root: no SchFS
|
||||
# volume on the root device" and made `make iso` and `make ramroot` unbootable.
|
||||
# RAMDISK_NINODES is gone with it: SchFS has no preallocated inode table, which
|
||||
# is what that knob existed to size.
|
||||
$(BUILD)/fs-ram.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
|
||||
@mkdir -p $(BUILD)
|
||||
@rm -f $@
|
||||
$(MKFS) $@ --size $(RAMDISK_BLOCKS) --ninodes $(RAMDISK_NINODES) \
|
||||
$(MKSCHFS) $@ --size $(RAMDISK_BLOCKS) --label schos \
|
||||
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(SYSROOT)
|
||||
|
||||
$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.img
|
||||
|
|
|
|||
|
|
@ -464,6 +464,34 @@ nl_cmp(const void *a, const void *b)
|
|||
|
||||
static void add_tree(uint64_t dirino, const char *hostdir);
|
||||
|
||||
// Paths kept out of the image, sysroot-relative.
|
||||
//
|
||||
// Same rule and same spelling as mkfs.c, because the two tools build the same
|
||||
// tree and a path excluded from one must be excluded from the other. This is a
|
||||
// licensing mechanism before it is a size one: the ramdisk image is linked into
|
||||
// the kernel binary, so anything staged in sysroot/ when it is built becomes
|
||||
// part of a distributable artifact.
|
||||
#define MAXEXCLUDE 32
|
||||
static const char *excludes[MAXEXCLUDE];
|
||||
static int nexcludes;
|
||||
// strlen(sysroot) + 1, so a host path can be turned back into the
|
||||
// sysroot-relative one the exclude patterns are written against.
|
||||
static size_t sysroot_prefix;
|
||||
|
||||
// Matches exactly, or as a directory prefix so that "usr/share/misc" also drops
|
||||
// "usr/share/misc/big.dat".
|
||||
static int
|
||||
is_excluded(const char *relpath)
|
||||
{
|
||||
for (int i = 0; i < nexcludes; i++) {
|
||||
size_t n = strlen(excludes[i]);
|
||||
if (strncmp(relpath, excludes[i], n) == 0 &&
|
||||
(relpath[n] == '\0' || relpath[n] == '/'))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Returns 1 if the entry was a directory, so the caller can total up the link
|
||||
// count without walking the directory a second time.
|
||||
static int
|
||||
|
|
@ -472,6 +500,15 @@ add_entry(uint64_t dirino, const char *hostdir, const char *name)
|
|||
char path[8192];
|
||||
snprintf(path, sizeof(path), "%s/%s", hostdir, name);
|
||||
|
||||
// Checked before the entry is dispatched, so an excluded directory is never
|
||||
// opened and never gets an inode -- an empty directory left behind would
|
||||
// still describe what was removed.
|
||||
if (nexcludes && strlen(path) > sysroot_prefix &&
|
||||
is_excluded(path + sysroot_prefix)) {
|
||||
fprintf(stderr, "mkschfs: excluding %s\n", path + sysroot_prefix);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
// lstat, not stat: a symlink must be stored as a symlink rather than
|
||||
// silently turned into a second copy of whatever it points at.
|
||||
|
|
@ -562,6 +599,14 @@ main(int argc, char **argv)
|
|||
nblocks = strtoull(argv[++i], NULL, 0);
|
||||
else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc)
|
||||
label = argv[++i];
|
||||
else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) {
|
||||
if (nexcludes >= MAXEXCLUDE) {
|
||||
fprintf(stderr, "mkschfs: too many --exclude paths (max %d)\n",
|
||||
MAXEXCLUDE);
|
||||
return 1;
|
||||
}
|
||||
excludes[nexcludes++] = argv[++i];
|
||||
}
|
||||
else if (strcmp(argv[i], "--hash-bits") == 0 && i + 1 < argc) {
|
||||
// Testing only: shrink the name hash so collisions are the norm
|
||||
// rather than a once-in-a-lifetime event. schfs_fsck takes the
|
||||
|
|
@ -583,7 +628,9 @@ main(int argc, char **argv)
|
|||
if (!out || !sysroot) {
|
||||
fprintf(stderr,
|
||||
"usage: mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]\n"
|
||||
" BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n",
|
||||
" [--exclude PATH]...\n"
|
||||
" BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n"
|
||||
" --exclude omits a sysroot-relative path and its subtree\n",
|
||||
SCHFS_BLKSIZE, (int)(6000 * SCHFS_BLKSIZE / (1024 * 1024)));
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -630,6 +677,10 @@ main(int argc, char **argv)
|
|||
if (r != 0) die("writing the root inode", r);
|
||||
ndirs++;
|
||||
|
||||
// add_entry() builds host paths as "<sysroot>/<name>", so this is where the
|
||||
// sysroot-relative part of such a path begins.
|
||||
sysroot_prefix = strlen(sysroot) + 1;
|
||||
|
||||
add_tree(VOL.root_objectid, sysroot);
|
||||
|
||||
// Publish. Everything above this line is invisible to a reader; if the tool
|
||||
|
|
|
|||
|
|
@ -3,8 +3,17 @@
|
|||
#
|
||||
# tools/posix/mkiso.sh [output.iso]
|
||||
#
|
||||
# The disc boots through El Torito's "no emulation" mode, which is the only
|
||||
# one of the three that fits how SchOS actually loads itself.
|
||||
# The disc carries two El Torito boot entries and a real directory tree:
|
||||
#
|
||||
# /boot/schos.bin BIOS: cdboot + stage2 + kernel, loaded no-emulation
|
||||
# /boot/efiboot.img UEFI: a FAT image holding EFI/BOOT/BOOTX64.EFI
|
||||
# /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc
|
||||
# /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy
|
||||
#
|
||||
# --- BIOS entry -------------------------------------------------------------
|
||||
#
|
||||
# El Torito's "no emulation" mode, which is the only one of the three that fits
|
||||
# how SchOS actually loads itself.
|
||||
#
|
||||
# floppy emulation caps the image at 2.88 MB; the ramdisk-root kernel
|
||||
# alone is bigger than that.
|
||||
|
|
@ -30,6 +39,38 @@
|
|||
# NOT using -boot-info-table on purpose: it patches 56 bytes over offset 8 of
|
||||
# the boot image, which is the middle of cdboot's code. stage2main.c finds
|
||||
# where it was loaded from by reading the El Torito boot catalog instead.
|
||||
#
|
||||
# --- UEFI entry -------------------------------------------------------------
|
||||
#
|
||||
# UEFI firmware does not run El Torito no-emulation code. It wants a FAT
|
||||
# filesystem containing EFI/BOOT/BOOTX64.EFI, offered as a second boot entry.
|
||||
# So one is built with mtools and added with -eltorito-alt-boot. The same
|
||||
# loader is also copied into the ISO tree at /EFI/BOOT/BOOTX64.EFI, because
|
||||
# some firmware looks there directly when booting from a USB stick.
|
||||
#
|
||||
# --- Booting from a USB stick -----------------------------------------------
|
||||
#
|
||||
# -isohybrid-gpt-basdat adds a GPT that marks the FAT image as a basic data
|
||||
# partition, so writing this .iso to a stick with dd produces something UEFI
|
||||
# will boot.
|
||||
#
|
||||
# That covers UEFI only, and deliberately. The classic BIOS isohybrid trick
|
||||
# needs syslinux's isohdpfx.bin, and even with it the BIOS path here would not
|
||||
# work: on a stick the firmware presents the medium as a hard disk, while
|
||||
# stage2main.c reads the kernel back over ATAPI. Booting BIOS off USB is what
|
||||
# build/usbstick.img is for.
|
||||
#
|
||||
# --- Rock Ridge and Joliet --------------------------------------------------
|
||||
#
|
||||
# Plain ISO 9660 gives 8.3 uppercase names with no permissions, so the sysroot
|
||||
# would arrive as /BIN/TOYBOX.;1 with every file marked the same. Rock Ridge
|
||||
# restores POSIX names, permissions and symlinks for Unix readers; Joliet does
|
||||
# long names for Windows. Both are additions to the same image -- a reader that
|
||||
# understands neither still sees a valid, if ugly, ISO 9660 disc.
|
||||
#
|
||||
# -cache-inodes matters more than it looks: the sysroot is a toybox multicall
|
||||
# binary with a couple of hundred hard links to it. Without inode caching each
|
||||
# link becomes its own copy and the disc grows by two orders of magnitude.
|
||||
|
||||
set -e
|
||||
|
||||
|
|
@ -39,14 +80,17 @@ STAGE2_SECTORS=${STAGE2_SECTORS:-48}
|
|||
STAGE2_BYTES=$((STAGE2_SECTORS * 512))
|
||||
LOAD_SIZE=$((1 + STAGE2_SECTORS))
|
||||
STAGING="$BUILD/iso"
|
||||
SYSROOT=${SYSROOT:-sysroot}
|
||||
|
||||
CDBOOT="$BUILD/boot/cdboot"
|
||||
STAGE2="$BUILD/boot/stage2"
|
||||
KERNEL="$BUILD/kernel/kernel"
|
||||
EFIAPP="$BUILD/uefi/BOOTX64.EFI"
|
||||
|
||||
for f in "$CDBOOT" "$STAGE2" "$KERNEL"; do
|
||||
[ -f "$f" ] || { echo "mkiso: missing $f -- run make first" >&2; exit 1; }
|
||||
done
|
||||
[ -f "$EFIAPP" ] || { echo "mkiso: missing $EFIAPP -- run 'make uefi' first" >&2; exit 1; }
|
||||
|
||||
# The three pieces must be contiguous and at these exact sizes: stage2main.c's
|
||||
# readseg() finds the kernel at sector (1 + STAGE2_SECTORS) of the boot image,
|
||||
|
|
@ -57,9 +101,61 @@ stage2_sz=$(wc -c < "$STAGE2")
|
|||
[ "$stage2_sz" -eq "$STAGE2_BYTES" ] || { echo "mkiso: stage2 is $stage2_sz bytes, expected $STAGE2_BYTES (STAGE2_SECTORS=$STAGE2_SECTORS)" >&2; exit 1; }
|
||||
|
||||
rm -rf "$STAGING"
|
||||
mkdir -p "$STAGING/boot"
|
||||
mkdir -p "$STAGING/boot" "$STAGING/EFI/BOOT"
|
||||
|
||||
cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schos.bin"
|
||||
cp "$EFIAPP" "$STAGING/EFI/BOOT/BOOTX64.EFI"
|
||||
|
||||
# The FAT image for the UEFI El Torito entry.
|
||||
#
|
||||
# It carries the kernel as well as the loader. BOOTX64.EFI opens \schos\kernel
|
||||
# on the volume it was itself loaded from, and for a UEFI boot that volume is
|
||||
# this FAT image -- not the ISO 9660 tree around it. Without the kernel in here
|
||||
# the loader comes up and halts with EFI_NOT_FOUND, which is what it did.
|
||||
#
|
||||
# That means the kernel is stored twice on the disc: once inside boot/schos.bin
|
||||
# for the BIOS entry, once here for the UEFI one. The alternative is teaching
|
||||
# the loader to go looking for other volumes, which is a real feature and not a
|
||||
# packaging detail -- worth doing deliberately rather than to save a copy.
|
||||
#
|
||||
# Sized from what actually goes in plus slack, rather than fixed: a hardcoded
|
||||
# size is a thing that breaks quietly the first time the kernel grows.
|
||||
EFI_IMG="$STAGING/boot/efiboot.img"
|
||||
payload_kb=$(( ( $(wc -c < "$EFIAPP") + $(wc -c < "$KERNEL") + 1023 ) / 1024 ))
|
||||
img_kb=$(( ( (payload_kb + 512 + 31) / 32 ) * 32 ))
|
||||
[ "$img_kb" -lt 512 ] && img_kb=512
|
||||
|
||||
if command -v mformat >/dev/null 2>&1 && command -v mcopy >/dev/null 2>&1; then
|
||||
rm -f "$EFI_IMG"
|
||||
# Create the file first and format it with an explicit geometry.
|
||||
#
|
||||
# Not `mformat -f`: that takes one of the standard floppy capacities
|
||||
# (360, 720, 1440, 2880 KB) and rejects anything else -- "Bad size 512".
|
||||
# Giving heads, sectors-per-track and a total sector count instead lets the
|
||||
# image be whatever the loader actually needs, which is the point of sizing
|
||||
# it from the file rather than hardcoding 1.44 MB.
|
||||
dd if=/dev/zero of="$EFI_IMG" bs=1024 count="$img_kb" status=none
|
||||
mformat -i "$EFI_IMG" -h 64 -s 32 -T $((img_kb * 2)) ::
|
||||
mmd -i "$EFI_IMG" ::/EFI ::/EFI/BOOT ::/schos
|
||||
mcopy -i "$EFI_IMG" "$EFIAPP" ::/EFI/BOOT/BOOTX64.EFI
|
||||
mcopy -i "$EFI_IMG" "$KERNEL" ::/schos/kernel
|
||||
HAVE_EFI=1
|
||||
else
|
||||
echo "mkiso: warning: mtools not found, building a BIOS-only disc" >&2
|
||||
echo " Fedora: sudo dnf install mtools" >&2
|
||||
rm -f "$EFI_IMG"
|
||||
HAVE_EFI=0
|
||||
fi
|
||||
|
||||
# The sysroot, as an ordinary hierarchy. Copied rather than linked so that
|
||||
# whatever ends up on the disc is a snapshot, not a view of a tree that may be
|
||||
# rebuilt underneath it.
|
||||
if [ -d "$SYSROOT" ]; then
|
||||
for d in "$SYSROOT"/*; do
|
||||
[ -e "$d" ] || continue
|
||||
cp -a "$d" "$STAGING/"
|
||||
done
|
||||
fi
|
||||
|
||||
# xorriso if it is here, mkisofs otherwise. Both accept the El Torito flags
|
||||
# below unchanged -- xorriso's -as mkisofs mode exists precisely to be
|
||||
|
|
@ -68,8 +164,10 @@ cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schos.bin"
|
|||
# mkisofs.exe.
|
||||
if command -v xorriso >/dev/null 2>&1; then
|
||||
MKISOFS="xorriso -as mkisofs"
|
||||
HYBRID="-isohybrid-gpt-basdat" # mkisofs proper does not know this one
|
||||
elif command -v mkisofs >/dev/null 2>&1; then
|
||||
MKISOFS="mkisofs"
|
||||
HYBRID=""
|
||||
else
|
||||
echo "mkiso: need xorriso or mkisofs on PATH" >&2
|
||||
echo " Windows: scoop install cdrtools" >&2
|
||||
|
|
@ -77,15 +175,31 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$HAVE_EFI" = 1 ]; then
|
||||
EFI_ARGS="-eltorito-alt-boot -e boot/efiboot.img -no-emul-boot $HYBRID"
|
||||
else
|
||||
EFI_ARGS=""
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086 # EFI_ARGS is a deliberate word list
|
||||
$MKISOFS \
|
||||
-quiet \
|
||||
-V SCHOS \
|
||||
-A "SchOS" \
|
||||
-R -J -joliet-long \
|
||||
-cache-inodes \
|
||||
-b boot/schos.bin \
|
||||
-no-emul-boot \
|
||||
-boot-load-size "$LOAD_SIZE" \
|
||||
$EFI_ARGS \
|
||||
-o "$OUT" \
|
||||
"$STAGING"
|
||||
|
||||
echo "==> $OUT ($(wc -c < "$OUT") bytes)"
|
||||
if [ "$HAVE_EFI" = 1 ]; then
|
||||
echo " BIOS: qemu-system-x86_64 -cdrom $OUT -boot d"
|
||||
echo " UEFI: qemu-system-x86_64 -bios OVMF_CODE.fd -cdrom $OUT"
|
||||
echo " USB: dd if=$OUT of=/dev/sdX bs=4M (UEFI only -- see the header)"
|
||||
else
|
||||
echo " qemu-system-x86_64 -cdrom $OUT -boot d"
|
||||
fi
|
||||
|
|
|
|||
Loading…
Reference in a new issue