commit f2fa4890b0f5a8f2789856784b845889b4692c18
parent a292100f526e3ee83fd6f4e76eee1eb3a4694559
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 4 Jun 2026 13:57:00 -0700
freebsd: cross-compile target, runtime, and link support
Make `kit cc --target=<arch>-freebsd` actually produce FreeBSD ELF for
aarch64/x64/rv64:
- target.c: parse the `freebsd` OS token (with version suffix, e.g.
freebsd15.0) -> KIT_OS_FREEBSD/ELF. It previously fell through to
freestanding.
- runtime.c: add {x86_64,aarch64,riscv64}-freebsd runtime variants (ELF,
mirroring the Linux runtime) so cc finds libkit_rt.
- obj/elf: read ELF COMDAT groups -- record an SHT_GROUP section's
signature symbol as absolute-defined instead of orphaning it into a
phantom undef. FreeBSD's crt1.o brands the binary via a `.freebsd.note`
COMDAT group. Also map STB_GNU_UNIQUE to a global binding.
- hosted.c: link FreeBSD 15's libsys (split out of libc) when present.
Linking a static hosted executable still hits the libc/libsys weak-alias
archive cycle (undefined `openat`); see doc/plan/FREEBSD.md.
test-elf/test-link/test-ar remain green.
Diffstat:
5 files changed, 69 insertions(+), 0 deletions(-)
diff --git a/driver/lib/hosted.c b/driver/lib/hosted.c
@@ -439,11 +439,32 @@ static int hosted_resolve_freebsd(const DriverHostedRequest* req,
DRIVER_HOSTED_MAX_AFTER, req, dirs, "libc.a",
DRIVER_HOSTED_INPUT_ARCHIVE) != 0)
return 1;
+ /* FreeBSD 15 split the raw syscall stubs out of libc into libsys; link it
+ * after libc when the sysroot provides it (pre-15 roots won't have it).
+ * libc.a and libsys.a are mutually recursive (libc calls the syscall
+ * stubs in libsys; libsys's stubs call back into libc), so re-list libc.a
+ * after libsys.a -- kit resolves each archive against the inputs before
+ * it, so the second occurrence picks up the back-references libsys
+ * introduces. (Equivalent to GNU ld's `--start-group libc libsys`.) */
+ if (hosted_libdir_has(req->env, dirs, "libsys.a") &&
+ (hosted_add_required_search(plan->after, &plan->nafter,
+ DRIVER_HOSTED_MAX_AFTER, req, dirs,
+ "libsys.a", DRIVER_HOSTED_INPUT_ARCHIVE) !=
+ 0 ||
+ hosted_add_required_search(plan->after, &plan->nafter,
+ DRIVER_HOSTED_MAX_AFTER, req, dirs, "libc.a",
+ DRIVER_HOSTED_INPUT_ARCHIVE) != 0))
+ return 1;
} else {
if (hosted_add_required_search(plan->after, &plan->nafter,
DRIVER_HOSTED_MAX_AFTER, req, dirs,
"libc.so.7", DRIVER_HOSTED_INPUT_DSO) != 0)
return 1;
+ if (hosted_libdir_has(req->env, dirs, "libsys.so.7") &&
+ hosted_add_required_search(plan->after, &plan->nafter,
+ DRIVER_HOSTED_MAX_AFTER, req, dirs,
+ "libsys.so.7", DRIVER_HOSTED_INPUT_DSO) != 0)
+ return 1;
}
if (hosted_add_required_search(plan->final, &plan->nfinal,
DRIVER_HOSTED_MAX_FINAL, req, dirs, "crtn.o",
diff --git a/driver/lib/runtime.c b/driver/lib/runtime.c
@@ -124,6 +124,13 @@ static const RuntimeVariant kRtVariants[] = {
"lib/include/lp64_le", 1, 0, kRtSrcX64,
(uint32_t)(sizeof(kRtSrcX64) / sizeof(kRtSrcX64[0])),
KIT_FLOAT_ABI_DEFAULT, NULL, NULL},
+ /* FreeBSD ELF runtime mirrors the Linux ELF runtime per-arch: the
+ * compiler-rt helpers and coroutine asm are ABI/ELF-based, not kernel
+ * based, so the same source set and data model apply. */
+ {"x86_64-freebsd", KIT_ARCH_X86_64, KIT_OS_FREEBSD, KIT_OBJ_ELF, 8, 8,
+ "lib/include/lp64_le", 1, 0, kRtSrcX64,
+ (uint32_t)(sizeof(kRtSrcX64) / sizeof(kRtSrcX64[0])),
+ KIT_FLOAT_ABI_DEFAULT, NULL, NULL},
{"x86_64-apple-darwin", KIT_ARCH_X86_64, KIT_OS_MACOS, KIT_OBJ_MACHO, 8, 8,
"lib/include/lp64_le", 1, 0, kRtSrcX64,
(uint32_t)(sizeof(kRtSrcX64) / sizeof(kRtSrcX64[0])),
@@ -136,6 +143,10 @@ static const RuntimeVariant kRtVariants[] = {
"lib/include/lp64_le", 1, 1, kRtSrcAarch64Linux,
(uint32_t)(sizeof(kRtSrcAarch64Linux) / sizeof(kRtSrcAarch64Linux[0])),
KIT_FLOAT_ABI_DEFAULT, NULL, NULL},
+ {"aarch64-freebsd", KIT_ARCH_ARM_64, KIT_OS_FREEBSD, KIT_OBJ_ELF, 8, 8,
+ "lib/include/lp64_le", 1, 1, kRtSrcAarch64Linux,
+ (uint32_t)(sizeof(kRtSrcAarch64Linux) / sizeof(kRtSrcAarch64Linux[0])),
+ KIT_FLOAT_ABI_DEFAULT, NULL, NULL},
{"aarch64-apple-darwin", KIT_ARCH_ARM_64, KIT_OS_MACOS, KIT_OBJ_MACHO, 8, 8,
"lib/include/lp64_le", 1, 0, kRtSrcAarch64Darwin,
(uint32_t)(sizeof(kRtSrcAarch64Darwin) / sizeof(kRtSrcAarch64Darwin[0])),
@@ -151,6 +162,10 @@ static const RuntimeVariant kRtVariants[] = {
"lib/include/lp64_le", 1, 0, kRtSrcRv64Linux,
(uint32_t)(sizeof(kRtSrcRv64Linux) / sizeof(kRtSrcRv64Linux[0])),
KIT_FLOAT_ABI_DEFAULT, NULL, NULL},
+ {"riscv64-freebsd", KIT_ARCH_RV64, KIT_OS_FREEBSD, KIT_OBJ_ELF, 8, 8,
+ "lib/include/lp64_le", 1, 0, kRtSrcRv64Linux,
+ (uint32_t)(sizeof(kRtSrcRv64Linux) / sizeof(kRtSrcRv64Linux[0])),
+ KIT_FLOAT_ABI_DEFAULT, NULL, NULL},
{"riscv64-elf", KIT_ARCH_RV64, KIT_OS_FREESTANDING, KIT_OBJ_ELF, 8, 8,
"lib/include/lp64_le", 1, 0, kRtSrcRv64Elf,
(uint32_t)(sizeof(kRtSrcRv64Elf) / sizeof(kRtSrcRv64Elf[0])),
diff --git a/driver/lib/target.c b/driver/lib/target.c
@@ -14,6 +14,13 @@ static int triple_tok_eq(const char* s, size_t n, const char* lit) {
return n == l && memcmp(s, lit, n) == 0;
}
+/* Prefix match for OS tokens that carry a trailing version, e.g. clang emits
+ * "freebsd15.0" / "freebsd14" rather than a bare "freebsd". */
+static int triple_tok_prefix(const char* s, size_t n, const char* lit) {
+ size_t l = kit_slice_cstr(lit).len;
+ return n >= l && memcmp(s, lit, l) == 0;
+}
+
KitPic driver_default_pic(KitObjFmt obj, KitOSKind os) {
/* WASM has no PIC/PIE concept; freestanding targets have no dynamic
* loader to apply load-time relocations. Everything else is hosted and
@@ -319,6 +326,12 @@ int driver_target_from_triple(const char* triple, KitTargetSpec* out) {
os_set = 1;
break;
}
+ if (triple_tok_prefix(parts[i], plen[i], "freebsd")) {
+ t.os = KIT_OS_FREEBSD;
+ t.obj = KIT_OBJ_ELF;
+ os_set = 1;
+ break;
+ }
if (triple_tok_eq(parts[i], plen[i], "wasi")) {
t.os = KIT_OS_WASI;
t.obj = KIT_OBJ_WASM;
diff --git a/src/obj/elf/elf.h b/src/obj/elf/elf.h
@@ -115,6 +115,11 @@
#define STB_LOCAL 0
#define STB_GLOBAL 1
#define STB_WEAK 2
+/* GNU extension: a global symbol that the dynamic loader keeps unique across
+ * the whole process (used for C++ inline statics, and by FreeBSD's crt for the
+ * ABI-brand note symbol). For static-link resolution it behaves as a global
+ * definition. */
+#define STB_GNU_UNIQUE 10
#define STT_NOTYPE 0
#define STT_OBJECT 1
diff --git a/src/obj/elf/read.c b/src/obj/elf/read.c
@@ -145,6 +145,10 @@ static u16 elf_kind_from_name(const char* name, u32 nlen, u64 sh_flags,
static u16 elf_bind_to_obj(u32 b) {
switch (b) {
case STB_GLOBAL:
+ case STB_GNU_UNIQUE:
+ /* GNU-unique is a global with extra runtime uniqueness semantics; for
+ * link-time resolution it is an ordinary global definition. FreeBSD's
+ * crt1.o brands the binary with a GNU-unique `.freebsd.note*` symbol. */
return SB_GLOBAL;
case STB_WEAK:
return SB_WEAK;
@@ -715,6 +719,17 @@ ObjBuilder* read_elf(Compiler* c, const char* name, const u8* data,
sec_id = OBJ_SEC_NONE;
value = st_value;
if (st_shndx == SHN_COMMON) cmnalign = st_value;
+ } else if (st_shndx < e_shnum && shdrs[st_shndx].sh_type == SHT_GROUP) {
+ /* A COMDAT group's signature symbol is defined in its SHT_GROUP
+ * section, which we consume into an ObjGroup and never keep as an
+ * obj section (so elf_to_obj is OBJ_SEC_NONE for it). The symbol just
+ * names the group; it is not a data location and is never a reloc
+ * target. Record it as an absolute defined symbol so it doesn't look
+ * like a phantom undefined reference -- FreeBSD's crt1.o brands the
+ * binary with such a symbol (.freebsd.note*). */
+ sec_id = OBJ_SEC_NONE;
+ value = st_value;
+ kind = SK_ABS;
} else if (st_shndx < e_shnum) {
sec_id = elf_to_obj[st_shndx];
value = st_value;