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:
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