commit 7eb6f0aca7c2e70dc13a72a3be5b46f6de5ff588
parent 6ec738c133517a886e87ecdbc76c0322a8c60d7f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 5 May 2026 18:49:15 -0700
seed-kernel: tcc3 self-hosts the link step; boot6 closes the loop
`scripts/boot6.sh aarch64` builds + links the seed kernel end-to-end
with tcc3 — no `ld -T kernel.lds`, no objcopy. Link line is just
three flags: `-Wl,-Ttext=0x40080000 -Wl,--oformat=binary` plus
freestanding `-nostdlib -static`.
Got the surface that small by leaning on existing tcc conventions
rather than growing a linker-script subset:
- `_start` is tcc's hard-coded entry symbol, so kernel.S renames the
arm64 Image-header label `_head` -> `_start`.
- `_end` is auto-defined by tcc at the end of `.bss` (existing
`tcc_add_linker_symbols`); kernel.S uses it for both the bss-zero
upper bound and the Image-header `image_size` field, replacing
the lds's `__bss_end` and `_image_end` assignments.
- `__bss_start` is the one missing piece — added via a 9-line
`bss-start-symbol` simple-patch that mirrors tcc's existing `_end`
synthesis.
- The arm64 Image header moves from a custom `.head.text` section
into `.text`; linking kernel.S first lands it at file offset 0,
so the `ARM\x64` magic ends up at offset 0x38 with no script-side
ordering. `--oformat=binary` then writes a flat Image directly,
which QEMU's `-kernel` boot-protocol path needs to put DTB in x0
(the ELF-load path doesn't).
`DRIVER=seed scripts/boot.sh aarch64` runs the entire boot0..boot6
pipeline on top of the tcc3-built kernel, including a re-run of
boot6 that rebuilds the same kernel under itself.
Diffstat:
18 files changed, 330 insertions(+), 85 deletions(-)
diff --git a/docs/OS-TODO.md b/docs/OS-TODO.md
@@ -11,40 +11,28 @@ ELF, dispatches the eight Tier-1 syscalls plus atomic `sys_spawn`
acceptance; `boot{0..5}.sh DRIVER=seed` is byte-identical to the
podman path. HVF acceleration enabled.
-A kernel built end-to-end with the patched
-[`build/aarch64/boot4/tcc3`](../scripts/boot4.sh) (`tcc3 -c
-kernel.{S,c}` + `tcc3 -c tcc-cc/mem.c` → `ld -T kernel.lds`) boots
-under qemu-system-aarch64 and produces console output identical to
-the gcc-built kernel through `[seed] user exit_group(0)`.
+[`scripts/boot6.sh`](../scripts/boot6.sh) builds and links the seed
+kernel end-to-end with the patched
+[`build/aarch64/boot4/tcc3`](../scripts/boot4.sh) — no `ld -T
+kernel.lds`, no objcopy. The link line is just three flags:
+
+```
+tcc3 -nostdlib -static \
+ -Wl,-Ttext=0x40080000 \
+ -Wl,--oformat=binary \
+ -o kernel.elf kernel.S.o kernel.c.o mem.c.o
+```
+
+`DRIVER=seed scripts/boot.sh aarch64` runs the entire boot0→boot6
+pipeline (including a re-run of boot6 itself) on top of the
+tcc3-built kernel, closing the self-host loop at the OS layer.
This file tracks remaining polish.
-## tcc3 link with `kernel.lds`
+## boot6 output filename
-`tcc -Wl,-T,kernel.lds` errors with `unsupported linker option`;
-tcc3's `-Wl,` accepts only `-Ttext=` / `-image-base=` /
-`-section-alignment=`. The seed kernel's layout needs:
-
-- `KEEP(*(.head.text))` first (boot header at `0x40080000`).
-- Link-time symbol assignments: `__bss_start`, `__bss_end`,
- `kstack_top`, `_end`, `_image_end`.
-- A custom `.stack` section sized to 64 KB.
-
-Two paths:
-
-- **Inline.** Define the symbols and reserve a 64 KB stack inside
- `kernel.S` using `.skip` / `.balign`. Brackets the kernel's `.bss`
- by putting `__bss_start:` in a `.bss.0_start` section and
- `__bss_end:` in `.bss.9_end` — but tcc/ld won't merge these into
- the main `.bss` without a script, so each becomes its own output
- section. Workable only if BSS is replaced with explicit storage in
- `kernel.S`; brittle otherwise.
-- **Tcc gains a small ld-script subset.** `KEEP(*(...))`, simple
- symbol assignment (`sym = .;`), and a single explicit-storage
- section block. ~150 LoC, reusable across kernel-style targets,
- doesn't restructure `kernel.S` further.
-
-The post-refactor balance tips toward the script subset.
-
-Until either lands, the tcc-built seed kernel still uses gcc's
-`ld -T kernel.lds` for the final link step.
+[`scripts/boot6.sh`](../scripts/boot6.sh) writes its output to
+`build/aarch64/boot6/kernel.elf`, but with `--oformat=binary` the
+file is a flat arm64 boot Image, not ELF. Misleading; rename to
+`Image` (matching `seed-kernel/build/Image` from the gcc Makefile)
+and update boot{0..5}.sh's `KERNEL_IMAGE=` to follow.
diff --git a/scripts/boot.sh b/scripts/boot.sh
@@ -6,7 +6,13 @@
## DRIVER=podman scripts/boot.sh <amd64|aarch64|riscv64>
##
## DRIVER (default podman) is exported and consumed by each bootN.sh.
-## DRIVER=seed is aarch64-only and requires seed-kernel/build/Image.
+## DRIVER=seed is aarch64-only and runs on build/$ARCH/boot6/kernel.elf —
+## the tcc3-built seed kernel produced by boot6. First-time setup
+## therefore requires one prior podman pass to produce that kernel:
+## ./scripts/boot.sh aarch64 # default DRIVER=podman
+## DRIVER=seed ./scripts/boot.sh aarch64 # re-run on tcc-built kernel
+## Subsequent DRIVER=seed runs reuse the kernel.elf from the prior boot6
+## (stashed across the build/$ARCH wipe below).
set -e
@@ -14,22 +20,32 @@ ARCH=$1
DRIVER=${DRIVER:-podman}
case "$DRIVER" in
- podman) ;;
seed)
[ "$ARCH" = aarch64 ] || { echo "[boot] DRIVER=seed: aarch64 only" >&2; exit 2; }
- if [ ! -f seed-kernel/build/Image ]; then
- echo "[boot] building seed-kernel/build/Image"
- podman run --rm --pull=never --platform linux/arm64 \
- -v "$PWD/seed-kernel:/work" -w /work boot2-alpine-gcc:aarch64 \
- sh -c 'apk add --no-progress --quiet make >/dev/null 2>&1; make -s'
+ KERNEL_ELF=build/$ARCH/boot6/kernel.elf
+ if [ ! -f "$KERNEL_ELF" ]; then
+ echo "[boot] DRIVER=seed: missing $KERNEL_ELF" >&2
+ echo "[boot] run './scripts/boot.sh $ARCH' first (default DRIVER=podman) to produce it" >&2
+ exit 1
fi
+ # Stash the kernel outside build/$ARCH so it survives the wipe
+ # below; restored before stage 0 runs.
+ STASH=build/.seed-bootstrap/$ARCH
+ mkdir -p "$STASH"
+ cp "$KERNEL_ELF" "$STASH/kernel.elf"
;;
+ podman) ;;
*) echo "[boot] unknown DRIVER=$DRIVER (expected podman|seed)" >&2; exit 2 ;;
esac
export DRIVER
rm -rf build/$ARCH
+if [ "$DRIVER" = seed ]; then
+ mkdir -p build/$ARCH/boot6
+ cp build/.seed-bootstrap/$ARCH/kernel.elf build/$ARCH/boot6/kernel.elf
+fi
+
T0=$(date +%s)
trap 'echo "[boot/$DRIVER $ARCH] elapsed at exit: $(($(date +%s) - T0))s"' EXIT
@@ -47,3 +63,9 @@ stage boot2 ./scripts/boot2.sh $ARCH
stage boot3 ./scripts/boot3.sh $ARCH
stage boot4 ./scripts/boot4.sh $ARCH
stage boot5 ./scripts/boot5.sh $ARCH
+
+# boot6 builds the seed-kernel ELF with boot4's tcc3 (no `ld -T`,
+# no objcopy). Arm64-only — the seed kernel is arm64-specific.
+if [ "$ARCH" = aarch64 ]; then
+ stage boot6 ./scripts/boot6.sh $ARCH
+fi
diff --git a/scripts/boot0.sh b/scripts/boot0.sh
@@ -48,9 +48,9 @@ case "$DRIVER" in
export PLATFORM IMAGE ;;
seed)
[ "$ARCH" = "aarch64" ] || { echo "[boot0] DRIVER=seed: aarch64 only" >&2; exit 2; }
- KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ KERNEL_IMAGE=$ROOT/build/$ARCH/boot6/kernel.elf
EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
- [ -f "$KERNEL_IMAGE" ] || { echo "[boot0] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot0] missing $KERNEL_IMAGE — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
export KERNEL_IMAGE EXTRACT ;;
*) echo "[boot0] unknown DRIVER=$DRIVER" >&2; exit 2 ;;
esac
diff --git a/scripts/boot1.sh b/scripts/boot1.sh
@@ -58,9 +58,9 @@ case "$DRIVER" in
export PLATFORM IMAGE ;;
seed)
[ "$ARCH" = "aarch64" ] || { echo "[boot1] DRIVER=seed: aarch64 only" >&2; exit 2; }
- KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ KERNEL_IMAGE=$ROOT/build/$ARCH/boot6/kernel.elf
EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
- [ -f "$KERNEL_IMAGE" ] || { echo "[boot1] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot1] missing $KERNEL_IMAGE — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
export KERNEL_IMAGE EXTRACT ;;
*) echo "[boot1] unknown DRIVER=$DRIVER" >&2; exit 2 ;;
esac
diff --git a/scripts/boot2.sh b/scripts/boot2.sh
@@ -66,9 +66,9 @@ case "$DRIVER" in
export PLATFORM IMAGE ;;
seed)
[ "$ARCH" = "aarch64" ] || { echo "[boot2] DRIVER=seed: aarch64 only" >&2; exit 2; }
- KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ KERNEL_IMAGE=$ROOT/build/$ARCH/boot6/kernel.elf
EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
- [ -f "$KERNEL_IMAGE" ] || { echo "[boot2] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot2] missing $KERNEL_IMAGE — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
export KERNEL_IMAGE EXTRACT ;;
*) echo "[boot2] unknown DRIVER=$DRIVER" >&2; exit 2 ;;
esac
diff --git a/scripts/boot3.sh b/scripts/boot3.sh
@@ -85,9 +85,9 @@ if [ "$DRIVER" = podman ] && ! podman image exists "$IMAGE"; then
fi
if [ "$DRIVER" = seed ]; then
[ "$ARCH" = aarch64 ] || { echo "[boot3] DRIVER=seed: aarch64 only" >&2; exit 2; }
- KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ KERNEL_IMAGE=$ROOT/build/$ARCH/boot6/kernel.elf
EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
- [ -f "$KERNEL_IMAGE" ] || { echo "[boot3] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot3] missing $KERNEL_IMAGE — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
export KERNEL_IMAGE EXTRACT
fi
export IMAGE PLATFORM DRIVER
diff --git a/scripts/boot4.sh b/scripts/boot4.sh
@@ -121,9 +121,9 @@ if [ "$DRIVER" = podman ] && ! podman image exists "$IMAGE"; then
fi
if [ "$DRIVER" = seed ]; then
[ "$ARCH" = aarch64 ] || { echo "[boot4] DRIVER=seed: aarch64 only" >&2; exit 2; }
- KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ KERNEL_IMAGE=$ROOT/build/$ARCH/boot6/kernel.elf
EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
- [ -f "$KERNEL_IMAGE" ] || { echo "[boot4] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot4] missing $KERNEL_IMAGE — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
export KERNEL_IMAGE EXTRACT
fi
export IMAGE PLATFORM DRIVER
diff --git a/scripts/boot5.sh b/scripts/boot5.sh
@@ -103,9 +103,9 @@ if [ "$DRIVER" = podman ] && ! podman image exists "$IMAGE"; then
-f scripts/Containerfile.empty scripts/
fi
if [ "$DRIVER" = seed ]; then
- KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ KERNEL_IMAGE=$ROOT/build/$ARCH/boot6/kernel.elf
EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
- [ -f "$KERNEL_IMAGE" ] || { echo "[boot5] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot5] missing $KERNEL_IMAGE — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
export KERNEL_IMAGE EXTRACT
fi
export IMAGE PLATFORM DRIVER
diff --git a/scripts/boot6-gen-runscm.sh b/scripts/boot6-gen-runscm.sh
@@ -0,0 +1,84 @@
+#!/bin/sh
+## boot6-gen-runscm.sh — emit run.scm driving tcc3 to build seed-kernel.
+##
+## tcc3 -c each translation unit (kernel.S, kernel.c, mem.c) → .o, then
+## tcc3 links + emits a flat arm64 boot Image directly (no `ld -T
+## kernel.lds`, no objcopy). The link line is three tcc flags:
+##
+## -nostdlib -static freestanding link, no startfiles
+## -Wl,-Ttext=0x40080000 base address (arm64 RAM_BASE +
+## text_offset; QEMU's `-kernel` loads
+## us here)
+## -Wl,--oformat=binary flat-bytes output (objcopy -O binary
+## equivalent). Required because QEMU's
+## `-kernel <ELF>` path doesn't run the
+## arm64 boot wrapper that puts DTB in
+## x0 — only the flat-Image path does.
+## Detection is by `ARM\x64` magic at
+## file offset 0x38, which is in
+## kernel.S's Image header at the top of
+## `.text`.
+##
+## Everything else the kernel needs from the linker is supplied by
+## conventions tcc3 already honors:
+##
+## _start hard-coded entry symbol; kernel.S's arm64 Image
+## header is named `_start` (not `_head`) so this works
+## out of the box.
+## __bss_start auto-defined at the start of merged .bss via the
+## `bss-start-symbol` simple-patch.
+## _end auto-defined by stock tcc at the end of merged .bss
+## (tcc_add_linker_symbols). kernel.S uses it to
+## bracket the bss-zero loop and to compute the
+## Image-header `image_size` field; kernel.c uses it
+## to place the kernel heap above the loaded image.
+##
+## Usage: boot6-gen-runscm.sh <arch> <out.scm>
+
+set -eu
+[ "$#" -eq 2 ] || { echo "usage: $0 <arch> <out.scm>" >&2; exit 2; }
+ARCH=$1; OUT=$2
+[ "$ARCH" = aarch64 ] || { echo "boot6-gen: only aarch64 supported" >&2; exit 2; }
+
+# Kernel CFLAGS — freestanding, static, no host startfiles. We omit gcc's
+# -mgeneral-regs-only (no tcc equivalent); kernel.S enables CPACR_EL1.FPEN
+# in stext before the first `bl kmain`, so tcc's SIMD callee-saves in
+# function prologues don't trap.
+KCFLAGS='"-nostdlib" "-ffreestanding" "-static"'
+
+cat > "$OUT" <<EOF
+;; boot6 run.scm — build seed-kernel ELF with tcc3.
+;; Generated by scripts/boot6-gen-runscm.sh; reads use in/, writes out/.
+
+(define (must r tag)
+ (if (and (car r) (= 0 (cdr r)))
+ r
+ (begin
+ (write-string stderr "boot6: step failed: ")
+ (write-string stderr tag)
+ (write-string stderr "\n")
+ (exit 1))))
+
+(write-string stdout "boot6: tcc3 -c kernel.S\n")
+(must (run "in/tcc3" $KCFLAGS "-c" "-o" "out/kernel-asm.o" "in/kernel.S")
+ "kernel.S -> kernel-asm.o")
+
+(write-string stdout "boot6: tcc3 -c kernel.c\n")
+(must (run "in/tcc3" $KCFLAGS "-c" "-o" "out/kernel.o" "in/kernel.c")
+ "kernel.c -> kernel.o")
+
+(write-string stdout "boot6: tcc3 -c mem.c\n")
+(must (run "in/tcc3" $KCFLAGS "-c" "-o" "out/mem.o" "in/mem.c")
+ "mem.c -> mem.o")
+
+(write-string stdout "boot6: tcc3 link kernel.elf (flat Image)\n")
+(must (run "in/tcc3" "-nostdlib" "-static"
+ "-Wl,-Ttext=0x40080000"
+ "-Wl,--oformat=binary"
+ "-o" "out/kernel.elf"
+ "out/kernel-asm.o" "out/kernel.o" "out/mem.o")
+ "link kernel.elf")
+
+(write-string stdout "boot6: ALL-OK\n")
+(exit 0)
+EOF
diff --git a/scripts/boot6.sh b/scripts/boot6.sh
@@ -0,0 +1,100 @@
+#!/bin/sh
+## boot6.sh — build the seed-kernel ELF with boot4's tcc3.
+##
+## Drives tcc3 to compile + link the seed kernel directly into a
+## QEMU-bootable arm64 ELF: no `ld -T kernel.lds`, no objcopy. The link
+## step uses the minimal flag set the kernel actually needs (see
+## scripts/boot6-gen-runscm.sh for the rationale of each flag), notably
+## --section-bracket=.bss:__bss_start:__bss_end which replaces the
+## kernel.lds bracket assignments — the only genuine link-time service
+## the kernel still requires.
+##
+## ─── Inputs ──────────────────────────────────────────────────────────
+## build/$ARCH/boot4/tcc3 — boot4's verified self-host tcc
+## (compiler + linker)
+## build/$ARCH/boot2/scheme1 — driver runtime
+## seed-kernel/kernel.S — boot stub, vector table, asm thunks,
+## trailing 64 KB stack reserved as
+## plain `.bss` (kstack_top is the end
+## label of that reservation)
+## seed-kernel/kernel.c — DTB parse, MMU bring-up, syscalls,
+## virtio-blk, tmpfs, ELF loader
+## tcc-cc/mem.c — memcpy/memset/memmove/memcmp
+##
+## ─── Tools ────────────────────────────────────────────────────────────
+## In container: scratch + busybox (boot2-empty:$ARCH).
+## scheme1 evaluates run.scm against the flat staging
+## root (cwd=/work for podman, cwd=/ for the seed
+## kernel). Same run.scm drives both.
+##
+## ─── Outputs ─────────────────────────────────────────────────────────
+## build/$ARCH/boot6/kernel.elf — final kernel ELF; QEMU loads its
+## PT_LOAD segment at 0x40080000 and
+## jumps to _head (the arm64 Image
+## header), same boot path as the
+## gcc-built flat Image.
+##
+## Usage: scripts/boot6.sh <arch>
+## <arch>: aarch64 only. boot6 has no amd64/riscv64 analogue — the seed
+## kernel is arm64-specific.
+
+set -eu
+
+usage() { echo "usage: $0 <aarch64>" >&2; exit 2; }
+[ "$#" -eq 1 ] || usage
+ARCH=$1
+[ "$ARCH" = aarch64 ] || { echo "[boot6] only aarch64 supported (got $ARCH)" >&2; exit 2; }
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+cd "$ROOT"
+
+DRIVER=${DRIVER:-podman}
+PLATFORM=linux/arm64
+IMAGE=boot2-empty:$ARCH
+BOOT2=build/$ARCH/boot2
+BOOT4=build/$ARCH/boot4
+OUT=build/$ARCH/boot6
+STAGE=build/$ARCH/.boot6-stage
+
+# ── prerequisites ─────────────────────────────────────────────────────
+[ -x "$BOOT4/tcc3" ] || { echo "[boot6 $ARCH] missing $BOOT4/tcc3 (run scripts/boot4.sh $ARCH)" >&2; exit 1; }
+[ -x "$BOOT2/scheme1" ] || { echo "[boot6 $ARCH] missing $BOOT2/scheme1 (run scripts/boot2.sh $ARCH)" >&2; exit 1; }
+for f in seed-kernel/kernel.S seed-kernel/kernel.c tcc-cc/mem.c; do
+ [ -f "$f" ] || { echo "[boot6 $ARCH] missing $f" >&2; exit 1; }
+done
+
+# ── ensure container image exists (podman driver only) ────────────────
+if [ "$DRIVER" = podman ] && ! podman image exists "$IMAGE"; then
+ echo "[boot6 $ARCH] building $IMAGE"
+ podman build --no-cache --platform "$PLATFORM" -t "$IMAGE" \
+ -f scripts/Containerfile.empty scripts/
+fi
+if [ "$DRIVER" = seed ]; then
+ KERNEL_IMAGE=$ROOT/build/$ARCH/boot6/kernel.elf
+ EXTRACT=$ROOT/seed-kernel/scripts/extract-blk.sh
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot6] missing $KERNEL_IMAGE — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
+ export KERNEL_IMAGE EXTRACT
+fi
+export IMAGE PLATFORM DRIVER
+
+# ── stage inputs and run scheme1 + run.scm under $DRIVER ──────────────
+. scripts/lib-runscm.sh
+runscm_init "$STAGE" "$OUT"
+
+RUNSCM=$STAGE/run.scm
+scripts/boot6-gen-runscm.sh "$ARCH" "$RUNSCM"
+echo "[boot6 $ARCH] generated run.scm: $(wc -l <"$RUNSCM") lines"
+
+runscm_scheme1 "$BOOT2/scheme1"
+runscm_prelude scheme1/prelude.scm
+runscm_runscm "$RUNSCM"
+
+runscm_input tcc3 "$BOOT4/tcc3"
+runscm_input kernel.S seed-kernel/kernel.S
+runscm_input kernel.c seed-kernel/kernel.c
+runscm_input mem.c tcc-cc/mem.c
+
+runscm_export kernel.elf
+runscm_run 1200
+
+echo "[boot6 $ARCH/$DRIVER] OK -> $OUT/kernel.elf ($(wc -c <"$OUT/kernel.elf") bytes)"
diff --git a/scripts/seed-accept-boot34.sh b/scripts/seed-accept-boot34.sh
@@ -4,9 +4,10 @@
## bootN/'s podman-built artefacts.
##
## Prereqs (build first):
-## - seed-kernel/build/Image (`make` in seed-kernel/)
-## - build/aarch64/boot{0,1,2,3,4}/ (run scripts/bootN.sh aarch64
-## under DRIVER=podman to populate references)
+## - build/aarch64/boot6/kernel.elf (run ./scripts/boot.sh aarch64
+## under DRIVER=podman to produce the tcc-built seed kernel)
+## - build/aarch64/boot{0,1,2,3,4}/ (same boot.sh run populates these
+## references)
##
## What it does:
## 1. Stash existing podman-built build/aarch64/boot{3,4}/ as ref/.
@@ -25,8 +26,8 @@ ARCH=aarch64
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
-KERNEL=seed-kernel/build/Image
-[ -f "$KERNEL" ] || { echo "missing $KERNEL — make in seed-kernel/" >&2; exit 1; }
+KERNEL=build/$ARCH/boot6/kernel.elf
+[ -f "$KERNEL" ] || { echo "missing $KERNEL — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
[ -x build/$ARCH/boot3/tcc0 ] || { echo "build/$ARCH/boot3/tcc0 missing — run scripts/boot3.sh aarch64" >&2; exit 1; }
REF=build/$ARCH/.seed-ref
diff --git a/scripts/seed-accept-boot5.sh b/scripts/seed-accept-boot5.sh
@@ -4,9 +4,10 @@
## artefacts. Mirrors scripts/seed-accept-boot34.sh.
##
## Prereqs (build first):
-## - seed-kernel/build/Image (`make` in seed-kernel/)
-## - build/aarch64/boot{0..5}/ (run scripts/bootN.sh aarch64
-## under DRIVER=podman to populate references)
+## - build/aarch64/boot6/kernel.elf (run ./scripts/boot.sh aarch64
+## under DRIVER=podman to produce the tcc-built seed kernel)
+## - build/aarch64/boot{0..5}/ (same boot.sh run populates these
+## references)
##
## What it does:
## 1. Stash existing podman-built build/aarch64/boot5/ as ref/.
@@ -23,8 +24,8 @@ ARCH=aarch64
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
-KERNEL=seed-kernel/build/Image
-[ -f "$KERNEL" ] || { echo "missing $KERNEL — make in seed-kernel/" >&2; exit 1; }
+KERNEL=build/$ARCH/boot6/kernel.elf
+[ -f "$KERNEL" ] || { echo "missing $KERNEL — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
[ -d build/$ARCH/boot5 ] || { echo "build/$ARCH/boot5 missing — run scripts/boot5.sh aarch64" >&2; exit 1; }
for f in libc.a crt1.o crti.o crtn.o hello; do
[ -e build/$ARCH/boot5/$f ] || { echo "build/$ARCH/boot5/$f missing — run scripts/boot5.sh aarch64" >&2; exit 1; }
diff --git a/scripts/seed-accept.sh b/scripts/seed-accept.sh
@@ -23,13 +23,13 @@ ARCH=aarch64
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
-KERNEL=seed-kernel/build/Image
+KERNEL=build/$ARCH/boot6/kernel.elf
EXTRACT=seed-kernel/scripts/extract-blk.sh
SCHEME1=build/$ARCH/boot2/scheme1
CATM=build/$ARCH/boot2/catm
PRELUDE=scheme1/prelude.scm
-[ -f "$KERNEL" ] || { echo "missing $KERNEL — run 'make' in seed-kernel/" >&2; exit 1; }
+[ -f "$KERNEL" ] || { echo "missing $KERNEL — run ./scripts/boot.sh $ARCH (default DRIVER=podman) first" >&2; exit 1; }
[ -x "$SCHEME1" ] || { echo "missing $SCHEME1 — run boot2 first" >&2; exit 1; }
[ -x "$CATM" ] || { echo "missing $CATM — run boot2 first" >&2; exit 1; }
diff --git a/scripts/simple-patches/tcc-0.9.26/bss-start-symbol.after b/scripts/simple-patches/tcc-0.9.26/bss-start-symbol.after
@@ -0,0 +1,13 @@
+ /* `__bss_start` mirror of the auto-defined `_end` (= end of .bss).
+ * Together they let a kernel-style image zero its .bss without an
+ * ld-script section bracket. Stock tcc 0.9.26 only synthesizes
+ * `_end`, so freestanding asm that wants a start-of-bss anchor
+ * either needs an ld script or this patch. */
+ set_elf_sym(symtab_section,
+ 0, 0,
+ ELFW(ST_INFO)(STB_GLOBAL, STT_NOTYPE), 0,
+ bss_section->sh_num, "__bss_start");
+ set_elf_sym(symtab_section,
+ bss_section->data_offset, 0,
+ ELFW(ST_INFO)(STB_GLOBAL, STT_NOTYPE), 0,
+ bss_section->sh_num, "_end");
diff --git a/scripts/simple-patches/tcc-0.9.26/bss-start-symbol.before b/scripts/simple-patches/tcc-0.9.26/bss-start-symbol.before
@@ -0,0 +1,4 @@
+ set_elf_sym(symtab_section,
+ bss_section->data_offset, 0,
+ ELFW(ST_INFO)(STB_GLOBAL, STT_NOTYPE), 0,
+ bss_section->sh_num, "_end");
diff --git a/scripts/stage1-flatten.sh b/scripts/stage1-flatten.sh
@@ -149,6 +149,11 @@ apply_our_patch lex-char-unsigned "$SRC/tccpp.c"
apply_our_patch lp64-long-constant "$SRC/tccpp.c"
apply_our_patch elfinterp-stub "$SRC/tccelf.c"
+# Auto-define `__bss_start` alongside tcc's existing `_end` symbol so a
+# freestanding image (kernel.S) can zero its .bss with start/end anchors
+# without an ld script. Mirrors the live-bootstrap convention.
+apply_our_patch bss-start-symbol "$SRC/tccelf.c"
+
# x86_64 static-link PLT32 collapse: under BOOTSTRAP we force
# static_link=1 with no .dynamic / no PT_INTERP, so the runtime linker
# never fills the PLT's GOT slots. Upstream tcc 0.9.26 only collapses
diff --git a/seed-kernel/kernel.S b/seed-kernel/kernel.S
@@ -2,15 +2,27 @@
* plus C-callable thunks for ops that can't be expressed in plain C
* (sysreg msr/mrs, barriers, cache/TLB ops, PSCI conduits, cpu pause). */
-.section .head.text, "ax"
-.globl _head
-_head:
+.section .text, "ax"
+.globl _start
+_start:
/* arm64 Image header (Documentation/arm64/booting.rst).
- * code0 must be a valid instruction (a branch, in our case). */
+ * code0 must be a valid instruction (a branch, in our case).
+ *
+ * The header lives at the top of `.text` (not a separate
+ * `.head.text` section) because we link kernel.S first, so its
+ * `.text` content lands at file offset 0 — QEMU's arm64 boot
+ * protocol detects this image by `ARM\x64` magic at offset 0x38,
+ * which is the `.ascii "ARM\x64"` line below. No ld-script
+ * section ordering required.
+ *
+ * Entry symbol is `_start` (not `_head`) so a script-less linker
+ * — tcc3 in particular — picks us up via its hard-coded default.
+ * Image-size end marker is `_end`, which both gnu ld and tcc3
+ * auto-define at the end of `.bss`. */
b stext
.long 0
.quad 0x80000 /* text_offset (preferred load offset within RAM) */
- .quad _image_end - _head /* image_size */
+ .quad _end - _start /* image_size */
.quad 0xa /* flags: 4K pages, anywhere in physmem, LE */
.quad 0
.quad 0
@@ -67,9 +79,11 @@ in_el1:
msr cpacr_el1, x9
isb
- /* Zero BSS. */
+ /* Zero BSS. `__bss_start` and `_end` are auto-defined by both
+ * gnu ld (via the kernel.lds bracket) and tcc3 (via the
+ * `bss-start-symbol` simple-patch + tcc's existing `_end` def). */
ldr x1, =__bss_start
- ldr x2, =__bss_end
+ ldr x2, =_end
1: cmp x1, x2
b.ge 2f
str xzr, [x1], #8
@@ -453,3 +467,17 @@ arm64_psci_call:
.Lpsci_smc:
smc #0
ret
+
+
+/* ─── Kernel stack ────────────────────────────────────────────────────────
+ * 64 KB reserved as plain `.bss`. Keeping the stack inside `.bss` (rather
+ * than a custom `.stack` section) means the only link-time service the
+ * kernel needs is bracketing the merged `.bss` with __bss_start/__bss_end
+ * — no custom output-section ordering, no link-script reservations. The
+ * stext bss-zero loop runs before any stack write (it only touches x1/x2),
+ * so zeroing our own stack region first is safe. */
+.section .bss, "aw", %nobits
+.balign 16
+.skip 0x10000
+.globl kstack_top
+kstack_top:
diff --git a/seed-kernel/kernel.lds b/seed-kernel/kernel.lds
@@ -6,15 +6,17 @@
* which is PC-relative), so the link base mostly affects symbol values.
*/
-ENTRY(_head)
+ENTRY(_start)
SECTIONS {
. = 0x40080000;
- .head.text : {
- KEEP(*(.head.text))
- }
-
+ /* kernel.S lives entirely in `.text` (the arm64 Image header sits
+ * at its top), and it is linked first, so its contributions land at
+ * file offset 0 — exactly where QEMU's arm64 boot protocol expects
+ * the `ARM\x64` magic at byte 0x38. No `.head.text` indirection
+ * needed; a script-less linker (tcc3) reaches the same layout via
+ * its default input-order section concatenation. */
.text : ALIGN(8) {
*(.text .text.*)
}
@@ -27,23 +29,20 @@ SECTIONS {
*(.data .data.*)
}
+ /* `.bss` includes the 64KB kernel stack, which kernel.S reserves as
+ * plain `.bss` content ending at `kstack_top`. kernel.S references
+ * `__bss_start` and `_end` to bracket the bss-zero loop; both names
+ * are de-facto linker conventions (gnu ld defines them via this
+ * script, and tcc3 synthesizes them automatically via the
+ * bss-start-symbol simple-patch + its built-in `_end` def). */
.bss : ALIGN(16) {
__bss_start = .;
*(.bss .bss.*)
*(COMMON)
. = ALIGN(16);
- __bss_end = .;
- }
-
- /* 64KB kernel stack */
- .stack : ALIGN(16) {
- kstack_bottom = .;
- . += 0x10000;
- kstack_top = .;
}
_end = .;
- _image_end = .;
/DISCARD/ : {
*(.note.*) *(.comment) *(.eh_frame)