kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit feb396e0fd9c03228accaafe31af0b6f1c8d6a7b
parent d5c7cfe06d456bb39988dacd4b24f1d394385f2c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 10:08:13 -0700

perf(cg): kill O(n^2) zero-init + const-tracker blowup that hung debug bootstrap

`make bootstrap-debug` hung for minutes compiling src/api/package.c. Root
cause: a ~28 KB all-char DistPackageManifest local under
-ftrivial-auto-var-init=zero. Two compounding O(n^2) issues, amplified ~20x
by the debug build's ASAN instrumentation:

1. zero_init_at lowered the implicit zero-init to one scalar store per leaf
   byte (~28k stores). Each non-zero-offset store stashes a fresh frontend
   temp (pcg_store -> pcg_local -> g->nlocals++), so g->nlocals grows
   linearly with stores emitted.

2. api_local_const_clear_all (hit on every store/call/branch/address-of via
   the memory/control/address-taken boundaries, ~40 call sites) scanned all
   g->nlocals. With (1) feeding the growth, the whole thing was O(n^2) in
   leaf count.

Fix A: -ftrivial-auto-var-init=zero now zeroes the whole object with a
single kit_cg_memset (zero_object_bytes_at) instead of recursing to per-leaf
stores. All-zero bytes is the zero value of every C type on supported
targets, and it is what gcc/clang emit. Collapses ~28k stores to one.

Fix C: thread const-tracked locals onto an intrusive active list
(KitCg.const_head / ApiSourceLocal.const_next+const_listed) so the
const-tracker's boundary ops clear in O(#tracked) instead of O(nlocals).
Mirrors the NativeDirectTarget cache_head/tail precedent. Removes the O(n^2)
for all functions, not just zero-init. Pure no-op behavior change (clearing
an already-invalid local was always a no-op).

package.c: minutes -> 1.1s (ASAN). 28 KB manifest synthetic: 15.8s -> 0.06s.
No regressions: parse 3880/128, cg-api/opt suites, toy 1392 all green;
const-forwarding still correct (forward, invalidate-at-boundary, re-store)
at -O0/-O1.

Diffstat:
Mlang/c/parse/parse.c | 9++++++++-
Mlang/c/parse/parse_init.c | 4++--
Mlang/c/parse/parse_priv.h | 6++++++
Msrc/cg/fold.c | 28+++++++++++++++++++++++++++-
Msrc/cg/internal.h | 13++++++++++++-
Msrc/cg/session.c | 2++
Rtest/hosted/cases/hello.c -> test/cross/cases/hello.c | 0
Rtest/hosted/cases/hello.expected -> test/cross/cases/hello.expected | 0
Rtest/hosted/cases/hello.stdout -> test/cross/cases/hello.stdout | 0
Dtest/hosted/run.sh | 143-------------------------------------------------------------------------------
Dtest/lib/exec_rv32_bare.sh | 213-------------------------------------------------------------------------------
Dtest/smoke/freestanding_system.sh | 393-------------------------------------------------------------------------------
12 files changed, 57 insertions(+), 754 deletions(-)

diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -928,7 +928,14 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { * variable is fully written before any read the store is dead and the * optimizer drops it. VLAs and incomplete arrays are left alone. */ pcg_set_loc(p, loc); - zero_init_at(p, s, var_ty, 0, var_ty); + /* Zero the whole object in one memset rather than recursing to one store + * per scalar leaf. The per-leaf path is O(leaves^2) on large aggregates + * (each leaf store stashes a fresh frontend temp, growing g->nlocals, + * which api_local_const_address_taken then re-scans on every store) and + * explodes under ASAN — e.g. a 28 KB manifest local in src/api/package.c + * stalled the debug bootstrap for minutes. memset is also what gcc/clang + * emit for -ftrivial-auto-var-init=zero. */ + zero_object_bytes_at(p, s, var_ty, 0, var_ty); } } } diff --git a/lang/c/parse/parse_init.c b/lang/c/parse/parse_init.c @@ -175,8 +175,8 @@ void push_subobject_lv(Parser* p, FrameSlot slot, const Type* arr_ty, pcg_lv_member(p, (i64)offset, elem_ty, /*bf_off=*/0, /*bf_w=*/0, /*ss=*/0); } -static void zero_object_bytes_at(Parser* p, FrameSlot slot, const Type* arr_ty, - u32 offset, const Type* ty) { +void zero_object_bytes_at(Parser* p, FrameSlot slot, const Type* arr_ty, + u32 offset, const Type* ty) { KitCgMemAccess access = pcg_mem(p, ty); push_subobject_lv(p, slot, arr_ty, offset, ty); pcg_addr(p); diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h @@ -563,6 +563,12 @@ void init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, const Type* ty); void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, const Type* ty); +/* Zero a whole sub-object with a single kit_cg_memset, instead of recursing to + * one scalar store per leaf. Correct for any type (all-zero bytes is the zero + * value of every C type on supported targets) and avoids O(leaves^2) blowup on + * large aggregates. */ +void zero_object_bytes_at(Parser* p, FrameSlot slot, const Type* arr_ty, + u32 offset, const Type* ty); void parse_static_init_at(Parser* p, u8* buf, u32 buflen, u32 offset, const Type* ty); void define_static_object(Parser* p, ObjSymId sym, ObjSecId section_id, diff --git a/src/cg/fold.c b/src/cg/fold.c @@ -554,8 +554,26 @@ void api_local_const_clear(ApiSourceLocal* rec) { } void api_local_const_clear_all(KitCg* g) { + KitCgLocal cur; if (!g) return; - for (u32 i = 0; i < g->nlocals; ++i) api_local_const_clear(&g->locals[i]); + /* Walk only the locals threaded onto the const-active list, not all nlocals: + * clearing an already-invalid local is a no-op, so this set is exactly the + * one worth visiting. Boundary ops (memory/control/address-taken) are hit on + * every call/branch/store, so this O(#tracked) vs O(nlocals) is what keeps a + * function with many locals (e.g. a large zeroed aggregate) out of O(n^2). */ + cur = g->const_head; + while (cur != KIT_CG_LOCAL_NONE) { + ApiSourceLocal* rec = api_local_from_handle(g, cur); + KitCgLocal next = rec ? rec->const_next : KIT_CG_LOCAL_NONE; + if (rec) { + rec->const_valid = 0; + rec->const_value = 0; + rec->const_listed = 0; + rec->const_next = KIT_CG_LOCAL_NONE; + } + cur = next; + } + g->const_head = KIT_CG_LOCAL_NONE; } void api_local_const_memory_boundary(KitCg* g) { api_local_const_clear_all(g); } @@ -604,6 +622,14 @@ void api_local_const_store(KitCg* g, KitCgLocal local, KitCgMemAccess access, } rec->const_value = api_fold_result(g->c, ty, (u64)value, width); rec->const_valid = 1; + /* Thread onto the const-active list so the next boundary can find and clear + * it without scanning all nlocals. A local already listed (possibly cleared + * since) stays linked once; membership is independent of const_valid. */ + if (!rec->const_listed) { + rec->const_next = g->const_head; + g->const_head = local; + rec->const_listed = 1; + } } int api_local_const_load(KitCg* g, KitCgLocal local, KitCgMemAccess access, diff --git a/src/cg/internal.h b/src/cg/internal.h @@ -123,10 +123,16 @@ typedef struct ApiSourceLocal { CGLocalDesc desc; CGLocal storage; i64 const_value; + /* Intrusive link for the const-active list (KitCg.const_head; see fold.c). + * KIT_CG_LOCAL_NONE terminates. const_listed marks membership so a re-store + * does not double-link; it stays set (the entry lingers as a cleared no-op) + * until the next boundary drains the list. */ + KitCgLocal const_next; u32 param_index; u8 kind; u8 const_valid; - u8 pad[2]; + u8 const_listed; + u8 pad[1]; } ApiSourceLocal; struct KitCg { @@ -151,6 +157,11 @@ struct KitCg { ApiSourceLocal* locals; u32 nlocals; u32 locals_cap; + /* Head of the const-active list threaded through ApiSourceLocal.const_next: + * the locals that currently hold (or recently held, pending a boundary) a + * forwardable constant. Lets the const-tracker clear in O(#tracked) rather + * than scanning all nlocals on every store/call/branch/address-of. */ + KitCgLocal const_head; KitCgTypeId fn_ret_type; /* KIT_CG_TYPE_NONE/void == no result */ SrcLoc cur_loc; diff --git a/src/cg/session.c b/src/cg/session.c @@ -46,6 +46,7 @@ static void cg_free_obj_state(KitCg* g) { g->sp = 0; g->cap = 0; g->nlocals = 0; + g->const_head = KIT_CG_LOCAL_NONE; g->locals_cap = 0; g->sym_cap = 0; g->fn_ret_type = 0; @@ -399,6 +400,7 @@ void kit_cg_func_begin_attrs(KitCg* g, KitCgSym cg_sym, g->fn_ret_type = cg_type_func_ret_id(c, fty); g->nlocals = 0; + g->const_head = KIT_CG_LOCAL_NONE; g->sp = 0; if (g->debug) { diff --git a/test/hosted/cases/hello.c b/test/cross/cases/hello.c diff --git a/test/hosted/cases/hello.expected b/test/cross/cases/hello.expected diff --git a/test/hosted/cases/hello.stdout b/test/cross/cases/hello.stdout diff --git a/test/hosted/run.sh b/test/hosted/run.sh @@ -1,143 +0,0 @@ -#!/usr/bin/env bash -# test/hosted/run.sh — the hosted test suite: build each C case for every -# (target, link-mode) config in the support set with scripts/hosted.sh, run it -# through the shared seam test/lib/exec_target.sh, and check exit code + stdout -# against the oracle (<name>.expected / <name>.stdout). The first principled -# cross-OS hosted-exec suite; seed case is cases/hello.c. -# -# Full matrix (15 configs): -# linux {aa64,x64,rv64} x {musl-static, musl-dynamic, glibc} (podman: alpine -# for musl, debian for glibc; each routed by its exec tag) -# macos-aarch64 (native) -# windows {x64,aarch64} (VM) -# freebsd {amd64,aarch64,riscv64} (VM) -# -# Two verdicts per (case, config): ":build" (cc+link ok) and ":run" (right exit -# code + stdout). A target whose sysroot is absent SKIPs build; a config with no -# runner here SKIPs run. The tag carries the libc, so one flush routes every -# config to its runner. -# -# Default config set: Linux + macOS (fast). FreeBSD + Windows (VMs) are added by -# KIT_HOSTED_VM=1. env: KIT, HOSTED_CONFIGS (override the list), KIT_HOSTED_VM, -# EXEC_VM_KEEP_UP. - -set -u - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -KIT="${KIT:-$ROOT/build/kit}" -HOSTED="$ROOT/scripts/hosted.sh" -CASES="$ROOT/test/hosted/cases" -BUILD_DIR="$ROOT/build/test/hosted" - -# shellcheck source=../lib/kit_sh_report.sh -. "$ROOT/test/lib/kit_sh_report.sh" -# shellcheck source=../lib/exec_target.sh -. "$ROOT/test/lib/exec_target.sh" -kit_report_init -trap exec_target_teardown_all EXIT - -[ -x "$KIT" ] || { echo "hosted: kit not found at $KIT (run 'make bin')" >&2; exit 2; } - -# exec_target's caller contract for the linux/macos runners (VM tags ignore these). -have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 -QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}" -QEMU_RV64_BIN="${QEMU_RV64_BIN:-$(command -v qemu-riscv64 2>/dev/null || true)}" -have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1 -case "$(uname -m 2>/dev/null)" in aarch64|arm64) is_aarch64=1 ;; *) is_aarch64=0 ;; esac -export have_podman QEMU_BIN QEMU_RV64_BIN have_qemu is_aarch64 -mkdir -p "$BUILD_DIR" -EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"; export EXEC_TARGET_MOUNT_ROOT - -# ---- config list ----------------------------------------------------------- -# A config is "<target>[:<mode>]". mode (static|dynamic) only varies for musl; -# glibc is dynamic-only, freebsd is static, windows/macos have one shape. -LINUX_CONFIGS="" -for a in aa64 x64 rv64; do - LINUX_CONFIGS="$LINUX_CONFIGS linux-musl-$a:static linux-musl-$a:dynamic linux-glibc-$a" -done -DEFAULT_CONFIGS="$LINUX_CONFIGS macos-aarch64" -VM_CONFIGS="freebsd-amd64 freebsd-aarch64 freebsd-riscv64 windows-x64 windows-aarch64" -if [ -n "${HOSTED_CONFIGS:-}" ]; then - CONFIGS="$HOSTED_CONFIGS" -else - CONFIGS="$DEFAULT_CONFIGS" - [ "${KIT_HOSTED_VM:-0}" = 1 ] && CONFIGS="$CONFIGS $VM_CONFIGS" -fi - -# Link flags for a (target, mode): musl honors the mode; freebsd is self-contained -# static; glibc/windows/macos use their default shape (glibc is dynamic-only). -link_flags_for() { - local target="$1" mode="$2" - case "$target" in - linux-musl-*) [ "$mode" = static ] && echo -static ;; - freebsd-*) echo -static ;; - *) echo ;; - esac -} - -# ---- build + queue --------------------------------------------------------- -H_NAME=(); H_OUT=(); H_RC=(); H_EXP=(); H_STDOUT=() - -printf 'hosted: configs=%s\n' "$CONFIGS" - -for case_src in "$CASES"/*.c; do - cbase="$(basename "${case_src%.c}")" - exp=0; [ -f "${case_src%.c}.expected" ] && exp="$(cat "${case_src%.c}.expected")" - want_out=""; [ -f "${case_src%.c}.stdout" ] && want_out="$(cat "${case_src%.c}.stdout")" - for config in $CONFIGS; do - target="${config%%:*}"; mode="${config#*:}"; [ "$mode" = "$config" ] && mode="" - os="${target%%-*}" - label="$cbase/$config" - sr="$("$HOSTED" path "$target" 2>/dev/null)" - if [ "$os" != macos ] && { [ -z "$sr" ] || [ ! -d "$sr" ]; }; then - kit_skip "$label:build" "missing sysroot (scripts/hosted.sh prepare $target)" - continue - fi - cdir="$BUILD_DIR/$(printf '%s' "$config" | tr ':/' '__')"; mkdir -p "$cdir" - ext=""; [ "$os" = windows ] && ext=".exe" - exe="$cdir/$cbase$ext" - # shellcheck disable=SC2046 - if ! "$HOSTED" cc "$target" $(link_flags_for "$target" "$mode") "$case_src" -o "$exe" \ - > "$cdir/$cbase.cc.out" 2> "$cdir/$cbase.cc.err"; then - kit_fail "$label:build" "hosted.sh cc failed" - sed 's/^/ | /' "$cdir/$cbase.cc.err" | head -20 - continue - fi - kit_pass "$label:build" - - tag="$("$HOSTED" tag "$target")" - if ! exec_target_supported "$tag"; then - kit_skip "$label:run" "no runner for $tag" - continue - fi - exec_target_queue "$tag" "$label" "$exe" \ - "$cdir/$cbase.out" "$cdir/$cbase.err" "$cdir/$cbase.rc" - H_NAME+=("$label:run"); H_OUT+=("$cdir/$cbase.out") - H_RC+=("$cdir/$cbase.rc"); H_EXP+=("$exp"); H_STDOUT+=("$want_out") - done -done - -# ---- execute + check ------------------------------------------------------- -exec_target_flush - -i=0; n="${#H_NAME[@]}" -while [ "$i" -lt "$n" ]; do - name="${H_NAME[$i]}"; exp=$(( ${H_EXP[$i]} & 255 )) - rc="$(cat "${H_RC[$i]}" 2>/dev/null || echo 127)" - got_out="$(cat "${H_OUT[$i]}" 2>/dev/null || true)" - if ! case "$rc" in ''|*[!0-9-]*) false ;; *) true ;; esac; then - kit_fail "$name" "did not run (rc=$rc)" - elif [ "$(( rc & 255 ))" -ne "$exp" ]; then - kit_fail "$name" "expected rc $exp, got $rc" - elif [ -n "${H_STDOUT[$i]}" ] && [ "$got_out" != "${H_STDOUT[$i]}" ]; then - kit_fail "$name" "stdout mismatch" - printf ' want: %s\n got: %s\n' "${H_STDOUT[$i]}" "$got_out" - else - kit_pass "$name" - fi - i=$((i + 1)) -done - -KIT_SKIP_IS_FAILURE=0 -kit_summary test-hosted -kit_exit diff --git a/test/lib/exec_rv32_bare.sh b/test/lib/exec_rv32_bare.sh @@ -1,213 +0,0 @@ -#!/usr/bin/env bash -# test/lib/exec_rv32_bare.sh — shared bare-metal execution helper for the rv32 -# cross-test lane (path "V") of the corpus harnesses (test/toy/run.sh and -# test/parse/run.sh). -# -# rv32 is a freestanding `-none-elf` target with no qemu-user / podman path -# (unlike the aa64/x64/rv64 Linux cross lanes that go through exec_target.sh). -# A corpus object — whose `main` returns an exit code — is instead linked with a -# bare-metal startup that sets the stack, enables the FPU (ilp32f), calls main, -# and reports its return through a SiFive test finisher, then run under -# qemu-system-riscv32 -machine virt. The qemu exit code equals main's return -# (0 -> 0x5555 -> qemu exit 0; N -> 0x3333|(N<<16) -> qemu exit N), so the -# corpus's existing `rc == expected` oracle applies unchanged. -# -# The link uses `kit ld` (a freestanding rv32 target defaults to no-PIE and -# auto-links no runtime, so the corpus + the kit runtime archive are supplied -# explicitly), exercising the full kit toolchain end to end. The startup stub is -# kit-assembled (`kit as`) too — kit's assembler now emits the `csrs` CSR pseudo -# used to enable the FPU — so the whole image is built by kit. -# -# Public API (after sourcing): -# rv32_bare_setup <workdir> populate RV32_BARE_OK (0/1) and cache -# the startup/wrapper/linkscript + rt. -# rv32_bare_run <obj> <work> <rcfile> link <obj> into a bootable image and -# run it; write the qemu exit code to -# <rcfile>. Echoes a one-line reason and -# returns: 0 ran (rc in <rcfile>), -# 2 link/build failure (caller decides). - -# shellcheck disable=SC2034 # RV32_BARE_* are consumed by the sourcing harness. - -_rv32_bare_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -# shellcheck source=check_rv32_env.sh -. "$_rv32_bare_root/test/lib/check_rv32_env.sh" - -RV32_BARE_OK=0 -RV32_BARE_KIT="${KIT:-$_rv32_bare_root/build/kit}" -# Entry symbol the wrapper calls and reports the exit code of. Toy cases define -# `main`; the C parse corpus defines `test_main`. Set before rv32_bare_setup. -RV32_BARE_ENTRY="${RV32_BARE_ENTRY:-main}" -RV32_BARE_QEMU="" -RV32_BARE_RT="" -RV32_BARE_START="" -RV32_BARE_WRAP="" -RV32_BARE_LDS="" -# ilp32f: hardware single float + soft double + i64 — the verified rv32 profile. -RV32_BARE_MARCH="rv32imafc_zicsr_zifencei" -RV32_BARE_MABI="ilp32f" - -rv32_bare_setup() { - local work="$1" - RV32_BARE_OK=0 - check_rv32_env >/dev/null 2>&1 - # The whole image (corpus, startup stub, wrapper, runtime archive, link) is - # built by kit — `kit cc` / `kit as` / `kit ld` — and run under - # qemu-system-riscv32, so the only external prerequisite is qemu. (clang is - # not invoked anywhere on this path; it remains a gate only for the smoke - # lane's ld.lld doctor in check_rv32_env.) - if [ "${RV32_HAVE_QEMU_SYSTEM:-0}" -ne 1 ]; then - return 0 - fi - RV32_BARE_QEMU="${RV32_QEMU_SYSTEM_BIN:-qemu-system-riscv32}" - [ -x "$RV32_BARE_KIT" ] || return 0 - - # The freestanding runtime (i64 mul/div/shift + soft double helpers). Build it - # on demand; without it, link of any corpus case touching those would fail. - RV32_BARE_RT="$_rv32_bare_root/build/rt/riscv32-elf-hardfloat/libkit_rt.a" - if [ ! -f "$RV32_BARE_RT" ]; then - make -C "$_rv32_bare_root" rt-riscv32-elf-hardfloat >/dev/null 2>&1 || true - fi - [ -f "$RV32_BARE_RT" ] || return 0 - - mkdir -p "$work" - RV32_BARE_START="$work/_rv32_start.o" - RV32_BARE_WRAP="$work/_rv32_wrap.o" - RV32_BARE_LDS="$work/_rv32.lds" - - # Bare-metal reset entry: stack at the top of `virt` RAM, enable the FPU - # (mstatus.FS=Initial) for ilp32f, then call the C wrapper. - cat > "$work/_rv32_start.S" <<'EOF' -.section .text.start,"ax",@progbits -.globl _start -_start: - li sp, 0x80100000 - li t0, 0x2000 - csrs mstatus, t0 # mstatus.FS = Initial (enable the FPU for ilp32f) - - // ---- static thread-local storage setup ----------------------------------- - // Build the per-thread TLS image [TCB(16) | .tdata | .tbss] in RAM and point - // tp at it. The corpus's TPREL relocs were resolved by kit-ld's scripted path - // (which leaves img->tls_vaddr == 0) to tgt->vaddr + 16, so we must set - // tp = __rv32_tls_block - __rv32_tdata_lma - // making tp + (tgt->vaddr + 16) = __rv32_tls_block + 16 + off — the address - // inside the copy where we place each variable. See the linker-script comment. - la t0, __rv32_tls_block # t0 = block base (TCB at +0) - addi t1, t0, 16 # t1 = dst = block + 16 (.tdata copy start) - la t2, __rv32_tdata_lma # t2 = src = .tdata load image - la t3, __rv32_tdata_size # t3 = .tdata byte count (abs symbol: la yields value) -.Lcopy: # copy .tdata init image, byte at a time - beqz t3, .Lcopy_done - lbu t4, 0(t2) - sb t4, 0(t1) - addi t1, t1, 1 - addi t2, t2, 1 - addi t3, t3, -1 - j .Lcopy -.Lcopy_done: - la t3, __rv32_tbss_size # t3 = .tbss byte count -.Lzero: # zero-fill .tbss (t1 already at end of .tdata copy) - beqz t3, .Lzero_done - sb zero, 0(t1) - addi t1, t1, 1 - addi t3, t3, -1 - j .Lzero -.Lzero_done: - la t2, __rv32_tdata_lma - sub tp, t0, t2 # tp = block - __rv32_tdata_lma - // --------------------------------------------------------------------------- - - call _rv32_cmain -.Lhang: j .Lhang - -// Per-thread TLS image scratch (single-threaded harness). Sized generously; -// the corpus TLS cases use only a handful of bytes. -.section .bss.rv32tls,"aw",@nobits -.balign 16 -__rv32_tls_block: - .zero 4096 -EOF - # The wrapper calls the corpus's main() and maps its return onto the SiFive - # test finisher at 0x100000. Compiled by kit (exercises rv32 codegen for the - # finisher store + the call); main may return i32 or i64 — the low word is the - # exit code. - cat > "$work/_rv32_wrap.c" <<EOF -#define FINISHER ((volatile unsigned int*)0x100000) -extern int ${RV32_BARE_ENTRY}(void); -__attribute__((noreturn)) void _rv32_cmain(void) { - int code = ${RV32_BARE_ENTRY}(); - *FINISHER = code ? (0x3333u | ((unsigned)code << 16)) : 0x5555u; - for (;;) {} -} -EOF - # The TLS output sections below give the bare stub a static-TLS image to - # seed from. kit-ld's *scripted* layout does NOT populate img->tls_vaddr (that - # only happens in the bucketed, scriptless path), so the linker's own - # __tdata_start/__tbss_size boundary symbols and PT_TLS are emitted as zero - # here — and the corpus's TPREL relocs resolve to (tgt->vaddr - 0) + 16 = - # tgt->vaddr + 16, i.e. the *placed vaddr* of the variable plus the 16-byte TCB - # bias (src/obj/elf/link.c:475). We therefore can't lean on the linker's TLS - # symbols; instead we define our own (__rv32_tdata_lma/_tdata_size/_tbss_size) - # that the linker never clobbers, and the stub computes the thread pointer so - # that tp + (tgt->vaddr + 16) lands inside a live RAM copy of the image. - # - # .tdata (PROGBITS, loaded at its vaddr) supplies the init image; .tbss - # (NOBITS) only contributes a size. They are placed contiguously so a single - # tp bias works for both: with the copy laid out as [TCB(16) | .tdata | .tbss] - # and .tdata copied to block+16, set tp = block - __rv32_tdata_lma. Then for - # any var v: tp + (v.vaddr + 16) = (block - tdata_lma) + (tdata_lma + off) + 16 - # = block + 16 + off — exactly where we copied/zeroed v. The - # linker's +16 and our +16 TCB reservation cancel, mirroring start.c's - # [TCB | tdata | tbss] convention (test/link/harness/start.c:146-149). - # - # __x = . assignments inside a section body are all applied *before* that - # section's inputs (link_layout.c:775), so the post-.tdata dot is captured in a - # following input-less marker section; sizes are then derived at top level. - cat > "$RV32_BARE_LDS" <<'EOF' -ENTRY(_start) -SECTIONS { - . = 0x80000000; - .text : { *(.text.start) *(.text*) } - .rodata : { *(.rodata*) } - .data : { *(.data*) } - .tdata : { . = ALIGN(16); __rv32_tdata_lma = .; *(.tdata .tdata.*) } - .tdata_end : { __rv32_tdata_end = .; } - .tbss : { __rv32_tbss_start = .; *(.tbss .tbss.*) } - .tbss_end : { __rv32_tbss_end = .; } - .bss : { *(.bss*) *(COMMON) } - __rv32_tdata_size = __rv32_tdata_end - __rv32_tdata_lma; - __rv32_tbss_size = __rv32_tbss_end - __rv32_tbss_start; - /DISCARD/ : { *(.riscv.attributes) *(.comment) } -} -EOF - if ! "$RV32_BARE_KIT" as -target riscv32-none-elf -march="$RV32_BARE_MARCH" \ - -mabi="$RV32_BARE_MABI" -o "$RV32_BARE_START" "$work/_rv32_start.S" \ - >/dev/null 2>&1; then - return 0 - fi - if ! "$RV32_BARE_KIT" cc -target riscv32-none-elf -march="$RV32_BARE_MARCH" \ - -mabi="$RV32_BARE_MABI" -O1 -ffreestanding -c "$work/_rv32_wrap.c" \ - -o "$RV32_BARE_WRAP" >/dev/null 2>&1; then - return 0 - fi - RV32_BARE_OK=1 -} - -rv32_bare_run() { # <obj> <work> <rcfile> - local obj="$1" work="$2" rcfile="$3" - local elf="$work/$(basename "$obj").elf" - local lderr="$work/$(basename "$obj").rv32ld.err" - if [ "$RV32_BARE_OK" -ne 1 ]; then - echo "rv32 bare-metal toolchain unavailable"; return 2 - fi - if ! "$RV32_BARE_KIT" ld -T "$RV32_BARE_LDS" -e _start \ - "$RV32_BARE_START" "$RV32_BARE_WRAP" "$obj" "$RV32_BARE_RT" \ - -o "$elf" 2>"$lderr"; then - echo "kit ld (rv32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2 - fi - local rc=0 - timeout 20 "$RV32_BARE_QEMU" -machine virt -bios none -kernel "$elf" \ - -nographic -no-reboot >/dev/null 2>&1 || rc=$? - printf '%s' "$rc" > "$rcfile" - return 0 -} diff --git a/test/smoke/freestanding_system.sh b/test/smoke/freestanding_system.sh @@ -1,393 +0,0 @@ -#!/usr/bin/env bash -# test/smoke/freestanding_system.sh - qemu-system bare-metal smoke for -# kit-compiled aarch64/x86_64/riscv64/riscv32 freestanding ELF images. -# -# Each lane builds the same small C payload with `kit cc -target *-none-elf`, -# assembles an arch-specific reset stub with `kit as`, links a static image with -# `kit ld -T`, then boots it under the matching qemu-system binary. This is a -# whole-toolchain smoke: no clang, ld.lld, hosted libc, or qemu-user path is in -# the image build. - -set -u - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -BUILD_DIR="$ROOT/build/test/freestanding-system" -mkdir -p "$BUILD_DIR" - -KIT_KIT_DIR="$ROOT/test/lib" -# shellcheck source=../lib/kit_sh_kit.sh -. "$ROOT/test/lib/kit_sh_kit.sh" -kit_report_init -KIT_SKIP_IS_FAILURE=1 - -KIT="${KIT:-$ROOT/build/kit}" -TIMEOUT="${TIMEOUT:-timeout}" -if ! command -v "$TIMEOUT" >/dev/null 2>&1; then - if command -v gtimeout >/dev/null 2>&1; then - TIMEOUT="gtimeout" - fi -fi - -if [ ! -x "$KIT" ]; then - skip_test "kit" "kit binary not found at $KIT" - kit_summary test-smoke-freestanding-system - kit_exit -fi - -if ! command -v "$TIMEOUT" >/dev/null 2>&1; then - skip_test "timeout" "timeout command unavailable" - kit_summary test-smoke-freestanding-system - kit_exit -fi - -cat > "$BUILD_DIR/app.c" <<'EOF' -static int calc(void) { - volatile unsigned long long a = 0x1122334455667788ull; - volatile unsigned long long b = 0x0102030405060708ull; - unsigned long long c = (a ^ b) + 0x10ull; - if (c != ((0x1122334455667788ull ^ 0x0102030405060708ull) + 0x10ull)) - return 2; - - int sum = 0; - for (int i = 1; i <= 10; ++i) sum += i; - if (sum != 55) return 3; - - volatile long long sx = -7; - if ((sx >> 1) != -4) return 4; - - unsigned x = 0x12345678u; - x = (x << 3) | (x >> 29); - if (x != 0x91a2b3c0u) return 5; - - return 0; -} - -int cmain(void) { return calc(); } -EOF - -have_qemu() { - command -v "$1" >/dev/null 2>&1 -} - -compile_obj() { # <arch-label> <target> <extra...> - local label="$1" target="$2" - shift 2 - local dir="$BUILD_DIR/$label" - mkdir -p "$dir" - "$KIT" cc -target "$target" "$@" -O1 -ffreestanding -c "$BUILD_DIR/app.c" \ - -o "$dir/app.o" 2>"$dir/cc.err" -} - -assemble_obj() { # <label> <target> <src> <extra...> - local label="$1" target="$2" src="$3" - shift 3 - "$KIT" as -target "$target" "$@" -o "$BUILD_DIR/$label/start.o" "$src" \ - 2>"$BUILD_DIR/$label/as.err" -} - -link_elf() { # <label> - local label="$1" dir="$BUILD_DIR/$1" - "$KIT" ld -T "$dir/link.ld" -e _start "$dir/start.o" "$dir/app.o" \ - -o "$dir/kernel.elf" 2>"$dir/ld.err" -} - -record_build_fail() { # <name> <log> - local name="$1" log="$2" - if [ -s "$log" ]; then - not_ok "$name" "$log" - else - not_ok "$name" "command failed with no stderr" - fi -} - -run_aa64() { - local label="aa64" dir="$BUILD_DIR/aa64" - if ! have_qemu qemu-system-aarch64; then - skip_test "$label" "qemu-system-aarch64 unavailable" - return - fi - mkdir -p "$dir" - cat > "$dir/start.S" <<'EOF' -.section .text.start,"ax",@progbits -.globl _start -_start: - adrp x0, stack_top - add x0, x0, :lo12:stack_top - mov sp, x0 - bl cmain - adrp x1, semihost_args - add x1, x1, :lo12:semihost_args - str x0, [x1, #8] - mov x0, #0x20 - hlt #0xf000 -.Lhang: - b .Lhang - -.section .data.semihost,"aw",@progbits -.balign 8 -semihost_args: - .quad 0x20026 - .quad 0 - -.section .bss.stack,"aw",@nobits -.balign 16 -stack_bottom: - .zero 65536 -stack_top: -EOF - cat > "$dir/link.ld" <<'EOF' -ENTRY(_start) -SECTIONS { - . = 0x40080000; - .text : { *(.text.start) *(.text*) } - .rodata : { *(.rodata*) } - .data : { *(.data*) } - .bss : { *(.bss*) *(COMMON) } - /DISCARD/ : { *(.comment) } -} -EOF - if ! compile_obj "$label" aarch64-none-elf; then - record_build_fail "$label cc" "$dir/cc.err"; return - fi - if ! assemble_obj "$label" aarch64-none-elf "$dir/start.S"; then - record_build_fail "$label as" "$dir/as.err"; return - fi - if ! link_elf "$label"; then - record_build_fail "$label ld" "$dir/ld.err"; return - fi - - local rc=0 - "$TIMEOUT" 20 qemu-system-aarch64 -machine virt -cpu cortex-a53 \ - -kernel "$dir/kernel.elf" -display none -serial none -monitor none \ - -semihosting-config enable=on,target=native -no-reboot \ - >"$dir/qemu.out" 2>"$dir/qemu.err" || rc=$? - if [ "$rc" -eq 0 ]; then - ok "$label qemu-system (rc=0)" - else - printf 'expected qemu rc 0, got %s; see %s\n' "$rc" "$dir/qemu.err" \ - > "$dir/qemu.diag" - not_ok "$label qemu-system" "$dir/qemu.diag" - fi -} - -run_riscv() { # <label> <qemu> <target> <march> <mabi> <stack> - local label="$1" qemu="$2" target="$3" march="$4" mabi="$5" stack="$6" - local dir="$BUILD_DIR/$label" - if ! have_qemu "$qemu"; then - skip_test "$label" "$qemu unavailable" - return - fi - mkdir -p "$dir" - cat > "$dir/start.S" <<EOF -.section .text.start,"ax",@progbits -.globl _start -_start: - li sp, $stack - li t0, 0x2000 - csrs mstatus, t0 - call cmain - li t0, 0x100000 - beqz a0, .Lpass - slli a0, a0, 16 - li t1, 0x3333 - or a0, a0, t1 - sw a0, 0(t0) -.Lhang: - j .Lhang -.Lpass: - li t1, 0x5555 - sw t1, 0(t0) - j .Lhang -EOF - cat > "$dir/link.ld" <<'EOF' -ENTRY(_start) -SECTIONS { - . = 0x80000000; - .text : { *(.text.start) *(.text*) } - .rodata : { *(.rodata*) } - .data : { *(.data*) } - .bss : { *(.bss*) *(COMMON) } - /DISCARD/ : { *(.riscv.attributes) *(.comment) } -} -EOF - if ! compile_obj "$label" "$target" -march="$march" -mabi="$mabi"; then - record_build_fail "$label cc" "$dir/cc.err"; return - fi - if ! assemble_obj "$label" "$target" "$dir/start.S" -march="$march" -mabi="$mabi"; then - record_build_fail "$label as" "$dir/as.err"; return - fi - if ! link_elf "$label"; then - record_build_fail "$label ld" "$dir/ld.err"; return - fi - - local rc=0 - "$TIMEOUT" 20 "$qemu" -machine virt -bios none \ - -kernel "$dir/kernel.elf" -nographic -no-reboot \ - >"$dir/qemu.out" 2>"$dir/qemu.err" || rc=$? - if [ "$rc" -eq 0 ]; then - ok "$label qemu-system (rc=0)" - else - printf 'expected qemu rc 0, got %s; see %s\n' "$rc" "$dir/qemu.err" \ - > "$dir/qemu.diag" - not_ok "$label qemu-system" "$dir/qemu.diag" - fi -} - -run_rv64() { - run_riscv rv64 qemu-system-riscv64 riscv64-none-elf \ - rv64imafd_zicsr_zifencei lp64 0x81000000 -} - -run_rv32() { - run_riscv rv32 qemu-system-riscv32 riscv32-none-elf \ - rv32imafc_zicsr_zifencei ilp32f 0x80100000 -} - -emit_x64_start() { - local out="$1" - cat > "$out" <<'EOF' -.section .multiboot,"a",@progbits -.balign 4 -.long 0x1badb002 -.long 0 -.long 0xe4524ffe - -.section .note.Xen,"a",@note -.balign 4 -.long 4 -.long 4 -.long 18 -.ascii "Xen" -.byte 0 -.long _start - -.section .text.start,"ax",@progbits -.globl _start -_start: - .byte 0xfa /* cli */ - .byte 0xbc /* mov $stack_top, %esp */ - .long stack_top - .byte 0xb8 /* mov $pml4, %eax */ - .long pml4 - .byte 0x0f, 0x22, 0xd8 /* mov %eax, %cr3 */ - .byte 0x0f, 0x20, 0xe0 /* mov %cr4, %eax */ - .byte 0x83, 0xc8, 0x20 /* or $CR4_PAE, %eax */ - .byte 0x0f, 0x22, 0xe0 /* mov %eax, %cr4 */ - .byte 0xb9, 0x80, 0x00, 0x00, 0xc0 /* mov $EFER, %ecx */ - .byte 0x0f, 0x32 /* rdmsr */ - .byte 0x0d, 0x00, 0x01, 0x00, 0x00 /* or $EFER_LME, %eax */ - .byte 0x0f, 0x30 /* wrmsr */ - .byte 0x0f, 0x20, 0xc0 /* mov %cr0, %eax */ - .byte 0x0d, 0x00, 0x00, 0x00, 0x80 /* or $CR0_PG, %eax */ - .byte 0x0f, 0x22, 0xc0 /* mov %eax, %cr0 */ - .byte 0x0f, 0x01, 0x15 /* lgdt gdt_desc */ - .long gdt_desc - .byte 0xea /* ljmp $0x08,$long_entry */ - .long long_entry - .hword 0x08 - -long_entry: - .byte 0x66, 0xb8, 0x10, 0x00 /* mov $0x10, %ax */ - .byte 0x8e, 0xd8 /* mov %ax, %ds */ - .byte 0x8e, 0xc0 /* mov %ax, %es */ - .byte 0x8e, 0xd0 /* mov %ax, %ss */ - .byte 0x48, 0xbc /* movabs $stack_top, %rsp */ - .quad stack_top - call cmain - .byte 0x66, 0xba, 0x01, 0x05 /* mov $0x501, %dx */ - .byte 0x66, 0xef /* outw %ax, %dx */ -.Lhang: - .byte 0xf4 /* hlt */ - jmp .Lhang - -.section .data.boot,"aw",@progbits -.balign 8 -gdt: - .quad 0 - .quad 0x00af9a000000ffff - .quad 0x00af92000000ffff -gdt_desc: - .hword 23 - .long gdt - -.balign 4096 -pml4: - .quad pdpt + 0x3 -EOF - for _ in $(seq 1 511); do printf ' .quad 0\n' >> "$out"; done - cat >> "$out" <<'EOF' -.balign 4096 -pdpt: - .quad pd + 0x3 -EOF - for _ in $(seq 1 511); do printf ' .quad 0\n' >> "$out"; done - printf '.balign 4096\npd:\n' >> "$out" - local i=0 - while [ "$i" -lt 512 ]; do - printf ' .quad 0x%016x\n' $((i * 0x200000 + 0x83)) >> "$out" - i=$((i + 1)) - done - cat >> "$out" <<'EOF' - -.section .bss.stack,"aw",@nobits -.balign 16 -stack_bottom: - .zero 65536 -stack_top: -EOF -} - -run_x64() { - local label="x64" dir="$BUILD_DIR/x64" - if ! have_qemu qemu-system-x86_64; then - skip_test "$label" "qemu-system-x86_64 unavailable" - return - fi - mkdir -p "$dir" - emit_x64_start "$dir/start.S" - cat > "$dir/link.ld" <<'EOF' -ENTRY(_start) -SECTIONS { - . = 0x100000; - .multiboot : { *(.multiboot) } - .note.Xen : { *(.note.Xen) } - .text : { *(.text.start) *(.text*) } - .rodata : { *(.rodata*) } - .data.boot : ALIGN(4096) { *(.data.boot) } - .data : { *(.data*) } - .bss.stack : ALIGN(16) { *(.bss.stack) } - .bss : { *(.bss*) *(COMMON) } - /DISCARD/ : { *(.comment) } -} -EOF - if ! compile_obj "$label" x86_64-none-elf; then - record_build_fail "$label cc" "$dir/cc.err"; return - fi - if ! assemble_obj "$label" x86_64-none-elf "$dir/start.S"; then - record_build_fail "$label as" "$dir/as.err"; return - fi - if ! link_elf "$label"; then - record_build_fail "$label ld" "$dir/ld.err"; return - fi - - local rc=0 want=1 - "$TIMEOUT" 20 qemu-system-x86_64 -kernel "$dir/kernel.elf" \ - -device isa-debug-exit,iobase=0x501,iosize=0x02 \ - -display none -serial none -monitor none -no-reboot \ - >"$dir/qemu.out" 2>"$dir/qemu.err" || rc=$? - if [ "$rc" -eq "$want" ] && ! grep -q 'Error loading' "$dir/qemu.err"; then - ok "$label qemu-system (guest rc=0)" - else - printf 'expected qemu rc %s (guest rc=0), got %s; see %s\n' \ - "$want" "$rc" "$dir/qemu.err" > "$dir/qemu.diag" - not_ok "$label qemu-system" "$dir/qemu.diag" - fi -} - -run_aa64 -run_x64 -run_rv64 -run_rv32 - -kit_summary test-smoke-freestanding-system -kit_exit