boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit 66f47611dd938c3f3a445b94fe4e42df02f8d175
parent ee3139b5876283352651f76efaaa65a93bdfcf10
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed,  6 May 2026 13:03:14 -0700

rm old docs

Diffstat:
Ddocs/PLAN.md | 517-------------------------------------------------------------------------------
Ddocs/SEED-AMD64-TODO.md | 122-------------------------------------------------------------------------------
Ddocs/SEED-VIRTIO-BLK.md | 359-------------------------------------------------------------------------------
Ddocs/TCC-ARM64-ASM.md | 314-------------------------------------------------------------------------------
4 files changed, 0 insertions(+), 1312 deletions(-)

diff --git a/docs/PLAN.md b/docs/PLAN.md @@ -1,517 +0,0 @@ -# Cleanup & Audit Plan - -The mechanical goal is met: hex0-seed → … → tcc3 → musl → seed-kernel runs on -{aarch64, amd64, riscv64} via two drivers (podman, seed). This plan covers -the next pass: making the result auditable and uniform. - -## Decisions - -1. **Output layout**: `build/<arch>/<driver>/...` -2. **Makefile vs bootN scripts**: replace the parallel Makefile recipes - (`make m1pp`, `make hex2pp`, `make scheme1`, etc.) with rules that drive - the `bootN.sh` scripts. One pipeline, one layout. -3. **Reproducibility criterion**: every `boot{N}/` output artifact must be - byte-identical between `DRIVER=podman` and `DRIVER=seed`, per arch. -4. **Source preparation is a separate up-front host stage.** All flattening, - patching, unpacking, calibration happens once into a canonical generated - source tree. Boot stages copy/reference from it; they do no source prep - themselves. -5. **Path-based Makefile deps.** `make build/aarch64/seed/boot6/Image` - walks the dependency chain — no separate `boot0`/`boot1`/… phony - targets driving stages. Outputs are the targets. -6. **Tests live in their own Makefile** (`tests/Makefile`), invoked from - the top level but not commingled with the build pipeline. -7. **Consistency over backcompat.** Patch, refactor, rename, rewrite as - needed. tcc patches are in scope when they buy uniformity. -8. **Phase order**: A6 → A3 → A0 → A4 → AT → AX → A2 → A5. Each phase - lands on a clean base from the previous one. AT (tcc patches) and AX - (tests Makefile) are largely independent of the others and can slot in - wherever convenient. - -## Phases - -### [DONE] A6. Hoist driver/arch boilerplate - -**Goal.** One source of truth for arch→platform mapping, driver dispatch, -prereq checks, and log prefixes. Stage-N scripts shrink to the parts that -are actually stage-specific. - -**Touch list.** -- New: `scripts/lib-arch.sh`. Exports `PLATFORM`, `KERNEL_NAME`, - `KERNEL_IMAGE`, `MUSL_ARCH` from `$ARCH`. Single source. -- `scripts/lib-pipeline.sh`, `scripts/lib-runscm.sh`: source `lib-arch.sh` - in `_init_*`; pull `case "$DRIVER"` validation + image build / kernel - check up here. -- `scripts/boot{0..6}.sh`: delete duplicated `case $ARCH` and - `case $DRIVER` blocks; delete prereq `[ -x "$BOOT(N-1)/x" ] || die` - blocks; replace with `require_prev hex2 catm M0` style helper. -- All scripts: standardize log prefix to `[bootN/$driver/$arch]`. -- `scripts/boot.sh`: same. - -**Validation.** `scripts/boot.sh aarch64` end-to-end on both drivers. - ---- - -### [DONE] A3. Per-driver output trees - -**Goal.** `build/<arch>/<driver>/...` everywhere. Kill the -`build/.seed-bootstrap/` shuffle in `boot.sh`. Two drivers can coexist on -disk; nothing gets clobbered when you switch. - -**Touch list.** -- `scripts/lib-pipeline.sh`, `scripts/lib-runscm.sh`: change `OUT`/`STAGE` - derivation to insert `$DRIVER`. -- `scripts/boot{0..6}.sh`: every `BOOT(N-1)=build/$ARCH/boot(N-1)` → - `build/$ARCH/$DRIVER/boot(N-1)`. -- `scripts/boot.sh`: drop the `.seed-bootstrap` stash/restore. The boot6 - kernel that the seed driver uses now lives at - `build/<arch>/podman/boot6/{Image|kernel.elf}` and is referenced - directly when running seed. -- `seed-kernel/run.sh`: kernel-path arg / env still works; just point - callers at the new location. -- `Makefile`: every `OUT_DIR := build/$(ARCH)` → include `$(DRIVER)`. -- `.gitignore` if it pins `build/<arch>/`. -- Top-level `Makefile` comment block (output-layout doc). - -**Validation.** Both `DRIVER=podman scripts/boot.sh aarch64` and -`DRIVER=seed scripts/boot.sh aarch64` produce coherent trees side by side. - ---- - -### [DONE] A0. Canonical generated source tree (host source-prep) - -**Goal.** All host-side source preparation happens once, up front, into a -single canonical tree at `build/<arch>/src/`. This tree is the audit -basis and the only thing boot stages read for source. Boot stages do no -flattening, no unpacking, no patching, no calibration. - -**Layout.** -``` -build/<arch>/src/ - bin/ # binary inputs that aren't built by a stage - hex0-seed # (vendored seed only) - src/ # everything textual - vendor-seed/ # ELF.hex2, *.hex0, *.hex1, *.hex2 (vendored) - M1pp/ # M1pp.P1 - hex2pp/ # hex2pp.P1 - P1/ # P1.M1pp, P1-<arch>.M1, P1-<arch>.M1pp, - # P1pp.P1pp, entry-*.P1pp, elf-end.P1pp - catm/ # catm.P1pp - scheme1/ # scheme1.P1pp, prelude.scm - cc/ # cc.scm, main.scm - tcc/ # tcc.flat.c, stdarg-bridge.h, - # tcc-0.9.26-…/{include,lib}/ tree - libc/ # libc.flat.c (mes-libc flattened) - musl/ # filtered musl-1.2.5 tree (overrides - # merged, deletes applied), per-arch - # skip-list, generated alltypes.h / - # syscall.h - kernel/ # seed-kernel kernel.c, arch/<arch>/*, - # user/* - test-fixtures/ # only the slice tests need at runtime - # (host runner reads tests/ directly) -``` - -The `bin/` vs `src/` split is established here; downstream stages -inherit it (their `$STAGE/in/{bin,src}/` is a copy-or-symlink view of -the right slice of this tree). - -**What moves into A0.** -- `scripts/stage1-flatten.sh` (tcc flatten) — runs once, output to - `build/<arch>/src/src/tcc/`. -- `scripts/libc-flatten.sh` (mes-libc flatten) — runs once, output to - `build/<arch>/src/src/libc/`. -- `scripts/musl-vendor.sh` (musl unpack + overrides + deletes) — runs - once, output to `build/<arch>/src/src/musl/`. -- `scripts/boot5-calibrate.sh` (musl skip-list per arch) — runs once, - output as part of `src/musl/skip.txt`. If skip-list is committed - upstream (`vendor/upstream/musl-1.2.5-skip-<arch>.txt`), copy it; else - run calibration (which itself depends on boot4 — see below). -- All the per-stage `.stage/in/` materialization moves out of - `lib-pipeline.sh` / `lib-runscm.sh` and becomes a thin "copy from - `build/<arch>/src/`" step. - -**Calibration ordering.** `boot5-calibrate.sh` needs a working tcc3, so -the canonical src tree is built in two passes: **A0a** (everything that -doesn't need a compiler) before boot0, **A0b** (musl filter using the -calibration result) after boot4. Both write into `build/<arch>/src/`. -Document the split clearly; the Makefile encodes it via deps. - -**Touch list.** -- New: `scripts/prep-src.sh`. Orchestrates A0a (vendored copy, tcc flatten, - libc flatten, musl unpack+merge). -- New: `scripts/prep-musl.sh`. The A0b half (run/copy calibration, apply - filter, snapshot result into `src/musl/`). -- `scripts/boot{3,4,5,6}.sh`: delete the auto-invoke flatten/calibrate - blocks; rely on `build/<arch>/src/` being present. -- `scripts/lib-pipeline.sh`, `scripts/lib-runscm.sh`: add a - `stage_input_from_src <subpath> <bin|src>` helper that pulls from the - canonical tree. -- `scripts/boot{0..6}.sh`: every `pipeline_input` / `runscm_input` call - switches to the new helper. Inputs become declarative. -- `scripts/boot{4,5,6}-gen-runscm.sh`: paths emitted into `run.scm` use - the canonical layout. -- Retire `WORK_SUBPATH` env in `scripts/boot-build-p1pp.sh` and similar - ad-hoc input plumbing. - -**Validation.** `tree build/<arch>/src/` is reviewable in one sitting. -Re-running any boot stage twice produces byte-identical outputs. The -flatten/musl scripts are no longer triggered from within a boot stage. - ---- - -### [DONE] A4. Makefile drives `bootN.sh` with path-based deps - -**Goal.** Outputs are targets. `make build/aarch64/seed/boot6/Image` -walks the chain: prep-src → boot0 → boot1 → … → boot6. - -**Status note.** Implemented with single-target rules anchored on per- -stage `.stamp` files (real artifacts depend on the stamp via empty -recipes). GNU make 4.4 does not propagate "must remake of prereq" -through grouped explicit targets (`&:`) across multi-level chains, so -the rule shape sketched below — combining real outputs and `.stamp` -into one grouped target — would skip the boot6 rebuild after a deep -source change. The single-target form keeps the stamp the recipe peg -and makes the artifact files declarative-only, which restores correct -propagation. The `tests/Makefile` include and the `boot-build-p1*.sh` -retirement are deferred to AX, which migrates the test infrastructure -that still depends on them. - -**Rule shape.** -``` -build/$(ARCH)/$(DRIVER)/boot1/M1pp \ -build/$(ARCH)/$(DRIVER)/boot1/hex2pp: \ - build/$(ARCH)/$(DRIVER)/boot0/hex2 \ - build/$(ARCH)/$(DRIVER)/boot0/M0 \ - build/$(ARCH)/$(DRIVER)/boot0/catm \ - build/$(ARCH)/src/.stamp \ - scripts/boot1.sh scripts/lib-pipeline.sh scripts/lib-arch.sh - scripts/boot1.sh $(ARCH) -``` - -The `.stamp` files in `build/<arch>/src/` and each `build/<arch>/<driver>/boot{N}/` -are the make-rule pegs. Real outputs are listed as additional targets so -single-binary builds work. - -**Touch list.** -- `Makefile`: rewrite the body. Per-stage rule blocks: src-prep, boot0, - boot1, …, boot6. Each lists its real outputs and depends on the - previous stage's outputs plus the relevant scripts and library files. -- `Makefile`: drop `m1pp`/`hex2pp`/`scheme1`/`cc`/`tcc-boot2`/`tcc-tcc`/ - `tcc-tcc-tcc` aliases entirely. Path is the name. -- `Makefile`: drop dead vars (`TOOLS_CATM`); single tcc-version var. -- `Makefile`: hoist `cloc` (~14 lines) into `scripts/count-lines.sh`. -- `Makefile`: include `tests/Makefile` for test targets (see AX). -- `Makefile`: top-level convenience targets accept `DRIVER` and `ARCH`, - expand to the right path. `make all` builds everything. -- `BOOT6_TIMEOUT` env in `boot6.sh` (parity with boot3/4/5). -- Retire `boot-build-p1.sh` and `boot-build-p1pp.sh` if the boot-script - path subsumes them (it should — they're container-side helpers for - the old Makefile recipes). - -**Validation.** `make build/aarch64/podman/boot6/Image` from a clean -tree builds the full chain. `make build/aarch64/podman/boot1/M1pp` only -builds prep-src + boot0 + boot1. `touch scripts/boot4.sh && make -build/aarch64/podman/boot6/Image` rebuilds boot4–6 only. Output bytes -unchanged from pre-A4. - ---- - -### AT. tcc patches for uniformity - -**Goal.** Eliminate per-arch workarounds that exist only because tcc -0.9.26 is incomplete. After AT, every arch is on the same footing in -seed-kernel and the build pipeline. - -The four subitems are independent — none gates another. AT.3 was -previously claimed to gate AT.4 (per the "once asm-support patches -land" wording); audit found that wrong (AT.4 is a pure refactor). - -**In scope.** - -- **AT.1. amd64 `.quad` 64-bit-literal truncation.** `seed-kernel/arch/amd64/kernel.S:406–413` - encodes the GDT as `.long lo, hi` pairs because `.quad 0x00af9a000000ffff` - truncates. Root cause is **not** in tcc's `gen_le64` (that path is 64-bit - clean); it's in mes-libc's `vendor/mes-libc/mes/abtol.c:29`, which - accumulates `strtoull` into an `int`, so any 64-bit hex literal in - `tccasm.c:asm_expr_unary` loses its high bits at parse time. Two fixes: - - **(preferred)** one-line edit in `abtol.c` (`int i` → `long long i`). - Repairs every `strto*` caller, not just asm. - - **(alternative)** new patch `tccasm-asm-expr-parse-number.{before,after}` - that inlines a 64-bit hex/oct/dec accumulator in `asm_expr_unary`, - bypassing `strtoull`. - Then revert `kernel.S:406–413` to plain `.quad` GDT entries. (`mmu.c` - has no workaround — uses C-side `u64` constants — and needs no change.) - Existing memory `project_tcc_arm64_svcul_truncation.md` describes an - unrelated arm64 codegen-side truncation already patched in - `scripts/simple-patches/tcc-0.9.26/arm64-svcul-no-truncate*`; track - separately. - -- **AT.2. `.note.*` SHT_NOTE / PT_NOTE.** [done] tcc emitted - `.note.*` sections as `SHT_PROGBITS` and never wrote a `PT_NOTE` - phdr, forcing the post-link `seed-kernel/scripts/elf-pvh-note.c` - tool to rewrite the ELF for amd64 PVH boot. Three patches landed in - `scripts/simple-patches/tcc-0.9.26/`: - - `note-section-sht-note.{before,after}` — in `tccelf.c` - `find_section()`, name-prefix-check `.note*` → create as `SHT_NOTE`. - - `pt-note-phdr.{before,after}` — in `tccelf.c:elf_output_file`, - gate `phnum++` on "any `SHT_NOTE+SHF_ALLOC` section exists" (else - aarch64/riscv64 phnum perturbs and breaks the A5 reproducibility - target); compute `min(sh_offset)` / `max(sh_offset+sh_size)` - over those sections; fill the leftover `PT_NULL` slot with one - `PT_NOTE` after `fill_unloadable_phdr` runs. - - `load-obj-accept-sht-note.{before,after}` — in - `tccelf.c:tcc_load_object_file`, add `SHT_NOTE` to the - accepted-section-type list. Strict pair with the find_section - change: without it, .o files emitted by the patched compiler - drop their `.note*` sections during link, then the subsequent - `.rela.note.*` merge derefs `sm_table[].s == NULL` and segfaults - the linker. (Found during validation: aarch64 was byte-identical, - amd64 boot6 link crashed under QEMU until this third patch.) - Then deleted `seed-kernel/scripts/elf-pvh-note.c`, the - `cp seed-kernel/scripts/elf-pvh-note.c` block in - `scripts/prep-src.sh`, the `if [ "$ARCH" = amd64 ]` block in - `scripts/boot6.sh`, and the amd64 PVH-fixup block in - `scripts/boot6-gen-runscm.sh`. `seed-kernel/arch/amd64/kernel.lds` - is unchanged (consumed by clang+ld.lld, not tcc). - -- **AT.3. riscv64 inline-asm — audit + memory update only, no patch.** - Audit found `riscv64-asm.c:801–819` ships only stubs for - `subst_asm_operand` (loud error), `asm_gen_code` (empty), and - `asm_compute_constraints` (empty) — same silent-drop class as arm64 - per memory `project_tcc_inline_asm_silent_drop.md`. But every - register-asm callsite in the repo is already worked around: musl - syscall/tp/atomics overrides under - `vendor/upstream/musl-1.2.5-overrides/arch/riscv64/`, and 13 externs - in `seed-kernel/arch/riscv64/arch.h` defined in `kernel.S`. There - is no live miscompile to fix. A real Phase-3 inline-asm port (~400+ - LOC, parallel to the unstarted arm64 Phase 3) is well out of scope. - AT.3 work: - - Update `project_tcc_inline_asm_silent_drop.md` to add a riscv64 - section. - - Document in `docs/TCC.md` that riscv64 and arm64 share the - silent-drop bug class until Phase 3 lands. - Three small wins that *don't* need any tcc work and can land here: - inline `arch_mmio_ptr` (pure pointer arithmetic), `arch_system_off` - (one MMIO write), and the `saved_user_sp` accessors in pure C — see - AT.4 for context. - -- **AT.4. arch.h API uniformity (refactor, not inline-asm).** The - previous framing — "amd64/riscv64 use externs, aarch64 inlines" — - was wrong. aarch64 also externs every primitive (arch.h:51–60); the - difference is one layer up: aarch64 exposes a small primitive set - (`sysreg_read/write`, `arm64_barrier(kind)`, `cpu_pause(kind)`, - `arm64_psci_call`) and synthesizes the `arch_*()` API as macros - (arch.h:62–72). amd64/riscv64 spell each `arch_*` as its own - dedicated extern. There is **zero** `__asm__`/inline-asm in any - seed-kernel C file, so AT.4 has no tcc dependency. Refactor: - - amd64: introduce `amd64_fence(kind)`, port-io and msr dispatcher - helpers; collapse the per-API externs in - `seed-kernel/arch/amd64/kernel.S:219–378` into the primitive set; - rewrite `arch_*()` in `arch.h` as macros. - - riscv64: introduce `riscv_csr_read/write(id)`, - `riscv_fence(kind)` (parallel to `sysreg_read`/`arm64_barrier`); - same macro rewrite for `arch_*()`. - - `arch_idle_forever` and `arch_system_off` may stay as their own - externs (don't compress into a `(kind)` dispatcher cleanly) — - aarch64 keeps `arm64_psci_call` in the same shape. - -**Touch list.** -- `vendor/mes-libc/mes/abtol.c` (AT.1 preferred fix) **or** new patch - `scripts/simple-patches/tcc-0.9.26/tccasm-asm-expr-parse-number.*` - (AT.1 alternative). -- `scripts/simple-patches/tcc-0.9.26/note-section-sht-note.*`, - `pt-note-phdr.*`, and `load-obj-accept-sht-note.*` (AT.2). The - patch directory is `scripts/simple-patches/tcc-0.9.26/` (not - `seed-kernel/simple-patches/`, which doesn't exist). -- `scripts/stage1-flatten.sh`: list any newly-added patches in - `apply_our_patch` (around line ~223); regenerate the flattened tree. -- `seed-kernel/arch/amd64/kernel.S`: lines 406–413 (`.quad` GDT - revert, AT.1); lines 219–378 (refactor to primitive set, AT.4). -- `seed-kernel/arch/amd64/arch.h`: rewrite `arch_*()` as macros (AT.4). -- `seed-kernel/arch/riscv64/{kernel.S,arch.h}`: same refactor (AT.4). -- `seed-kernel/arch/riscv64/arch.h`: inline `arch_mmio_ptr`, - `arch_system_off`, `arch_read_user_sp`, `arch_write_user_sp` (AT.3 - side wins). -- `seed-kernel/scripts/elf-pvh-note.c`: delete (AT.2). -- `scripts/prep-src.sh`: drop lines 121–123 staging elf-pvh-note.c - (AT.2). -- `scripts/boot6.sh`: drop lines 80–89 (AT.2). -- `scripts/boot6-gen-runscm.sh`: drop lines 99–117 (AT.2). -- `docs/TCC.md`: document the AT patch set; add the riscv64+arm64 - silent-drop note (AT.3). -- Update `project_tcc_inline_asm_silent_drop.md` to cover riscv64 - (AT.3). Other `project_tcc_*` memories stay as historical record. - -**Validation.** -- AT.1: `readelf -x .data build/amd64/$DRIVER/boot1/M1pp` (or any - amd64 binary using a 64-bit hex literal) shows correct upper bits. - amd64 boots after `.quad` revert. -- AT.2: `readelf -S build/amd64/$DRIVER/boot6/Image` shows `.note.Xen` - type `NOTE`; `readelf -l` shows one `PT_NOTE` covering it. `cmp` - vs the pre-patch post-fixup ELF byte-identical at the affected - offsets. amd64 boots through QEMU PVH `-kernel`. -- AT.2 reproducibility: aarch64/riscv64 `readelf -l` phdr counts - unchanged from pre-patch (proves the SHT_NOTE-presence gate works). -- AT.3: docs+memory updated; no functional change expected. -- AT.4: seed-kernel builds on all three arches with the new macro - layer; amd64/riscv64 byte output unchanged (refactor only). -- All test suites still pass. `boot6` has no amd64-conditional shell - logic. - ---- - -### [DONE] AX. tests/ Makefile - -**Goal.** Tests are a separate concern with their own Makefile. Top-level -build is decoupled from test dispatch. - -**Status note.** Landed as a single phase. Per-suite split happened via -`SUITE=<name>` dispatch (kept to minimize churn) rather than per-target -phony names like `test-cc`; `make test SUITE=cc` and `make test` (all) -both resolve through `tests/Makefile`. The standalone `make -C tests` -entry detects a missing `REPO_ROOT` and re-execs the top-level Makefile -with goal forwarding. - -**Touch list (landed).** -- `tests/Makefile`: includes the legacy test-build rules (M1pp/hex2pp/ - scheme1/cc/tcc-boot2/tcc-tcc/tcc-tcc-tcc/mes-libc) and the suite - dispatch. Top-level `Makefile` sets `REPO_ROOT := $(CURDIR)` and - `include tests/Makefile`. -- `tests/lib-runner.sh`: `discover`, `report`, `show_diff`, `fail`, - `compare_runtime`, `read_expected`. Sourced by `tests/run-suite.sh`. -- Moved `scripts/{run-tests,boot-run-tests,boot-build-p1,boot-build-p1pp, - boot-build-cc,seed-accept*}.sh` → `tests/{run,run-suite,build-p1, - build-p1pp,build-cc,seed-accept}.sh`. The three seed-accept scripts - collapsed into one with a `kernel | boot34 | boot5` mode arg. -- `boot-build-p1{,pp}.sh` retirement was scoped to "move under tests/" - rather than full deletion: they are still pure transformations the - test path needs (single P1pp → ELF), and the bootN.sh chain doesn't - expose an equivalent. -- Test fixtures normalized to `NNN-` prefix everywhere. `cc-cg`, - `cc-libc`, `cc-lex`, `cc-pp`, `cc-util`, `M1pp` migrated NN- → NNN-; - `tests/P1` (was unprefixed) got sequential NNN- prefixes assigned - alphabetically. -- Golden-file convention unified to `.expected` (primary output) / - `.expected-exit` (exit code). `.expected-toks` renamed → `.expected` - in `cc-lex`/`cc-pp`; the suite contract still differentiates by - pipeline (lex/pp pipelines emit token streams to stdout). -- `tests/README.md` documents the per-suite contract, fixture-naming - rule, and how to add fixtures. - -**Validation.** `make test` from repo root green on the default arch. -`make -C tests <target>` delegates back to the parent. `tests/Makefile` -is self-contained when `REPO_ROOT` is exported. - ---- - -### A2. Audit manifest + hashes - -**Goal.** A single command produces `manifest.txt` + `sha256.txt` -covering the canonical source tree and every boot stage's outputs, with -provenance. - -**Notes.** -- The canonical source tree from A0 is the audit basis. A2 adds the - manifest and the hashes; the bytes are already there. -- For host-flattened tcc/libc: snapshot the flattened source (per - decision), record the flatten script's SHA in the manifest line so - the recipe is identified. -- One audit per `<arch>/<driver>` pair. Compare-drivers (A5) diffs two - manifests. - -**Touch list.** -- New: `scripts/build-audit.sh`. Walks `build/<arch>/src/` and - `build/<arch>/<driver>/boot{0..6}/`; emits - `build/<arch>/<driver>/audit/manifest.txt` (one line per file: - `<path> <stage> <role> <origin> <sha256>`) and - `build/<arch>/<driver>/audit/sha256.txt` (flat hash list). -- `Makefile`: `audit` target depending on prep-src + boot0..boot6 for - the active driver. - -**Validation.** `sha256sum -c build/<arch>/<driver>/audit/sha256.txt` -clean. Line counts of `manifest.txt` and `find` agree. - ---- - -### A5. `make compare-drivers` reproducibility target - -**Goal.** Mechanical proof: every `boot{N}/` output artifact is -byte-identical between `DRIVER=podman` and `DRIVER=seed`. - -**Recipe.** -1. `DRIVER=podman make build/$arch/podman/boot6/Image` -2. `DRIVER=seed make build/$arch/seed/boot6/Image` - (the seed driver consumes the podman-built kernel for its QEMU run) -3. `diff -r build/$arch/podman/boot{0..6}/ build/$arch/seed/boot{0..6}/` -4. `diff build/$arch/{podman,seed}/audit/manifest.txt` (provenance lines - may differ; hashes must match for shared artifacts). - -**Touch list.** -- New: `scripts/compare-drivers.sh`. -- `Makefile`: `compare-drivers` target wrapping it. - -**Pre-step: determinism audit.** Find and eliminate non-determinism -sources before declaring success. Likely suspects: -- ELF timestamps (any `__DATE__`/`__TIME__` macros in tcc/musl/kernel) -- cpio mtimes when packing initramfs (lib-pipeline.sh, lib-runscm.sh, - seed-kernel build) — pin to epoch 0 or fixed value -- File-iteration order in shell glob enumeration (boot5 musl source - walk is the main risk; `LC_ALL=C sort` everything) -- Any `find -printf` ordering, tar mtimes, ar mtimes - -**Validation.** `make compare-drivers ARCH=aarch64` exits 0. Per arch, -report the highest stage that converges (amd64/riscv64 may be partial -until the live blockers in `docs/SEED-{AMD64,RISCV64}-TODO.md` are -cleared). - ---- - -## B. Polish (independent, do anytime) - -### Docs -- `README.md`: arch matrix; DRIVER explanation; pointer to - `scripts/boot.sh` and `make build/<arch>/<driver>/boot6/Image`; - test-suite section. -- `docs/SEED-AMD64-TODO.md`: prune items completed by 799eba0; keep - amd64 boot3+ validation, fixed-point check, run-tests under seed. -- `docs/SEED-RISCV64-TODO.md`: prune items completed by e4bfcde; the - boot0 stage-4 user-trap blocker stays. -- `docs/SEED-VIRTIO-BLK.md`: banner marking it as design notes, not - pipeline state. -- Audit `docs/OS.md`, `docs/LIBC.md`, `docs/LIBC.txt`, - `docs/TCC-ARM64-ASM.md` for staleness vs current code. -- `cc/cc.scm.md` → `docs/CC-IMPL.md` (or fold into `docs/CC.md`). -- `docs/TCC.md`: rewrite around the AT patch set. - -### kernel.c -- DTB / cpio / hvm_start_info magics → named constants with one-line - comments. -- amd64 PVH `hvm_start_info` path: inline the one-sentence ABI note - from SEED-AMD64-TODO. -- `sys_unlinkat` flags param: two-line guard or a comment. - -### bootN.sh polish (mostly folded into A6, A0, A4) -- `boot4.sh` `TCC_BOOTSTRAP_RELAX_FIXEDPOINT`: surface in - `boot.sh --help`. -- Standardize argument convention: positional `$ARCH` only; - env-vars only for tunables (`DRIVER`, `BOOT*_TIMEOUT`, etc.). - ---- - -## Validation gates - -After each phase: -1. `make test` (all suites) green on the default arch. -2. `scripts/boot.sh aarch64` end-to-end clean on both drivers (or the - equivalent `make build/aarch64/<driver>/boot6/Image` after A4). -3. Diff `build/aarch64/<driver>/boot{0..6}/` trees against the previous - phase's outputs — phases A6, A3, A0, A4, AX must be no-ops for - output bytes. AT changes outputs (intentionally — workarounds gone). - A2 adds the audit tree. A5 is the proof. - -After all phases: -- `make compare-drivers ARCH=aarch64` exits 0. -- `make compare-drivers ARCH=amd64` and `ARCH=riscv64` report progress - up to the current seed-driver completion line. -- `tree build/<arch>/src/` is the audit basis: one tree, one read. diff --git a/docs/SEED-AMD64-TODO.md b/docs/SEED-AMD64-TODO.md @@ -1,122 +0,0 @@ -# amd64 seed-kernel TODO - -Working doc. Captures the tcc 0.9.26 limitations we worked around -to bring up `boot6 amd64` and `DRIVER=seed ./scripts/boot.sh amd64`, -plus what's still unvalidated. Pairs with `docs/OS.md` (kernel -contract), `docs/TCC.md` (compiler), and `docs/SEED-RISCV64-TODO.md` -(parallel write-up for the riscv64 path). - -## Goal - -`DRIVER=seed ./scripts/boot.sh amd64` should run the full -boot0→boot6 chain entirely inside the tcc-built amd64 seed kernel -(the kernel is its own build driver, podman only mints the first -kernel image). This validates every kernel path the chain depends -on under real workloads. - -## What works (May 2026) - -- `scripts/boot6.sh amd64` (DRIVER=podman) builds a clean PVH ELF at - `build/amd64/boot6/kernel.elf`. Boots under - `qemu-system-x86_64 -machine microvm -kernel ...`; runs scheme1 + - tcc3 + the user smoke tests under `seed-kernel/run.sh ARCH=amd64`. -- `DRIVER=seed scripts/boot6.sh amd64` self-rebuilds the kernel - inside itself (closes the bootstrap loop). -- `DRIVER=seed scripts/boot{0,1,2}.sh amd64` complete cleanly on the - tcc-built kernel (boot0 ≈1 s, boot1 ≈14 s, boot2 ≈43 s under TCG). - Outputs are `build/amd64/boot{0,1,2}/...`. - -## tcc 0.9.26 limitations worked around for amd64 - -These are amd64-specific gotchas; the existing `docs/TCC.md` and -the simple-patches in `scripts/simple-patches/tcc-0.9.26/` cover the -shared/aarch64/riscv64 issues. - -1. **`.quad` literal truncation.** tcc's assembler silently - truncates a `.quad` value to its low 32 bits when the high half - is non-zero (`gen_le64(int64_t c)` is fed a value already lost - through the parser path). The amd64 GDT entries - (`0x00af9a000000ffff`, `0x00af92000000ffff`) are the natural - trip wire — without the workaround, the bootloader's `lgdt` - loads zero P/L bits, the long-mode `ljmp` raises #GP, and the - kernel never prints anything. - - **Workaround:** `seed-kernel/arch/amd64/kernel.S` encodes each - descriptor as `.long lo, hi` so the high half is parsed as a - fresh 32-bit literal. Comment in-place explains why. - - **TODO:** add a `simple-patch` to fix `.quad` parsing in - tccasm.c so the source can use the natural form. Suspected - site is the parser path that intermediates 64-bit values - through a 32-bit `int` before reaching `gen_le64`. - -2. **No PT_NOTE program header / SHT_NOTE section type.** tcc's - linker emits exactly two PT_LOAD phdrs for static EXEs (no - PT_NOTE), and `find_section` defaults every assembler-created - section to SHT_PROGBITS regardless of name. QEMU's PVH - `-kernel` path scans PT_NOTE phdrs for the Xen 18 note that - names the 32-bit entry; without one it errors out with - "Error loading uncompressed kernel without PVH ELF Note". - - **Workaround:** `seed-kernel/scripts/elf-pvh-note.c` is a - tiny tcc-built post-link tool that locates `.note.Xen` via - the section header table, retypes the section as SHT_NOTE, - and writes a fresh PT_NOTE phdr at `phoff + phnum*phentsize` - (the gap between Ehdr/Phdrs and the first PT_LOAD's content - at offset `s_align` = 0x200000 has plenty of room). Wired - into `boot6-gen-runscm.sh` and `boot6.sh` for amd64 only. - - **TODO:** patch tcc to (a) detect ".note.*" section names and - create them as SHT_NOTE, and (b) bump phnum + emit PT_NOTE - phdrs for each SHT_NOTE alloc section after `layout_sections`. - That removes the need for the post-link tool. - -## amd64 kernel additions (not tcc workarounds) - -These are real kernel features that aarch64/riscv64 already had in -some form and amd64 was missing. Listing them so the riscv64-style -TODO has full context, not because they're broken. - -- **MSR-based `syscall` entry path.** scheme1 (and any tcc-built - user binary that follows the SysV/Linux amd64 ABI) emits the - `syscall` instruction, which goes through MSR_LSTAR — not the - IDT. The previous kernel only handled `int $0x80`. Added - `amd64_syscall_entry` in `kernel.S` and `MSR_EFER.SCE | MSR_STAR - | MSR_LSTAR | MSR_SFMASK` setup in `mmu.c::setup_cpu_tables`. -- **CR0.MP / CR4.OSFXSR / CR4.OSXMMEXCPT.** tcc emits xmm-spill - prologues (`movq %xmm7, -0x20(%rbp)`) in user binaries; without - these CR bits set, the first such instruction raises #UD/#NM. - The gcc-built kernel + `-mno-sse` user (user/hello.c) avoided - this; tcc has no `-mno-sse` equivalent so we enable SSE in - long-mode init. -- **PVH `hvm_start_info` cmdline parsing.** microvm has no DTB. - PVH passes the `hvm_start_info` phys addr in EBX; we preserve - it through long-mode init and pass to `kmain`. `parse_dtb` in - `kernel.c` recognises magic 0x336ec578 and reads - `cmdline_paddr` at +0x18 to populate `dt.bootargs`. -- **Legacy `SYS_open(2)` alias.** amd64 stage0 `hex0-seed` issues - `syscall 2` (Linux's legacy open) directly. Added - `ARCH_SYS_open` in amd64 arch.h and a switch case that aliases - to `sys_openat(AT_FDCWD, ...)`. - -## What's not yet validated - -1. **boot3 → boot6 under DRIVER=seed amd64.** boot3 (scheme1 - driving cc.scm to compile tcc.flat.c + libc.flat.c → tcc0) - is compute-bound under TCG-emulated x86_64 on Apple Silicon - (no hvf for amd64). The aarch64 path uses hvf and runs at - near-native speed; amd64 is 5–20× slower. Concretely: boot3 - timed out at the previous 1800 s default. We bumped the - timeout knob (`BOOT3_TIMEOUT`, `BOOT4_TIMEOUT`) and a 4 h - boot3 run is in progress — see `build/amd64/.boot3-stage/ - transcript.txt` for the live log. -2. **Fixed-point check.** Once boot3/4/5 complete, the artifacts - (catm/scheme1/tcc0/tcc1/tcc2/tcc3/libc.a/...) should byte-match - the DRIVER=podman amd64 outputs. Untested. -3. **`scripts/run-tests.sh amd64`** under DRIVER=seed. Untested. -4. **boot4's tcc fixed-point assertion (`tcc2 == tcc3`)** under - DRIVER=seed amd64. Untested. - -## Cost / acceleration note - -The amd64 seed driver runs under `qemu-system-x86_64 -cpu max` -with TCG (no hvf for x86_64 on Apple Silicon). A full -`DRIVER=seed scripts/boot.sh amd64` is realistically a multi-hour -operation. The aarch64 path is the fast/CI iterating loop; the -amd64 seed path is mostly there for parity validation. diff --git a/docs/SEED-VIRTIO-BLK.md b/docs/SEED-VIRTIO-BLK.md @@ -1,359 +0,0 @@ -# Seed kernel: virtio-blk for I/O - -Plan to replace the cpio-initrd-in / UART-hex-out I/O shape of -`seed-kernel/` with a pair of virtio-blk-MMIO devices: one read-only -disk carrying the boot inputs (cpio newc, byte-identical to today's -`-initrd`) and one read-write disk for outputs. UART stays the console. - -## Motivation - -Current shape (`seed-kernel/run.sh`, `lib-seed-runscm.sh:98-103`): - -- **In**: QEMU `-initrd` loads cpio newc; kernel finds it via DTB - `/chosen/linux,initrd-{start,end}` (kernel.c:286-296), unpacks via - `parse_cpio` (kernel.c:429-465) into the in-memory tmpfs. -- **Out**: PL011 UART is the only egress. On exit (`dumpfs` bootarg), - `dump_tmpfs` (kernel.c:926-939) hex-encodes every tmpfs file framed by - `=== DUMP-BEGIN ===` / `=== FILE … ===` / `=== DUMP-END ===` sentinels. - `seed-kernel/scripts/extract-dump.sh` reassembles host-side. - -This works but has real costs: - -1. **Output throughput.** Hex doubles size; PL011 is byte-at-a-time MMIO. - For boot5 (full musl + libc.a + crt*.o, tens of MB), UART dump is the - dominant wall-time cost on TCG and a non-trivial slice under HVF. -2. **No mid-run egress.** Output exists only at exit. A crash mid-build - loses everything in tmpfs. -3. **Symmetry.** Inputs ride a structured device (initrd memory region); - outputs ride a debug device (UART). Two separate framings. -4. **Optionality.** Once the kernel can talk to virtio-blk, mounting a - real on-disk artifact cache (across multiple runs) becomes trivial. - -## Non-goals - -- Generalised block layer, partitions, ext2/FAT, write-back cache, - buffer cache, multi-queue, MSI-X, IRQ delivery. -- virtio-net, virtio-9p, virtio-console, PCI transport. -- Replacing the in-memory tmpfs semantics user code sees (`sys_openat`, - `sys_read`, `sys_write` against `files[]` stays exactly as-is). - -The seed kernel keeps its single-process, polling-only, -no-interrupts shape. virtio-blk just swaps the boot-time *transport* -and the exit-time *dump format*. - -## Design - -Two virtio-blk-MMIO devices on QEMU virt: - -``` --drive file=in.img,if=none,format=raw,id=hd0,readonly=on \ --device virtio-blk-device,drive=hd0 \ --drive file=out.img,if=none,format=raw,id=hd1 \ --device virtio-blk-device,drive=hd1 -``` - -`in.img` is the cpio newc archive (today's `initramfs.cpio`), padded to -a 512-byte multiple. `out.img` is a pre-allocated zero file sized to an -upper bound (≈256 MB covers boot5 worst case). - -Boot flow: - -1. `kmain` brings up MMU as today. -2. New `virtio_blk_init()` walks DTB nodes named `virtio_mmio@*` - (compatible `"virtio,mmio"`); for each, probe MagicValue / Version / - DeviceID. ID 2 = block; finish device init for each, then read - sector 0 to classify (cpio magic → blk0, otherwise → blk1). Panic - if the count of either is not exactly 1. -3. `parse_cpio` is fed by `blk_read_all(blk0)` instead of the initrd - memory region. cpio bytes land in the existing kheap-backed buffer; - `parse_cpio` is unchanged. -4. On exit, kernel writes a serialised tmpfs to blk1 in the flat - format described below, then PSCI off. No bootarg gating — the - write is unconditional (a no-op if `files[]` is empty). -5. Host reads blk1 with a small extractor (`extract-blk.sh`). - -UART stays the console; `uart_puts` everywhere is untouched. The -`dumpfs` bootarg, `dump_tmpfs`, the `=== DUMP-{BEGIN,END} ===` / -`=== FILE … ===` sentinels, and `scripts/extract-dump.sh` are all -deleted in the same change. - -### On-disk layout for outputs - -Tiny custom format — no FS. Sector-aligned (512 B), little-endian, all -offsets in sectors: - -``` -sector 0 magic "SEEDFS\0\0" (8B) | nfiles u32 | reserved u32 - followed by nfiles directory entries: - path[96] | data_offset_sectors u32 | size_bytes u64 | _pad - (entry size 112 B → 4 entries/sector → sector 1.. for table) -sector N.. file data, each file padded up to 512-byte boundary -``` - -Reusing the existing `path[96]` and `MAX_FILES=4096` from `struct file` -keeps the table at ≤900 KB (under 2 sectors of header + ~896 KB table). -The host extractor walks the table and writes each file out. - -This is roughly "cpio-without-the-headers-per-file." Could equally -write cpio newc back; the flat table is just smaller code in the -kernel (no hex name length, no per-entry headers, no parse loop). - -## Memory / DMA - -virtio-mmio descriptors carry **physical** addresses. Kernel-side -buffers must therefore have known PAs. - -The current MMU (kernel.c:144-213) gives us this for free in two -regions: - -- **`L1[1..3]`** identity-maps VA 1..4 GB to PA 1..4 GB as Normal - memory. Kernel image (0x40080000) and kheap (0x40xxxxxx..0x4b000000) - live here, so any `kalloc()`'d buffer has VA == PA. `mem_cpy` etc. - work directly. -- **`L1[4]`** is a 1 GB Device block aliasing PA 0..1 GB at VA 4..5 GB, - which is how we already reach UART; we'll reach the virtio-mmio - control regs at `0x0a000000..0x0a004000` through the same alias - (`DEVICE_ALIAS_BASE + 0x0a000000`). - -So DMA buffers come from `kalloc()` (Normal, identity-mapped, VA==PA) -and the device regs from the existing high alias. No MMU changes. - -**Cache coherency.** virtio-mmio in QEMU is `dma-coherent` per the DTB -(`/virtio_mmio@…/dma-coherent`); virtio-mmio v2 + the modern feature -bits assume coherent DMA. Inner-shareable WBWA (already programmed in -TCR/MAIR) plus DMB before NotifyQueue and DMB after reading the used -ring is sufficient. No explicit cache maintenance ops. - -**Reservation.** virtio queue memory must be 4 KB-aligned. The cpio -read buffer must be sized to the cpio length (fetched from blk0 capacity -and trimmed at parse). Sizes of interest today: - -- boot5 cpio ≈ 30-80 MB. Already fits in current kheap (192 MB). -- Output blob: bound by tmpfs total bytes. Current `kheap_end = - 0x4b000000` allows ~176 MB heap; sufficient for boot5's output - (≈10s of MB). Sizing is unchanged from today's cpio-in-RAM design. - -Conclusion: no memory layout changes required. Only one new fixed -allocation: a small (single 4 KB page) virtqueue area per device. - -## virtio-blk-MMIO driver shape - -A polling, single-virtqueue, one-request-at-a-time driver. Spec ref: -virtio 1.2 §4.2 (MMIO transport) and §5.2 (block device). - -**Layout (one struct `virtio_mmio` per device):** - -```c -volatile u32 *regs; // VA in DEVICE_ALIAS_BASE+phy -struct vring_desc desc[8]; // 8 descriptors plenty (we issue 1 at a time, 3 chained) -struct vring_avail avail; -struct vring_used used; -u16 next_desc; -u16 last_used; -u64 capacity_sectors; -``` - -**Registers used (offsets from §4.2):** - -`MagicValue (0x000)`, `Version (0x004)`, `DeviceID (0x008)`, -`DeviceFeatures (0x010)` / `DeviceFeaturesSel (0x014)`, -`DriverFeatures (0x020)` / `DriverFeaturesSel (0x024)`, -`QueueSel (0x030)`, `QueueNumMax (0x034)`, `QueueNum (0x038)`, -`QueueReady (0x044)`, `QueueNotify (0x050)`, -`InterruptStatus (0x060)`, `InterruptACK (0x064)`, -`Status (0x070)`, `QueueDescLow/High (0x080/084)`, -`QueueDriverLow/High (0x090/094)`, `QueueDeviceLow/High (0x0a0/0a4)`, -`Config (0x100)` (block: 8-byte capacity at +0). - -**Init sequence (§3.1.1):** - -1. `MagicValue == 0x74726976` ("virt"), `Version == 2`, `DeviceID == 2`. -2. `Status = 0` (reset), then `|= ACKNOWLEDGE`, then `|= DRIVER`. -3. Read `DeviceFeatures` (sel 0 and 1); negotiate `VIRTIO_F_VERSION_1` - only (bit 32). Refuse `VIRTIO_BLK_F_RO` if mismatched with intent - (we set `readonly=on` on blk0 so the device offers RO; the driver - doesn't need to *negotiate* RO since we just won't issue writes). -4. `Status |= FEATURES_OK`; reread Status to confirm. -5. `QueueSel = 0`; `QueueNumMax` (≥8 always on QEMU); `QueueNum = 8`. -6. Allocate 4 KB-aligned 4 KB page; lay out desc[8] / avail / used per - §2.7, write `QueueDesc{Low,High}`, `QueueDriver{Low,High}`, - `QueueDevice{Low,High}` to PAs. -7. `QueueReady = 1`; `Status |= DRIVER_OK`. -8. Read `Config + 0` for capacity (sectors). - -**Request shape (§5.2.6):** chain of three descriptors: - -``` -desc[0]: read-only, points to struct virtio_blk_req_hdr {u32 type, u32 reserved, u64 sector} -desc[1]: write-only (for read req) / read-only (for write req), points to data buffer (multi-sector OK with one descriptor per spec; in practice we use 1 desc per ≤4 MB chunk and loop) -desc[2]: write-only, 1 byte status -``` - -Add head index to `avail.ring[avail.idx % qsz]`, `dmb ishst`, -`avail.idx++`, `dmb ishst`, `regs[QueueNotify] = 0`, then poll -`used.idx` until it advances past `last_used`. Read status byte; -0 = OK, else fail. - -Chunk size: pick 1 MB per request (2048 sectors). Cpio fetch loops -until `capacity_sectors` sectors are read or the cpio TRAILER is seen -(we can also just read all of `capacity_sectors` since `in.img` is -sized to the cpio). - -### Public API - -```c -int blk_init(void); // probes DTB, finds blk0/blk1 -u64 blk_capacity(int dev); // sectors -int blk_read (int dev, u64 sector, void *buf, u64 nsectors); -int blk_write(int dev, u64 sector, const void *buf, u64 nsectors); -``` - -Used by: - -- `kmain`: `blk_init()`; `blk_read(0, 0, cpio_buf, blk_capacity(0))`, - then `parse_cpio(cpio_buf, capacity*512)`. -- `dump_tmpfs_blk()`: serialise `files[]` into the SEEDFS layout - described above and `blk_write(1, …)`. - -## DTB walking - -`parse_dtb` (kernel.c:254-317) currently records only `chosen.initrd*` -and the `memory@…` reg. Extend with a callback that, when entering a -node whose name starts with `"virtio_mmio@"`, captures up to N reg -tuples into a `dtb_info::virtio_mmio[]` array (PA + size). The MMU -device alias already covers all of these. - -Subtlety: per QEMU virt, only some of the 32 virtio-mmio slots are -populated — unpopulated slots return `MagicValue==0` / `DeviceID==0`. -The driver init must skip those. - -## Build-system changes - -`seed-kernel/Makefile`: - -- Add `kernel.c` dep on a new `virtio_blk.c` (or inline in `kernel.c` - to keep the single-TU shape — leaning toward inline; the driver is - ≤300 lines). -- Add `$(OUT)/in.img` rule: copy `initramfs.cpio`, pad to a - 512-byte multiple with `truncate -s %512`. -- Add `$(OUT)/out.img` rule: `truncate -s 256M`. -- Update `run.sh`: drop `-initrd "$INITRD"`; add the two - `-drive`/`-device` pairs above. `INITRD` becomes `IN_IMG`, - `OUT_IMG` is created fresh per run. - -`scripts/lib-seed-runscm.sh`: - -- Replace `-initrd "$INITRAMFS"` with the two-disk variant. -- Drop `dumpfs` from the `-append` line (no longer recognised). -- Replace `"$EXTRACT" … "$TRANSCRIPT"` with - `extract-blk.sh "$S_OUT_DIR" "$OUT_IMG"`. The DUMP-END grep guard - is replaced by checking that `extract-blk.sh` finds the SEEDFS - magic at sector 0; absence means the kernel didn't reach exit. - -`seed-kernel/scripts/extract-blk.sh`: reads sector 0 magic, walks -the table, writes files. Output contract matches what -`extract-dump.sh` produced (same filenames in the same dump dir), -so `seed_runscm_export` and downstream acceptance scripts don't -need to change. - -`seed-kernel/scripts/extract-dump.sh` is deleted; `tier1-gate.sh` -and `tier2-gate.sh` switch their `EXTRACT` envvar / direct calls -to `extract-blk.sh`. - -## Implementation order - -Single branch, single landing. Internal checkpoints in a sensible -order; no dual paths in the tree at any commit boundary. - -1. **Add `virtio_blk` driver and DTB enumeration.** Extend - `parse_dtb` to record `virtio_mmio@…` reg tuples. Add the driver - (init, `blk_read`, `blk_write`). Not yet wired into `kmain`. - Sanity check: a unit-test `kmain` that probes and prints - capacity for both disks boots cleanly under a hand-built - `run.sh` with two `-drive`/`-device` pairs. -2. **Cut over input path.** Replace the initrd-region read in - `kmain` with `blk_read(0, 0, cpio_buf, blk_capacity(0))`. Delete - the `chosen.initrd-{start,end}` handling from `parse_dtb` and - the "no initrd" panic. Update `Makefile` to produce `in.img` - from `initramfs.cpio`. -3. **Cut over output path.** Add `dump_tmpfs_blk` and - `extract-blk.sh`. Delete `dump_tmpfs`, `dump_tmpfs`'s sentinels, - the `dumpfs` bootarg parser, `g_dumpfs`, and - `scripts/extract-dump.sh`. `dump_tmpfs_blk` runs unconditionally - from `sys_exit_final` before PSCI off. -4. **Acceptance.** Run `tier1-gate.sh`, `tier2-gate.sh`, - `tests/seed-accept.sh`, `tests/seed-accept.sh boot34`, - `tests/seed-accept.sh boot5`. - All must produce byte-identical artifacts to the prior - (cpio+dumpfs) tree at `HEAD~1`. Expect boot5 to surface any - off-by-one in the directory table fastest (≈3900 tmpfs entries). - -## Decisions (resolved) - -- **Console.** PL011 stays. `uart_putc/_puts/_putd/_putx` and user - `write(1, …)` are unchanged. Only the file dump moves to virtio. -- **virtio version.** Pin MMIO Version == 2 (MagicValue == 0x74726976, - Version regs read in init). Anything else: `uart_puts` a panic line - and `wfe`. QEMU 10 (current host) and any QEMU ≥4.0 ship v2; the - build harness has a single QEMU floor and we don't support pre-v2. -- **Identifying blk0 vs blk1.** Slot order in the DTB does not depend - on `-drive` attachment (verified with `dumpdtb`: all 32 - `virtio_mmio@…` nodes are present unconditionally), and QEMU's - command-line-to-slot mapping is not contractual across versions. - Use **content-based** identification: after enumerating all - populated DeviceID==2 devices, read sector 0 of each and call the - one whose first 6 bytes are `"070701"` (cpio newc magic) `blk0`; - the other is `blk1`. If neither matches or both match, panic. This - removes the dependency on `-drive` ordering on the qemu command - line entirely. -- **Output image size.** Host pre-allocates `out.img` as a 256 MB - sparse file (`truncate -s 256M out.img`). Header at sector 0 - records total used bytes; `extract-blk.sh` reads only that many. - No truncation of `out.img` is needed — sparse + bounded read is - free on APFS / ext4. -- **Initial kheap sizing.** Today `kheap_end` starts at `0x44000000` - (64 MB) and bumps to `0x4b000000` after `parse_cpio` finishes, - because the initrd region was reserved up to `0x4b000000`. Without - `-initrd`, that region is free from boot, so set the initial - `kheap_end = 0x4b000000` (176 MB). The cpio read buffer - (`kalloc(blk_capacity(0) * 512)`) lands in this range. Boot5 cpio - ≈ 80 MB; comfortably fits. -- **Persistence across runs.** Out of scope. `out.img` is - re-created (truncated to 256 MB of zeros) before each run by the - harness; the kernel always writes a fresh header at sector 0. -- **Per-request chunking.** 1 MB chunks (2048 sectors) per virtio - request, single data descriptor per chunk (3-descriptor chain: - hdr / data / status). 8-entry virtqueue, one in-flight request at - a time, polling `used.idx`. No interrupts (`InterruptACK` written - once per used entry to clear the device-side bit, but no IRQ - handler — DAIF stays masked as today). -- **Coherency.** Inner-shareable WBWA (already programmed); `dmb - ishst` before `QueueNotify`, `dmb ish` after observing - `used.idx` advance. No `dc civac` / `ic` ops — virtio-mmio is - `dma-coherent` per DTB and the device DMAs into the same - inner-shareable domain the kernel reads. - -## Risks (residual) - -- **Empty / mis-sized `in.img`.** If the harness fails to stage the - cpio onto blk0, `parse_cpio` walks zero bytes and `find_file("init")` - fails — exactly the same failure mode as a missing `-initrd` today - (`kernel.c:1136-1139`). No new risk. -- **Boot5 file count growth.** `MAX_FILES = 4096` and `path[96]` - remain the binding limits, unchanged from today. The on-disk - directory table is sized off these constants; bumping either - requires a same-commit bump to `extract-blk.sh`'s parser. - -## Estimated effort - -- DTB walk extension + virtio_blk driver + integration in `kmain`: - ~300 lines C, one work session. -- Output serialiser + extractor: ~80 lines C + ~40 lines shell. -- Build/run wiring + acceptance plumbing: ~50 lines shell across - Makefile, run.sh, lib-seed-runscm.sh. -- Stabilising acceptance against existing fixtures: a couple sessions - to chase any byte-divergence (most likely culprit is dump ordering - or padding, both fixable in extractor). - -Total: ~1-2 days of focused work, gated by byte-identical -acceptance vs the cpio+dumpfs tree at the pre-cutover commit. diff --git a/docs/TCC-ARM64-ASM.md b/docs/TCC-ARM64-ASM.md @@ -1,314 +0,0 @@ -# tcc arm64 assembler — design - -Working doc. Adds an `arm64-asm.c` to vendored tcc 0.9.26 so the -ARM64-target build accepts `.S` inputs and `__asm__("…")` blocks. -Lands in two phases: a narrow first cut covering exactly what the -repo's `.S` files need today, then incremental extension to ride -parity with `riscv64-asm.c` and (modulo x86 quirks) `i386-asm.c`. - -Goal of this doc: lock the **internal shape** so phase 1 is a -genuine subset of the final assembler — not a stub we throw away. - -## Why this exists - -`vendor/upstream/tcc-0.9.26.tar.gz` ships per-target asm: - -| arch | file | notes | -|------|------|-------| -| x86_64 / i386 | `i386-asm.c` (1720 LoC) | shared by both targets | -| arm | `arm-asm.c` (94 LoC) | stub: directives only, every opcode → `tcc_error` | -| riscv64 | `riscv64-asm.c` (856 LoC) | real assembler | -| arm64 | — | absent; `CONFIG_TCC_ASM` undefined for `TCC_TARGET_ARM64` | - -Today the boot2 Makefile compensates for the arm64 gap by cross-asm'ing -`tcc-cc/aarch64/start.S` and `tcc-libc/aarch64/{start,sys_stubs}.S` -through host clang `-target aarch64-linux-gnu` (Makefile:386–410). -This doc describes the assembler that lets us delete that. - -## Phase 1 — narrow scope - -Cover exactly the mnemonics the in-tree `.S` fixtures use, plus the -directive surface `tccasm.c` already handles. - -Mnemonics required by `tcc-cc/aarch64/start.S` and -`tcc-libc/aarch64/{start,sys_stubs}.S`: - -| mnemonic | forms used | encoding family | -|----------|------------|-----------------| -| `mov` | `mov xN, #imm` (incl. negative); `mov xN, xM` | movz/movn/movk + ORR(reg) alias | -| `add` | `add xN, sp, #imm` | add (immediate, 64-bit) | -| `ldr` | `ldr xN, [xM]`, `ldr xN, [sp]` | LDR (immediate, unsigned offset, 64-bit) | -| `bl` | `bl <symbol>` | BL → `R_AARCH64_CALL26` | -| `b` | `b .` (self-loop) | B → `R_AARCH64_JUMP26` (or in-section fixup) | -| `ret` | `ret` (uses x30) | RET (Xn=30 default) | -| `svc` | `svc #0`, `svc #imm16` | SVC | - -Registers: `x0`-`x30`, `w0`-`w30`, `sp`, `xzr`, `wzr`. (`fp`=`x29`, -`lr`=`x30`, `ip0`=`x16`, `ip1`=`x17` are aliases — defer to phase 2.) - -Directives: anything `tccasm.c` already drives — `.globl`, `.text`, -`.data`, `.byte`, `.word`, `.quad`, `.ascii`, `.asciz`, `.align`, -`.skip`, `.section`, `.previous`, labels, `.set`. Phase 1 does -**not** need to add code here; pulling in `tccasm.c` is automatic -once `arm64-asm.c` defines `CONFIG_TCC_ASM`. - -Inline `__asm__` constraint plumbing (`subst_asm_operand`, -`asm_compute_constraints`, `asm_gen_code`) follows `riscv64-asm.c`'s -"defined but no-op" pattern in phase 1: `.S` files work, full -constraint-based inline asm doesn't yet. Same posture upstream -shipped riscv64 with. - -**Phase 1 acceptance:** the existing `tcc-libc` and `tcc-cc` suites -pass on `ARCH=aarch64` with the host cross-asm path removed from -the Makefile (start/sys_stubs assembled by tcc-boot2 itself). - -## File layout - -``` -arm64-asm.c new — opcode table, parser, encoders (~600 LoC at parity) -arm64-tok.h new — DEF_ASM(...) for regs + mnemonics (~150 LoC at parity) -tcctok.h +3 lines: include arm64-tok.h under TCC_TARGET_ARM64 -tcc.h +1 line: include arm64-asm.c in the per-target block -libtcc.c +1 line: same, in the ONE_SOURCE block -``` - -Patches go in `scripts/simple-patches/tcc-0.9.26/` and apply via -`stage1-flatten.sh`'s `apply_our_patch` mechanism — same shape as -the existing arm64 patches (`arm64-stdarg-array`, -`arm64-va-pointer-operand`, `arm64-va-arg-pointer`). New -`arm64-asm.c` and `arm64-tok.h` ship as straight files in -`scripts/simple-patches/tcc-0.9.26/files/` and are copied into -`$SRC` by the flatten script before preprocessing. - -## Internal shape - -The narrow set is small enough to write linearly, but it's worth -ten minutes more to put the real ARM64 ISA encoding model in place -on day one so phase 2 is "add table rows," not "rewrite." - -### Operand model - -```c -enum { - OPT_REG, /* X/W register, 0..31 (sp/zr distinguished by use) */ - OPT_SHIFT_REG, /* Xn[, lsl/lsr/asr/ror #imm] */ - OPT_EXT_REG, /* Xn[, uxtw/sxtw/sxtx #imm] */ - OPT_IMM, /* unparsed signed/unsigned immediate */ - OPT_IMM12, /* add/sub immediate: 12-bit + optional lsl#12 */ - OPT_LOG_IMM, /* and/orr/eor logical immediate (N:imms:immr) */ - OPT_MOV_IMM, /* movz/movk/movn 16-bit + hw shift */ - OPT_MEM, /* [Xn], [Xn,#imm], [Xn,Xm{,ext}], pre/post indexed */ - OPT_LABEL, /* symbol+addend; resolves to PC-rel reloc */ - OPT_COND, /* eq/ne/lt/... (4-bit cond code) */ - OPT_SYS, /* sysreg encoding for mrs/msr — phase 3 */ -}; - -typedef struct Operand { - uint32_t kind; /* OPT_* */ - uint8_t is_w; /* 0=64-bit, 1=32-bit (X vs W) */ - uint8_t is_sp; /* 1 if textual form was sp (vs xzr) */ - union { - struct { uint8_t reg; uint8_t shift_kind; uint8_t shift_amt; } r; - struct { uint8_t base, idx, ext_kind, ext_amt; - int32_t disp; uint8_t mode; /* off|preidx|postidx */ } m; - ExprValue e; /* immediates and label refs */ - uint8_t cond; - }; -} Operand; -``` - -The `kind` enum is the type signature instruction encoders match -against. Phase 1 uses only `OPT_REG`, `OPT_IMM`, `OPT_MEM` -(simple base+offset variant), and `OPT_LABEL`. Adding the rest is -"new enum value + new parse path"; encoders not yet handling them -just `expect("supported operand")` until they do. - -`is_w`, `is_sp` — AArch64-specific. `Wn` and `Xn` share register -numbers; the encoder needs to know the size to set the `sf` bit. -`sp` and `xzr` both encode as register 31 but are not -interchangeable per-instruction; track which token the user wrote. - -### Encoder organization - -One static helper per ARM64 instruction format, mirroring -`riscv64-asm.c`'s `asm_emit_i / asm_emit_r / asm_emit_u / asm_emit_s`. -Group by encoding family in C ARM ARM (Section C4): - -| helper | covers | -|-----------------------------|-----------------------------------------| -| `emit_dp_imm_addsub` | add/sub/cmp/cmn (immediate) | -| `emit_dp_imm_log` | and/orr/eor/tst (immediate) | -| `emit_dp_imm_mov` | movz/movk/movn (incl. `mov` aliases) | -| `emit_dp_imm_bitfield` | sbfm/ubfm/bfm + sxtw/uxtb/lsl-imm aliases | -| `emit_dp_reg_addsub` | add/sub/cmp shifted-reg + extended-reg | -| `emit_dp_reg_log` | and/orr/eor/bic shifted-reg | -| `emit_dp_reg_shift` | lslv/lsrv/asrv/rorv + lsl-reg aliases | -| `emit_dp_reg_mul` | madd/msub/mul/mneg/smull/umull | -| `emit_dp_reg_csel` | csel/csinc/csinv/csneg + cset/cinc aliases | -| `emit_ldst_imm` | ldr/str (immediate, unsigned + pre/post) | -| `emit_ldst_reg` | ldr/str (register offset, with extend) | -| `emit_ldst_pair` | ldp/stp (incl. pre/post indexed) | -| `emit_ldst_pseudo_eq` | `ldr Xn, =imm64` / `=sym` — inline lowering, see below | -| `emit_branch_imm` | b/bl + label reloc | -| `emit_branch_cond` | b.cond + label reloc | -| `emit_branch_cmp` | cbz/cbnz/tbz/tbnz | -| `emit_branch_reg` | br/blr/ret | -| `emit_system` | svc/hvc/smc/brk/hint (nop/yield/wfe/wfi) | - -Phase 1 implements only `emit_dp_imm_addsub`, `emit_dp_imm_mov`, -`emit_ldst_imm`, `emit_branch_imm`, `emit_branch_reg` (just `ret`), -and `emit_system` (just `svc`). Each helper has the full ISA-format -encoding from day one — phase 1 just feeds it the narrow operand -shapes. - -### `asm_opcode` dispatch - -Same shape as `riscv64-asm.c`'s tail switch: outer switch on the -TOK groups dispatching to a per-family parser, which parses -operands, validates `kind`s, and calls the matching `emit_*` helper. -Adding mnemonics in phase 2 is a new `case TOK_ASM_xxx:` plus, if -needed, a new shared parser routine. - -### Label & relocation interface - -`bl <sym>` / `b <sym>` emit zero into the instruction word and call -`greloca(cur_text_section, sym, ind, R_AARCH64_CALL26 /* or -JUMP26 */, 0)` — both reloc types are already handled by -`arm64-link.c:30-46`. Local backward references (`b .`, numbered -labels) resolve through the symbol table the same way `tccasm.c` -already wires for other arches: `asm_new_label` defines, `asm_expr` -resolves, the relocation collapses at link time. - -Symbol address loads (the `ldr Xn, =sym` pseudo, and any phase-2 -movz/movk-via-`:abs_g0:`/`:abs_g1:`/etc. modifiers) emit the same -4-instruction `movz/movk` chain that `arm64-gen.c:431-440` uses for -compiler-emitted loads: `R_AARCH64_MOVW_UABS_G0_NC` + -`G1_NC` + `G2_NC` + `G3`. `adrp`+`add` is deliberately **not** -used — `arm64-gen.c:425-430` documents that the ±4GB ADR_PREL_PG -range fails on tcc's static layout and the MOVW chain is the -working idiom. The relocs are exercised by every existing -tcc-built ARM64 binary, so this is well-trodden ground. - -### Inline-asm constraint plumbing - -Phase 1 stubs: - -```c -ST_FUNC void subst_asm_operand(CString *s, SValue *sv, int mod) { - tcc_error("ARM64 inline asm operands not implemented yet"); -} -ST_FUNC void asm_compute_constraints(...) { /* no-op */ } -ST_FUNC void asm_gen_code(...) { /* no-op */ } -ST_FUNC void asm_clobber(uint8_t *cr, const char *str) { - /* parse register name, mark cr[reg]=1 — copy from riscv64-asm.c */ -} -ST_FUNC int asm_parse_regvar(int t) { /* x0..xzr, w0..wzr → 0..31 */ } -``` - -This is enough for `.S` files, top-level `__asm__("…")` strings, -and the `__asm__("name")` symbol-rename form. Constraint-driven -register allocation (`__asm__("…" : "=r"(out) : "r"(in))`) lights -up in phase 3 once `subst_asm_operand` + `asm_compute_constraints` -are real — straight port from `i386-asm.c`'s template logic -adapted to ARM64 register names; no surprises. - -## Phase plan - -**Phase 1 (this design)** — `mov`/`add`/`ldr`/`bl`/`b`/`ret`/`svc`, -all integer-register operand kinds restricted to OP_REG/OP_IMM/OP_MEM -(base+disp)/OP_LABEL. Acceptance: `.S` files in tcc-cc/tcc-libc -assemble through `tcc-boot2`; Makefile drops `TCC_ASM` dance for -ARCH=aarch64. - -**Phase 2 (implemented)** — broadens to riscv64-parity coverage. -Surface added: -- DP-imm: `add`/`sub`/`adds`/`subs`/`cmp`/`cmn`/`neg`/`negs`, - `and`/`orr`/`eor`/`ands`/`tst` (logical-imm), `movz`/`movn`/`movk`, - `sbfm`/`ubfm`/`bfm` + `lsl`/`lsr`/`asr`/`sxtb`/`sxth`/`sxtw`/ - `uxtb`/`uxth` immediate aliases. -- DP-reg: shifted-reg `add`/`sub` (and set-flags variants), - extended-reg form when one operand is `sp`, logical-reg - `and`/`orr`/`eor`/`bic`/`orn`/`eon`/`bics`/`mvn`, variable shifts - `lslv`/`lsrv`/`asrv`/`rorv` with `lsl`/`lsr`/`asr`/`ror` reg - aliases, `mul`/`mneg`/`madd`/`msub`/`smull`/`umull`/`smaddl`/ - `umaddl`/`smsubl`/`umsubl`/`smulh`/`umulh`/`udiv`/`sdiv`, - `csel`/`csinc`/`csinv`/`csneg` + `cset`/`csetm`/`cinc`/`cinv`/ - `cneg` aliases. -- Mem: `ldr`/`str` register-offset (with optional `lsl`/extend - shift) and pre/post-indexed forms; `ldrb`/`ldrh`/`ldrsb`/`ldrsh`/ - `ldrsw`/`strb`/`strh`; `ldp`/`stp` X- and W-forms with all index - modes. -- Branches: `b.cond` and `cbz`/`cbnz`/`tbz`/`tbnz` (in-section - targets only — no `R_AARCH64_CONDBR19`/`TSTBR14` reloc handlers - in `arm64-link.c`, so extern targets error out), `br`/`blr`, - full `ret`. -- Pseudo: `ldr Xn, =imm64` lowers via `arm64_movimm`; `ldr Xn, - =sym` lowers to the 4-insn `MOVW_UABS_G{0..3}` reloc chain. -- System: `svc`/`hvc`/`smc`/`brk`/`hlt`, `nop`/`yield`/`wfe`/`wfi`/ - `sev`/`sevl`/`hint`, `dsb`/`dmb`/`isb`. - -`arm64_encode_bimm64` and `arm64_movimm` from `arm64-gen.c` are -called directly: under `ONE_SOURCE` (the bootstrap pipeline) -both `.c` files inhabit the same TU sequentially, so the static -helpers are visible to `arm64-asm.c` without ST_FUNC promotion. - -Pre-existing limitation: `mes-libc`'s `strtoull` truncates via -`strtol`, so 64-bit hex literals (e.g. `#0xff00ff00ff00ff00`) get -clamped at parse-time. Computed expressions (`#1<<32`) work -around it. Out of scope to fix in this phase — surfaces in any -asm path that takes wide immediates and is unrelated to the -encoder logic. - -`tests2/73_arm64.c` does not exist upstream — the doc was -speculative. Validation instead is the in-tree `.S` round-trip -plus a hand-checked `build/phase2-test/test.S` fixture covering -each new family. - -**Phase 3** — full inline-asm constraint surface -(`subst_asm_operand` + `asm_compute_constraints`). Ports the -i386-asm.c template walk; ARM64-specific bits are operand modifier -letters (`%w0` for W-form, `%x0` for X-form) and clobber semantics. - -## Validation - -Unit-level: a new `tests/tcc-asm/` suite with one `.S` (or -`__asm__()` C wrapper) fixture per mnemonic+operand-shape combo, -diffing the encoded bytes against a known-good (host clang or -upstream gas) reference. Same shape as the existing P1 suite — -fixture in, expected hex out, byte diff. - -Integration-level: drop the host cross-asm out of Makefile lines -386–410 for ARCH=aarch64 and let `tcc-boot2` build start.S / -sys_stubs.S directly. The existing `tcc-cc` and `tcc-libc` suites -then exercise the new assembler end-to-end, including the -stage-2/stage-3 fixed-point check. - -Self-host check (phase 2+): compile the patched `tcc.flat.c` itself -with `tcc-tcc` and confirm the `arm64-asm.c` it just compiled is -byte-identical to the one cc.scm produced. - -## Resolved decisions - -- **No literal pool.** Phase 2 lowers `ldr Xn, =imm64` to an inline - `movz/movk` chain (call into the same `arm64_movi`/`arm64_movimm` - logic `arm64-gen.c:155-221` already uses) and `ldr Xn, =sym` to - the 4-instruction MOVW_UABS chain. tcc's own codegen never emits a - pool, so adding pool infrastructure would be net-new for one gas - pseudo nothing in-tree uses; inline lowering matches what - compiler-emitted code already does. Cost: `Xn` is clobbered with - the constant rather than loaded from `.rodata`, and `ldr =0x… ; - .word foo` won't share — neither matters for any in-tree fixture. - -- **Logical-immediate encoder: lift `arm64_encode_bimm64`.** - `arm64-gen.c:106-153` already implements the full N:imms:immr - encoder as a static function, used by gen.c itself for - `orr-immediate`-as-`movi` (line 187) and direct logical-imm - codegen (line 1395). Promote it to an `ST_FUNC` declared in the - arm64 block of `tcc.h` and call it from `arm64-asm.c`. Zero new - code, no port, no licensing question. - -- **`R_AARCH64_MOVW_UABS_G*` is the primary path.** `arm64-gen.c:429` - hardcodes `avoid_adrp = 1`, meaning every symbol address load in - every existing tcc-built ARM64 binary already goes through the - MOVW_UABS_G{0..3}_NC chain. `relocate()` (arm64-link.c:174-189) - implements all four. Use them; don't use `adrp`/`adr` at all.