kit

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

commit ae0c391c593ccc327d48423e6103a9ebf16cf397
parent 3334513345b6bc6fa45adec0119683b7951256b8
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun, 10 May 2026 11:20:01 -0700

test: parameterize harnesses by CFREE_TEST_ARCH

Wires the cg / elf / link / parse runners to honor CFREE_TEST_ARCH
(aa64 / x64 / rv64), defaulting to aa64 to preserve historical behavior.
The C runners read the env var via a new test/lib/cfree_test_target.h;
each run.sh derives the matching clang `--target=` triple, exec_target
arch label, and a host-arch-vs-target-arch native gate that controls
the in-process and JIT lanes.

Also extends src/obj/elf_read.c to dispatch the reloc_from table by
e_machine (aarch64 / x86_64 / riscv64), and rewrites
test/link/harness/start.c with per-arch syscall + TLS prologues so
path E links on every arch.

Negative test corpus updated: e_machine_x86 → e_machine_unknown
(x86_64 is now supported); reloc_type_unsupported stderr substring
matches the new generic message.

Outcome: aa64 default behavior unchanged. CFREE_TEST_ARCH=x64 / rv64
runs each suite end-to-end; failures surface real stub gaps
(cg/codegen panics, missing linker reloc handlers, unmapped reloc
types) rather than harness deficiencies.

Diffstat:
Msrc/obj/elf_read.c | 29+++++++++++++++++++++--------
Mtest/cg/harness/cg_runner.c | 20+++++++++-----------
Mtest/cg/run.sh | 60+++++++++++++++++++++++++++++++++++++++++-------------------
Atest/elf/bad/e_machine_unknown.elf | 0
Atest/elf/bad/e_machine_unknown.expect | 2++
Dtest/elf/bad/e_machine_x86.elf | 0
Dtest/elf/bad/e_machine_x86.expect | 2--
Mtest/elf/bad/gen.py | 12++++++++----
Mtest/elf/bad/reloc_type_unsupported.expect | 4++--
Mtest/elf/run.sh | 15+++++++++++++--
Mtest/elf/unit/align_4k.c | 11+++++------
Mtest/elf/unit/smoke.c | 12+++++-------
Atest/lib/cfree_test_target.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/link/harness/jit_runner.c | 10++++++----
Mtest/link/harness/link_exe_runner.c | 10++++++----
Mtest/link/harness/start.c | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/link/run.sh | 46++++++++++++++++++++++++++++++++++++----------
Mtest/parse/harness/parse_runner.c | 19+++++++++----------
Mtest/parse/run.sh | 51++++++++++++++++++++++++++++++++++++---------------
Mtest/test.mk | 2+-
20 files changed, 308 insertions(+), 107 deletions(-)

diff --git a/src/obj/elf_read.c b/src/obj/elf_read.c @@ -222,10 +222,21 @@ ObjBuilder* read_elf(Compiler* c, const char* name, const u8* data, (u32)e_type); u16 e_machine = elf_rd_u16(data + 18); - if (e_machine != EM_AARCH64) - compiler_panic(c, no_loc(), - "read_elf: unsupported e_machine 0x%x (only AArch64)", - (u32)e_machine); + u32 (*reloc_from)(u32); + switch (e_machine) { + case EM_AARCH64: + reloc_from = elf_aarch64_reloc_from; + break; + case EM_X86_64: + reloc_from = elf_x86_64_reloc_from; + break; + case EM_RISCV: + reloc_from = elf_riscv64_reloc_from; + break; + default: + compiler_panic(c, no_loc(), + "read_elf: unsupported e_machine 0x%x", (u32)e_machine); + } u64 e_shoff = elf_rd_u64(data + 40); u16 e_shentsize = elf_rd_u16(data + 58); @@ -432,10 +443,11 @@ ObjBuilder* read_elf(Compiler* c, const char* name, const u8* data, u32 esym = ELF64_R_SYM(r_info); u32 etype = ELF64_R_TYPE(r_info); - u32 kind = elf_aarch64_reloc_from(etype); + u32 kind = reloc_from(etype); if (kind == (u32)-1) compiler_panic(c, no_loc(), - "read_elf: unsupported AArch64 reloc type %u", etype); + "read_elf: unsupported reloc type %u for e_machine 0x%x", + etype, (u32)e_machine); ObjSymId target_sym = OBJ_SYM_NONE; if (esym && sym_elf_to_obj && esym < nsyms) @@ -535,9 +547,10 @@ ObjBuilder* read_elf_dso(Compiler* c, const char* name, const u8* data, (u32)e_type); u16 e_machine = elf_rd_u16(data + 18); - if (e_machine != EM_AARCH64) + if (e_machine != EM_AARCH64 && e_machine != EM_X86_64 && + e_machine != EM_RISCV) compiler_panic(c, no_loc(), - "read_elf_dso: unsupported e_machine 0x%x (only AArch64)", + "read_elf_dso: unsupported e_machine 0x%x", (u32)e_machine); u64 e_phoff = elf_rd_u64(data + 32); diff --git a/test/cg/harness/cg_runner.c b/test/cg/harness/cg_runner.c @@ -29,6 +29,7 @@ #include "core/core.h" #include "core/pool.h" #include "debug/debug.h" +#include "lib/cfree_test_target.h" #include "link/link.h" #include "obj/obj.h" #include "opt/opt.h" @@ -229,14 +230,11 @@ static const CgCase* find_case(const char* name) { return NULL; } -static void target_aarch64_linux(CfreeTarget* t) { - memset(t, 0, sizeof *t); - t->arch = CFREE_ARCH_ARM_64; - t->os = CFREE_OS_LINUX; - t->obj = CFREE_OBJ_ELF; - t->ptr_size = 8; - t->ptr_align = 8; - t->big_endian = 0; +static void target_from_env(CfreeTarget* t) { + if (cfree_test_target_init(t) != 0) { + fprintf(stderr, "cg-runner: cfree_test_target_init failed\n"); + exit(2); + } } /* Has this case registered any path-W DWARF directives? Used to decide @@ -396,7 +394,7 @@ static int mode_dump_tape(const char* name) { } CfreeTarget target; - target_aarch64_linux(&target); + target_from_env(&target); CfreeEnv env; memset(&env, 0, sizeof env); env.heap = &g_heap; @@ -465,7 +463,7 @@ static int mode_emit(const char* name, const char* out_path) { } CfreeTarget target; - target_aarch64_linux(&target); + target_from_env(&target); CfreeEnv env; memset(&env, 0, sizeof env); env.heap = &g_heap; @@ -525,7 +523,7 @@ static int mode_jit(const char* name) { } CfreeTarget target; - target_aarch64_linux(&target); + target_from_env(&target); CfreeEnv env; memset(&env, 0, sizeof env); env.heap = &g_heap; diff --git a/test/cg/run.sh b/test/cg/run.sh @@ -43,9 +43,23 @@ JIT_RUNNER="$BUILD_DIR/jit-runner" DWARF_CHECK="$BUILD_DIR/cg-check-dwarf" NORMALIZE="$ROOT/test/elf/normalize.py" -CLANG_TARGET="--target=aarch64-linux-gnu" +# CFREE_TEST_ARCH selects the cross-target the harness drives the +# compiler at. Default aa64 preserves historical behavior. The runners +# (cg-runner / link-exe-runner / jit-runner) read the same env var via +# test/lib/cfree_test_target.h, so the C side and the shell side stay +# in lockstep. +CFREE_TEST_ARCH="${CFREE_TEST_ARCH:-aa64}" +case "$CFREE_TEST_ARCH" in + aa64|aarch64|arm64) TEST_ARCH=aa64; CLANG_TRIPLE=aarch64-linux-gnu; EXEC_ARCH=aarch64 ;; + x64|x86_64|amd64) TEST_ARCH=x64; CLANG_TRIPLE=x86_64-linux-gnu; EXEC_ARCH=x64 ;; + rv64|riscv64) TEST_ARCH=rv64; CLANG_TRIPLE=riscv64-linux-gnu; EXEC_ARCH=rv64 ;; + *) printf 'unknown CFREE_TEST_ARCH=%s\n' "$CFREE_TEST_ARCH" >&2; exit 2 ;; +esac +export CFREE_TEST_ARCH + +CLANG_TARGET="--target=$CLANG_TRIPLE" CC="${CC:-cc}" -CFREE_CFLAGS="-I$ROOT/include -I$ROOT/src -I$TEST_DIR/harness" +CFREE_CFLAGS="-I$ROOT/include -I$ROOT/src -I$ROOT/test -I$TEST_DIR/harness" ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" # Filters (env vars or positional args; args win): @@ -111,6 +125,16 @@ command -v podman >/dev/null 2>&1 && have_podman=1 arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 +# is_native_target=1 when the cross-target arch matches the host arch. +# Path D (in-process JIT) and path J (jit-runner) require native execution +# of cfree-emitted code; on a non-matching host we skip them. +is_native_target=0 +case "$TEST_ARCH" in + aa64) [ $is_aarch64 -eq 1 ] && is_native_target=1 ;; + x64) { [ "$arch_raw" = "x86_64" ] || [ "$arch_raw" = "amd64" ]; } && is_native_target=1 ;; + rv64) [ "$arch_raw" = "riscv64" ] && is_native_target=1 ;; +esac + READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" # Shared per-arch exec helper — see test/lib/exec_target.sh. Path E @@ -190,10 +214,11 @@ else have_exe_runner=1 fi -# jit-runner — for path J. Only on aarch64 host. -if [ $is_aarch64 -eq 1 ]; then +# jit-runner — for path J. Only when the host arch matches the cross-target +# (otherwise the JIT can't execute the emitted code natively). +if [ $is_native_target -eq 1 ]; then if [ ! -x "$JIT_RUNNER" ]; then - if $CC -I"$ROOT/include" "$LINK_TEST_DIR/harness/jit_runner.c" \ + if $CC -I"$ROOT/include" -I"$ROOT/test" "$LINK_TEST_DIR/harness/jit_runner.c" \ "$LIB_AR" -o "$JIT_RUNNER" 2>"$BUILD_DIR/jit-runner.err"; then have_jit_runner=1 printf ' %s jit-runner\n' "$(color_grn built)" @@ -265,19 +290,16 @@ for OPT_LEVEL in $OPT_LEVELS; do # negative-return cases compare correctly. expected_byte=$(( expected & 0xff )) - # Path E target arch. cg-runner --arches NAME prints the arches a - # case can run on (one per line). Today every case is aarch64-only; - # multi-arch cases will land alongside x64 codegen in MULTIARCH - # phase 3 and broaden this list. - case_arches="$("${CG_RUN[@]}" --arches "$name" 2>/dev/null)" - case_arches="${case_arches:-aarch64}" - # First arch is the canonical one path E targets. (Cases are still - # single-arch through phase 2; the loop is a placeholder seam.) - case_arch="$(printf '%s\n' "$case_arches" | head -n1)" - - # ---- Path D: in-process JIT (only on aarch64) ------------------------ + # Path E target arch. The shell drives every case at the + # CFREE_TEST_ARCH-selected target — emit panics on stub backends + # surface as case failures rather than harness skips, which is + # the multi-arch contract through Phase 2. cg-runner's --arches + # output is informational at this stage. + case_arch="$EXEC_ARCH" + + # ---- Path D: in-process JIT (only when host arch == cross-target) ---- if [ $RUN_D -eq 1 ]; then - if [ $is_aarch64 -eq 1 ]; then + if [ $is_native_target -eq 1 ]; then t0=$(now_ms) "${CG_RUN[@]}" --jit "$name" >"$work/d.out" 2>"$work/d.err" d_rc=$? @@ -288,7 +310,7 @@ for OPT_LEVEL in $OPT_LEVELS; do note_fail "$name/D${TAG} (expected $expected_byte got $d_rc, ${dt}ms)" fi else - note_skip "$name/D${TAG}" "not on aarch64 host" + note_skip "$name/D${TAG}" "host arch != $TEST_ARCH (no native JIT)" fi fi @@ -366,7 +388,7 @@ for OPT_LEVEL in $OPT_LEVELS; do note_fail "$name/J${TAG} (expected $expected_byte got $j_rc, ${dt}ms)" fi else - note_skip "$name/J${TAG}" "no jit-runner (not aarch64 host)" + note_skip "$name/J${TAG}" "no jit-runner (host arch != $TEST_ARCH)" fi fi diff --git a/test/elf/bad/e_machine_unknown.elf b/test/elf/bad/e_machine_unknown.elf Binary files differ. diff --git a/test/elf/bad/e_machine_unknown.expect b/test/elf/bad/e_machine_unknown.expect @@ -0,0 +1 @@ +not a recognized object file +\ No newline at end of file diff --git a/test/elf/bad/e_machine_x86.elf b/test/elf/bad/e_machine_x86.elf Binary files differ. diff --git a/test/elf/bad/e_machine_x86.expect b/test/elf/bad/e_machine_x86.expect @@ -1 +0,0 @@ -unsupported e_machine -\ No newline at end of file diff --git a/test/elf/bad/gen.py b/test/elf/bad/gen.py @@ -108,10 +108,14 @@ def m_wrong_endian(buf): return b -@case("e_machine_x86", "unsupported e_machine") -def m_e_machine_x86(buf): +@case("e_machine_unknown", "not a recognized object file") +def m_e_machine_unknown(buf): + """ELF reader supports aarch64 / x86_64 / riscv64. Use an e_machine + outside that set; cfree_detect_target rejects it before read_elf + sees it, with the same "not a recognized object file" diagnostic + bad_magic produces.""" b = bytearray(buf) - struct.pack_into("<H", b, 18, 0x3E) # EM_X86_64 + struct.pack_into("<H", b, 18, 0x00FF) return b @@ -192,7 +196,7 @@ def m_rela_info_oob(buf): return b -@case("reloc_type_unsupported", "unsupported AArch64 reloc type") +@case("reloc_type_unsupported", "unsupported reloc type") def m_reloc_type_unsupported(buf): """Flip the r_info type to something we don't decode.""" b = bytearray(buf) diff --git a/test/elf/bad/reloc_type_unsupported.expect b/test/elf/bad/reloc_type_unsupported.expect @@ -1 +1 @@ -unsupported AArch64 reloc type -\ No newline at end of file +unsupported reloc type +\ No newline at end of file diff --git a/test/elf/run.sh b/test/elf/run.sh @@ -95,7 +95,7 @@ else [ -n "$sysroot" ] && sysroot_flag="-isysroot $sysroot" # shellcheck disable=SC2086 if ! "$cc" -std=c11 -Wall -Wextra -Werror $sysroot_flag \ - -I"$ROOT/include" -I"$ROOT/src" \ + -I"$ROOT/include" -I"$ROOT/src" -I"$ROOT/test" \ "$src" "$LIB_AR" -o "$bin" 2> "$BUILD_DIR/$(basename "$src" .c).build.log"; then note_fail "$name" continue @@ -112,6 +112,17 @@ printf '\n' # ----- Layer B: cases/*.c ------------------------------------------------ +# Map CFREE_TEST_ARCH (default aa64) to the clang `--target=` triple the +# Layer B golden objects are compiled against. cfree-roundtrip then +# detects the input's e_machine and constructs a matching CfreeTarget, +# so the readelf diff stays apples-to-apples per arch. +case "${CFREE_TEST_ARCH:-aa64}" in + aa64|aarch64|arm64) CLANG_TARGET="aarch64-linux-gnu" ;; + x64|x86_64|amd64) CLANG_TARGET="x86_64-linux-gnu" ;; + rv64|riscv64) CLANG_TARGET="riscv64-linux-gnu" ;; + *) printf 'unknown CFREE_TEST_ARCH=%s\n' "${CFREE_TEST_ARCH}" >&2; exit 2 ;; +esac + printf 'Layer B — clang-oracle cases\n' case_srcs=( "$TEST_DIR"/cases/*.c ) if [ ${#case_srcs[@]} -eq 0 ]; then @@ -136,7 +147,7 @@ else [ -f "${src%.c}.cflags" ] && extra_cflags="$(cat "${src%.c}.cflags")" # shellcheck disable=SC2086 - if ! clang --target=aarch64-linux-gnu -c -O0 $extra_cflags "$src" -o "$wd/golden.o" \ + if ! clang --target="$CLANG_TARGET" -c -O0 $extra_cflags "$src" -o "$wd/golden.o" \ 2> "$wd/clang.log"; then note_skip "$name" "clang -c failed (cross-compile not configured?)" continue diff --git a/test/elf/unit/align_4k.c b/test/elf/unit/align_4k.c @@ -14,6 +14,7 @@ #include "core/core.h" #include "core/pool.h" +#include "lib/cfree_test_target.h" #include "obj/obj.h" static void* heap_alloc(CfreeHeap* h, size_t n, size_t a) { @@ -65,12 +66,10 @@ static const uint8_t TEXT_BYTES[8] = { int main(void) { CfreeTarget target; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; + if (cfree_test_target_init(&target) != 0) { + fprintf(stderr, "FAIL: cfree_test_target_init\n"); + return 1; + } CfreeEnv env = {.heap = &g_heap, .file_io = NULL, .diag = &g_diag}; CfreeCompiler* cc = cfree_compiler_new(target, &env); diff --git a/test/elf/unit/smoke.c b/test/elf/unit/smoke.c @@ -22,6 +22,7 @@ #include "core/core.h" #include "core/pool.h" +#include "lib/cfree_test_target.h" #include "obj/obj.h" /* ---- env ---- */ @@ -231,13 +232,10 @@ static void verify_shape(const ObjBuilder* ob, Pool* p) { int main(void) { CfreeTarget target; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; - target.big_endian = 0; + if (cfree_test_target_init(&target) != 0) { + fprintf(stderr, "FAIL: cfree_test_target_init\n"); + return 1; + } CfreeEnv env; env.heap = &g_heap; diff --git a/test/lib/cfree_test_target.h b/test/lib/cfree_test_target.h @@ -0,0 +1,54 @@ +/* Shared CfreeTarget setup for C test runners. + * + * Reads CFREE_TEST_ARCH (one of "aa64"/"aarch64", "x64"/"x86_64", + * "rv64"/"riscv64") and fills *t with a Linux/ELF/LP64 target for + * that arch. Defaults to aarch64 when the env var is unset or empty, + * preserving the historical behavior of every harness that called + * the previous local target_aarch64_linux() helper. + * + * Header-only and self-contained: include from any runner TU. The + * caller still chooses whether the build's host arch matches the + * cross-target (e.g. JIT lanes only run when they match). */ + +#ifndef CFREE_TEST_TARGET_H +#define CFREE_TEST_TARGET_H + +#include <cfree.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static inline const char* cfree_test_arch_name(void) { + const char* a = getenv("CFREE_TEST_ARCH"); + if (!a || !*a) return "aa64"; + return a; +} + +static inline int cfree_test_target_init(CfreeTarget* t) { + memset(t, 0, sizeof *t); + t->os = CFREE_OS_LINUX; + t->obj = CFREE_OBJ_ELF; + t->ptr_size = 8; + t->ptr_align = 8; + t->big_endian = 0; + const char* a = cfree_test_arch_name(); + if (!strcmp(a, "aa64") || !strcmp(a, "aarch64") || !strcmp(a, "arm64")) { + t->arch = CFREE_ARCH_ARM_64; + return 0; + } + if (!strcmp(a, "x64") || !strcmp(a, "x86_64") || !strcmp(a, "amd64")) { + t->arch = CFREE_ARCH_X86_64; + return 0; + } + if (!strcmp(a, "rv64") || !strcmp(a, "riscv64")) { + t->arch = CFREE_ARCH_RV64; + return 0; + } + fprintf(stderr, + "cfree_test_target: unrecognized CFREE_TEST_ARCH=\"%s\" " + "(expected aa64/x64/rv64)\n", + a); + return -1; +} + +#endif diff --git a/test/link/harness/jit_runner.c b/test/link/harness/jit_runner.c @@ -31,6 +31,8 @@ #include <sys/stat.h> #include <unistd.h> +#include "lib/cfree_test_target.h" + static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { (void)h; (void)a; @@ -312,10 +314,10 @@ int main(int argc, char** argv) { } CfreeTarget target; - memset(&target, 0, sizeof(target)); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; + if (cfree_test_target_init(&target) != 0) { + fprintf(stderr, "jit_runner: cfree_test_target_init failed\n"); + return 2; + } CfreeEnv env; memset(&env, 0, sizeof(env)); diff --git a/test/link/harness/link_exe_runner.c b/test/link/harness/link_exe_runner.c @@ -21,6 +21,8 @@ #include <sys/stat.h> #include <unistd.h> +#include "lib/cfree_test_target.h" + static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { (void)h; (void)a; @@ -156,10 +158,10 @@ int main(int argc, char** argv) { } CfreeTarget target; - memset(&target, 0, sizeof(target)); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; + if (cfree_test_target_init(&target) != 0) { + fprintf(stderr, "link_exe_runner: cfree_test_target_init failed\n"); + return 2; + } CfreeEnv env; memset(&env, 0, sizeof(env)); diff --git a/test/link/harness/start.c b/test/link/harness/start.c @@ -32,11 +32,17 @@ extern char __tdata_start[]; extern char __tdata_end[]; extern char __tbss_size[]; /* SK_ABS: address-of yields the byte count */ +/* TLS-block prologue layout — per-arch ABI dictates whether the TCB sits + * before or after .tdata in the thread-pointer-relative image. AArch64 + * keeps a 16-byte reserved TCB; SysV-x86_64 uses TLS variant II (negative + * offsets from the thread pointer, see below); RISC-V LP64 follows + * variant I and points the thread pointer at the TCB end. */ #define AARCH64_TCB_SIZE 16 /* Per-thread TLS image; the test harness is single-threaded so a * file-scope buffer is enough. Sized generously for any test we run - * here. Layout: [TCB(16) | .tdata copy | .tbss zero-fill]. */ + * here. Layout: [TCB | .tdata copy | .tbss zero-fill] for variants + * that put the TCB first. */ static char g_tls_block[4096] __attribute__((aligned(16))); /* IFUNC startup init. Mirrors rt/lib/cfree/ifunc_init.c — duplicated @@ -60,20 +66,66 @@ void __cfree_ifunc_init(void) { } __attribute__((noreturn)) static void do_exit(int code) { +#if defined(__aarch64__) register long x8 __asm__("x8") = 94; /* sys_exit_group */ register long x0 __asm__("x0") = code; __asm__ volatile("svc #0" ::"r"(x8), "r"(x0) : "memory"); +#elif defined(__x86_64__) + register long rax __asm__("rax") = 231; /* sys_exit_group */ + register long rdi __asm__("rdi") = code; + __asm__ volatile("syscall" ::"r"(rax), "r"(rdi) : "memory"); +#elif defined(__riscv) && __riscv_xlen == 64 + register long a7 __asm__("a7") = 94; /* sys_exit_group */ + register long a0 __asm__("a0") = code; + __asm__ volatile("ecall" ::"r"(a7), "r"(a0) : "memory"); +#else +#error "start.c: unsupported architecture" +#endif __builtin_unreachable(); } static void tls_init(void) { unsigned long td_n = (unsigned long)(__tdata_end - __tdata_start); unsigned long bs_n = (unsigned long)(unsigned long long)__tbss_size; - char* dst = g_tls_block + AARCH64_TCB_SIZE; unsigned long i; +#if defined(__aarch64__) + /* Variant I (TCB first): tp -> [TCB(16) | tdata | tbss] */ + char* dst = g_tls_block + AARCH64_TCB_SIZE; for (i = 0; i < td_n; ++i) dst[i] = __tdata_start[i]; for (i = 0; i < bs_n; ++i) dst[td_n + i] = 0; __asm__ volatile("msr tpidr_el0, %0" ::"r"(g_tls_block) : "memory"); +#elif defined(__x86_64__) + /* SysV TLS variant II: TLS bytes at *negative* offsets from the + * thread pointer (fs base). Lay out [tdata | tbss | TCB] where the + * TCB self-pointer sits at offset 0. The first slot of the TCB + * must be the thread pointer (self) per ELF ABI. */ + char* tcb = g_tls_block + sizeof(g_tls_block) - 64; + *(void**)tcb = tcb; + char* tls = tcb - (td_n + bs_n); + for (i = 0; i < td_n; ++i) tls[i] = __tdata_start[i]; + for (i = 0; i < bs_n; ++i) tls[td_n + i] = 0; + /* arch_prctl(ARCH_SET_FS, tcb): syscall 158, code 0x1002. */ + register long rax __asm__("rax") = 158; + register long rdi __asm__("rdi") = 0x1002; + register long rsi __asm__("rsi") = (long)tcb; + __asm__ volatile("syscall" + : "+r"(rax) + : "r"(rdi), "r"(rsi) + : "rcx", "r11", "memory"); +#elif defined(__riscv) && __riscv_xlen == 64 + /* Variant I: tp -> [TCB | tdata | tbss], TCB is reserved (here just + * the first 16 bytes of the block); RISC-V psABI puts tp 16 bytes + * past the start of the static TLS block convention varies, but + * the unwind/glibc convention used by linker-generated code + * resolves &var via tp + offset_from_TLS_image_start. We place + * .tdata immediately after a 16-byte reservation. */ + char* dst = g_tls_block + 16; + for (i = 0; i < td_n; ++i) dst[i] = __tdata_start[i]; + for (i = 0; i < bs_n; ++i) dst[td_n + i] = 0; + __asm__ volatile("mv tp, %0" ::"r"(g_tls_block) : "memory"); +#else +#error "start.c: unsupported architecture" +#endif } void _start(void) { diff --git a/test/link/run.sh b/test/link/run.sh @@ -53,9 +53,21 @@ NORMALIZE="$ROOT/test/elf/normalize.py" LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner" JIT_RUNNER="$BUILD_DIR/jit-runner" -CLANG_TARGET="--target=aarch64-linux-gnu" +# CFREE_TEST_ARCH selects the cross-target. Default aa64 preserves the +# pre-multiarch behavior. The C runners read the same env via +# test/lib/cfree_test_target.h. +CFREE_TEST_ARCH="${CFREE_TEST_ARCH:-aa64}" +case "$CFREE_TEST_ARCH" in + aa64|aarch64|arm64) TEST_ARCH=aa64; CLANG_TRIPLE=aarch64-linux-gnu; EXEC_ARCH=aarch64 ;; + x64|x86_64|amd64) TEST_ARCH=x64; CLANG_TRIPLE=x86_64-linux-gnu; EXEC_ARCH=x64 ;; + rv64|riscv64) TEST_ARCH=rv64; CLANG_TRIPLE=riscv64-linux-gnu; EXEC_ARCH=rv64 ;; + *) printf 'unknown CFREE_TEST_ARCH=%s\n' "$CFREE_TEST_ARCH" >&2; exit 2 ;; +esac +export CFREE_TEST_ARCH + +CLANG_TARGET="--target=$CLANG_TRIPLE" CC="${CC:-cc}" -CFREE_CFLAGS="-I$ROOT/include" +CFREE_CFLAGS="-I$ROOT/include -I$ROOT/test" ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" # Filters (env vars or positional args; args win): @@ -116,6 +128,15 @@ command -v podman >/dev/null 2>&1 && have_podman=1 arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 +# is_native_target=1 when the cross-target arch matches the host arch. +# Required for in-process JIT (path D) and the jit-runner (path J). +is_native_target=0 +case "$TEST_ARCH" in + aa64) [ $is_aarch64 -eq 1 ] && is_native_target=1 ;; + x64) { [ "$arch_raw" = "x86_64" ] || [ "$arch_raw" = "amd64" ]; } && is_native_target=1 ;; + rv64) [ "$arch_raw" = "riscv64" ] && is_native_target=1 ;; +esac + READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" # Shared per-arch exec helper. Path E queues each linked.exe and we @@ -149,7 +170,7 @@ else "$(color_yel warn)" "$LINK_EXE_RUNNER" >&2 fi -if [ $is_aarch64 -eq 1 ]; then +if [ $is_native_target -eq 1 ]; then if [ -x "$JIT_RUNNER" ]; then have_jit_runner=1 printf ' %s jit-runner\n' "$(color_grn found)" @@ -258,9 +279,9 @@ for case_dir in "$TEST_DIR/cases"/*/; do # ---- compile with clang cross ------------------------------------------ if [ $have_clang_cross -eq 0 ]; then - note_skip "$name/R" "no aarch64 clang" - [ $jit_only -eq 0 ] && note_skip "$name/E" "no aarch64 clang" - note_skip "$name/J" "no aarch64 clang" + note_skip "$name/R" "no $TEST_ARCH clang" + [ $jit_only -eq 0 ] && note_skip "$name/E" "no $TEST_ARCH clang" + note_skip "$name/J" "no $TEST_ARCH clang" continue fi @@ -368,6 +389,11 @@ for case_dir in "$TEST_DIR/cases"/*/; do dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) note_fail "$name/E (link failed, ${dt}ms)" elif [ $kernel_image -eq 1 ]; then + if [ "$TEST_ARCH" != "aa64" ]; then + dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) + note_skip "$name/E" "kernel_image is aa64-only (TEST_ARCH=$TEST_ARCH)" + continue + fi QEMU_KERNEL_BIN="$(command -v qemu-system-aarch64 2>/dev/null || true)" if [ -z "$QEMU_KERNEL_BIN" ]; then dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) @@ -403,7 +429,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do for s in "${gc_present_syms[@]:-}"; do [ -n "$s" ] && gcp="${gcp}${s}"$'\n'; done E_GC_ABSENT_LIST+=("$gca") E_GC_PRESENT_LIST+=("$gcp") - exec_target_queue aarch64 "$name" "$exe" \ + exec_target_queue "$EXEC_ARCH" "$name" "$exe" \ "$work/exec.out" "$work/exec.err" "$work/exec.rc" else note_skip "$name/E" "no runner (qemu/podman)" @@ -453,7 +479,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J (${dt}ms)" else note_fail "$name/J (expected $expected, got $j_rc, ${dt}ms)"; fi elif [ $RUN_J -eq 1 ] && [ $kernel_image -eq 0 ]; then - note_skip "$name/J" "no jit-runner (not aarch64 host or build failed)" + note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH or build failed)" fi done @@ -477,7 +503,7 @@ for case_dir in "$TEST_DIR/bad"/*/; do fi expect="$(cat "$expect_file")" - if [ $have_clang_cross -eq 0 ]; then note_skip "$name" "no aarch64 clang"; continue; fi + if [ $have_clang_cross -eq 0 ]; then note_skip "$name" "no $TEST_ARCH clang"; continue; fi tu_srcs=() for f in "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do @@ -542,7 +568,7 @@ for case_dir in "$TEST_DIR/bad"/*/; do fi fi elif [ $RUN_J -eq 1 ]; then - note_skip "$name/J" "no jit-runner (not aarch64 host or build failed)" + note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH or build failed)" fi done diff --git a/test/parse/harness/parse_runner.c b/test/parse/harness/parse_runner.c @@ -27,6 +27,8 @@ #include <sys/stat.h> #include <unistd.h> +#include "lib/cfree_test_target.h" + /* ---- env: heap, diag ---- */ static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { @@ -207,14 +209,11 @@ static CfreeExecMem g_execmem = { /* ---- helpers ---- */ -static void target_aarch64_linux(CfreeTarget* t) { - memset(t, 0, sizeof *t); - t->arch = CFREE_ARCH_ARM_64; - t->os = CFREE_OS_LINUX; - t->obj = CFREE_OBJ_ELF; - t->ptr_size = 8; - t->ptr_align = 8; - t->big_endian = 0; +static void target_from_env(CfreeTarget* t) { + if (cfree_test_target_init(t) != 0) { + fprintf(stderr, "parse-runner: cfree_test_target_init failed\n"); + exit(2); + } } static int read_file(const char* path, uint8_t** out, size_t* out_len) { @@ -277,7 +276,7 @@ static int mode_emit(const char* src_path, const char* out_path) { fprintf(stderr, "parse-runner: cannot read %s\n", src_path); return 2; } - target_aarch64_linux(&tgt); + target_from_env(&tgt); env_init(&env); c = cfree_compiler_new(tgt, &env); if (!c) { @@ -353,7 +352,7 @@ static int mode_jit(const char* src_path) { fprintf(stderr, "parse-runner: cannot read %s\n", src_path); return 2; } - target_aarch64_linux(&tgt); + target_from_env(&tgt); env_init(&env); c = cfree_compiler_new(tgt, &env); if (!c) { diff --git a/test/parse/run.sh b/test/parse/run.sh @@ -41,9 +41,21 @@ LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner" JIT_RUNNER="$BUILD_DIR/jit-runner" NORMALIZE="$ROOT/test/elf/normalize.py" -CLANG_TARGET="--target=aarch64-linux-gnu" +# CFREE_TEST_ARCH selects the cross-target. Default aa64 preserves the +# pre-multiarch behavior. The C runners read the same env via +# test/lib/cfree_test_target.h. +CFREE_TEST_ARCH="${CFREE_TEST_ARCH:-aa64}" +case "$CFREE_TEST_ARCH" in + aa64|aarch64|arm64) TEST_ARCH=aa64; CLANG_TRIPLE=aarch64-linux-gnu; EXEC_ARCH=aarch64 ;; + x64|x86_64|amd64) TEST_ARCH=x64; CLANG_TRIPLE=x86_64-linux-gnu; EXEC_ARCH=x64 ;; + rv64|riscv64) TEST_ARCH=rv64; CLANG_TRIPLE=riscv64-linux-gnu; EXEC_ARCH=rv64 ;; + *) printf 'unknown CFREE_TEST_ARCH=%s\n' "$CFREE_TEST_ARCH" >&2; exit 2 ;; +esac +export CFREE_TEST_ARCH + +CLANG_TARGET="--target=$CLANG_TRIPLE" CC="${CC:-cc}" -HARNESS_CFLAGS="-std=c11 -Wall -Wextra -I$ROOT/include" +HARNESS_CFLAGS="-std=c11 -Wall -Wextra -I$ROOT/include -I$ROOT/test" ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" FILTER="${1:-${CFREE_TEST_FILTER:-}}" @@ -96,6 +108,15 @@ command -v podman >/dev/null 2>&1 && have_podman=1 arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 +# is_native_target=1 when the cross-target arch matches the host arch. +# Required for in-process JIT (path D) and the jit-runner (path J). +is_native_target=0 +case "$TEST_ARCH" in + aa64) [ $is_aarch64 -eq 1 ] && is_native_target=1 ;; + x64) { [ "$arch_raw" = "x86_64" ] || [ "$arch_raw" = "amd64" ]; } && is_native_target=1 ;; + rv64) [ "$arch_raw" = "riscv64" ] && is_native_target=1 ;; +esac + READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" # Shared per-arch exec helper — see test/lib/exec_target.sh. @@ -125,7 +146,7 @@ fi # cfree-roundtrip — for path R. if [ ! -x "$ROUNDTRIP_BIN" ]; then - if $CC -I"$ROOT/include" "$ROOT/test/elf/cfree-roundtrip.c" "$LIB_AR" \ + if $CC -I"$ROOT/include" -I"$ROOT/test" "$ROOT/test/elf/cfree-roundtrip.c" "$LIB_AR" \ -o "$ROUNDTRIP_BIN" 2>"$BUILD_DIR/cfree-roundtrip.err"; then have_roundtrip=1 printf ' %s cfree-roundtrip\n' "$(color_grn built)" @@ -139,7 +160,7 @@ fi # link-exe-runner — for path E. if [ ! -x "$LINK_EXE_RUNNER" ]; then - if $CC -I"$ROOT/include" "$LINK_TEST_DIR/harness/link_exe_runner.c" \ + if $CC -I"$ROOT/include" -I"$ROOT/test" "$LINK_TEST_DIR/harness/link_exe_runner.c" \ "$LIB_AR" -o "$LINK_EXE_RUNNER" 2>"$BUILD_DIR/link-exe-runner.err"; then have_exe_runner=1 printf ' %s link-exe-runner\n' "$(color_grn built)" @@ -151,10 +172,10 @@ else have_exe_runner=1 fi -# jit-runner — for path J. Only on aarch64 host. -if [ $is_aarch64 -eq 1 ]; then +# jit-runner — for path J. Only when host arch matches the cross-target. +if [ $is_native_target -eq 1 ]; then if [ ! -x "$JIT_RUNNER" ]; then - if $CC -I"$ROOT/include" "$LINK_TEST_DIR/harness/jit_runner.c" \ + if $CC -I"$ROOT/include" -I"$ROOT/test" "$LINK_TEST_DIR/harness/jit_runner.c" \ "$LIB_AR" -o "$JIT_RUNNER" 2>"$BUILD_DIR/jit-runner.err"; then have_jit_runner=1 printf ' %s jit-runner\n' "$(color_grn built)" @@ -214,9 +235,9 @@ for src in "${CASES[@]}"; do fi expected_byte=$(( expected & 0xff )) - # ---- Path D: in-process JIT (aarch64 only) --------------------------- + # ---- Path D: in-process JIT (only when host arch == cross-target) ---- if [ $RUN_D -eq 1 ]; then - if [ $is_aarch64 -eq 1 ]; then + if [ $is_native_target -eq 1 ]; then t0=$(now_ms) "$PARSE_RUNNER" --jit "$src" >"$work/d.out" 2>"$work/d.err" d_rc=$? @@ -227,7 +248,7 @@ for src in "${CASES[@]}"; do note_fail "$name/D (expected $expected_byte got $d_rc, ${dt}ms)" fi else - note_skip "$name/D" "not on aarch64 host" + note_skip "$name/D" "host arch != $TEST_ARCH (no native JIT)" fi fi @@ -271,19 +292,19 @@ for src in "${CASES[@]}"; do >"$work/exec_link.out" 2>"$work/exec_link.err"; then dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) note_fail "$name/E (link failed, ${dt}ms)" - elif exec_target_supported aarch64; then + elif exec_target_supported "$EXEC_ARCH"; then link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt )) E_NAMES+=("$name") E_WORK+=("$work") E_LINK_MS+=("$link_dt") E_EXPECTED+=("$expected_byte") - exec_target_queue aarch64 "$name" "$exe" \ + exec_target_queue "$EXEC_ARCH" "$name" "$exe" \ "$work/exec.out" "$work/exec.err" "$work/exec.rc" else - note_skip "$name/E" "no runner for aarch64" + note_skip "$name/E" "no runner for $EXEC_ARCH" fi else - note_skip "$name/E" "no link-exe-runner, aarch64 clang, or start.o" + note_skip "$name/E" "no link-exe-runner, $TEST_ARCH clang, or start.o" fi fi @@ -300,7 +321,7 @@ for src in "${CASES[@]}"; do note_fail "$name/J (expected $expected_byte got $j_rc, ${dt}ms)" fi else - note_skip "$name/J" "no jit-runner (not aarch64 host)" + note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH)" fi fi done diff --git a/test/test.mk b/test/test.mk @@ -91,7 +91,7 @@ $(DEBUG_TEST_BIN): test/debug/roundtrip_unit.c $(LIB_AR) # # HARNESS_CFLAGS drops -Wpedantic; the runners cast cfree_jit_lookup's # void* to a function pointer, which pedantic rejects under C11. -HARNESS_CFLAGS = -std=c11 -Wall -Wextra -Werror -isysroot $(SYSROOT) -Iinclude +HARNESS_CFLAGS = -std=c11 -Wall -Wextra -Werror -isysroot $(SYSROOT) -Iinclude -Itest ROUNDTRIP_BIN = build/test/cfree-roundtrip LINK_EXE_RUNNER = build/test/link-exe-runner