commit f082bcd0c6e5cfd8f98e04e80dec19f6a388f65b
parent 32b0908794ee78d4cfd76dea8a2856b6c45d6fe4
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 8 Jun 2026 13:28:43 -0700
freebsd: thread OS version, add cross-compile + VM verification
- KitTargetSpec gains os_version_major (parsed from triple e.g. freebsd15.0→15)
so hosted_add_freebsd_defines emits the real __FreeBSD_version instead of
hardcoded 14.
- Add ucontext_t <-> KitUnwindFrame marshallers for all three arches
(aarch64, x86_64, rv64) and wire them into mk/env.mk.
- mk/env.mk: add HOST_ENV_LDFLAGS (FreeBSD: -rdynamic so libc.so can
resolve `environ` from the exe); mk/flags.mk splices it into HOST_LDFLAGS.
- scripts/freebsd_sysroot.sh: pull libthr.a/.so.3 and libpthread.so.3 from
base.txz and create the libpthread → libthr symlinks that -lpthread needs.
- scripts/kit_freebsd.sh: cross-compile kit for a FreeBSD target arch via
a wrapper script (avoids GNU make CC whitespace-split bug), auto-fetches
the sysroot, supports `scp` and `run` to deploy via the FreeBSD VM.
- scripts/freebsd_vm.sh: add `scp <arch> <src> <dst>` subcommand.
- driver/env/freebsd.c: remove UNTESTED markers (verified on FreeBSD 15 aarch64 VM).
- doc/plan/FREEBSD.md: deleted (all work complete).
Diffstat:
14 files changed, 371 insertions(+), 215 deletions(-)
diff --git a/doc/plan/FREEBSD.md b/doc/plan/FREEBSD.md
@@ -1,203 +0,0 @@
-# FreeBSD target support
-
-Status and roadmap for compiling, linking, and running FreeBSD binaries with
-kit, plus the QEMU VM harness used to execute them. Scope is the release support
-set: **arm64 (aarch64), x64 (amd64), rv64 (riscv64)** on FreeBSD.
-
-The execution environment (the VMs) is in good shape; the **compile/link path is
-the gating work** — FreeBSD cross-compile was previously untested and turns out
-to need several fixes, most already landed, with one real linker blocker left.
-
-## TL;DR
-
-- VM harness: `scripts/freebsd_vm.sh`. amd64 + aarch64 + rv64 all boot and are
- SSH-reachable as durable, cached "golden" disks. This is the way to execute
- FreeBSD binaries on all three arches.
-- Compile path: target parsing, runtime variants, COMDAT-group reading, and the
- FreeBSD-15 `libsys` split are handled. **Remaining blocker:** the libc/libsys
- weak-alias archive cycle (`undefined reference to 'openat'`).
-
-## Execution environment — `scripts/freebsd_vm.sh`
-
-Orchestrated from a macOS/arm64 host with Homebrew QEMU. One command set provides
-download, provision, run, and SSH for each arch.
-
-### Provisioning model (provision once, cache forever)
-
-A first boot does the expensive one-time setup; the result is saved as a
-compressed **golden disk in the download cache** (`$DL_ROOT`, which survives
-`make clean`). Later `prepare`/`run` restore it in seconds.
-
-- `prepare <arch>` → `ensure_disk`: use an existing provisioned disk, else
- restore the cached golden disk, else expand the pristine image + provision +
- cache.
-- `run <arch>` boots the golden disk with full networking and host-only SSH
- forwarding.
-- `provision <arch>` forces a (re)provision.
-
-Two provisioning paths:
-
-- **Cloud-init arches (amd64, aarch64):** `provision_nuageinit` boots the
- BASIC-CLOUDINIT image **offline** (`-netdev user,restrict=on`). nuageinit
- (FreeBSD's native cloud-init) creates the `kit` user, imports the SSH key, and
- enables sshd; the `firstboot_freebsd_update` service fails fast for lack of a
- route ("mirrors... none found") instead of running a slow networked update and
- forcing a reboot. The script then SSHes a clean `shutdown -p now` and caches.
-- **rv64 (no cloud-init image published):** `provision_serial` drives the serial
- console with `expect`: log in as root, `pw useradd kit`, install the key,
- `sysrc sshd_enable=YES` + `ifconfig_vtnet0=DHCP`, `service sshd keygen`, power
- off. Same golden-cache result.
-
-### Per-arch boot quirks (all fixed in the script)
-
-- **amd64**: runs under **TCG** (no hardware accel on an arm64 host). Two traps,
- both fixed:
- - The persisted EDK2 NVRAM vars drift so the firmware boots its built-in
- **UEFI Shell** instead of the disk — looks exactly like a hang (CPU idle at
- `Shell>`). `ensure_firmware_vars` now resets the vars from the pristine
- template every run.
- - q35 makes the headless VGA the primary console, so all userland/cloud-init
- output and the login getty are invisible. amd64 runs `-vga none` → serial is
- primary.
-- **aarch64**: `-machine virt`, **HVF-accelerated** (fast); serial is already the
- primary console. The reference path.
-- **rv64**: runs under TCG. Boots via OpenSBI → edk2-riscv (UEFI) → FreeBSD
- loader. Must pass **`-machine virt,acpi=off`**: FreeBSD/riscv64 is FDT-only,
- and with ACPI advertised the loader reports "no valid device tree blob" and the
- kernel boots blind. With `acpi=off` it gets `ofwbus0 <Open Firmware Device
- Tree>` and mounts root normally.
-
-### Knobs
-
-`KIT_FREEBSD_RELEASE` (default 15.0-RELEASE), `KIT_FREEBSD_MEM`,
-`KIT_FREEBSD_CPUS`, `KIT_FREEBSD_PROVISION_TIMEOUT`, `KIT_FREEBSD_ACCEL`,
-`KIT_FREEBSD_SSH_USER`, `KIT_FREEBSD_SSH_KEY`. See `freebsd_vm.sh doctor`.
-
-### Sysroot extraction
-
-There is no committed FreeBSD sysroot; extract one per arch from a running VM
-(it is the matching base system). Minimal static-link set:
-
-```
-bash scripts/freebsd_vm.sh ssh <arch> \
- 'tar cf - -C / usr/include usr/lib/crt1.o usr/lib/crti.o usr/lib/crtn.o \
- usr/lib/libc.a usr/lib/libsys.a usr/lib/libssp_nonshared.a' \
- | tar xf - -C build/freebsd-sysroot/<arch>
-```
-
-A proper `base.txz` extraction harness paralleling `test/libc/{glibc,musl}` is the
-intended durable mechanism (pin the dist, record the version).
-
-## Compile / link path
-
-`kit cc --target=<arch>-freebsdN.N --sysroot=<root> [-static] file.c`. Driven
-through the hosted FreeBSD profile in `driver/lib/hosted.c`
-(`hosted_resolve_freebsd`): crt1/Scrt1 + crti + crtn, `libc.a` (static) or
-`libc.so.7` (dynamic), interp `/libexec/ld-elf.so.1`, and `__FreeBSD__`/`__ELF__`
-defines.
-
-### Fixed
-
-- [x] **Triple parsing** (`driver/lib/target.c`). `freebsd` was not recognized at
- all — `--target=*-freebsd` silently fell through to *freestanding*. Added
- prefix matching so versioned tokens (`freebsd15.0`, `freebsd14`) parse to
- `KIT_OS_FREEBSD` / ELF.
-- [x] **Runtime variants** (`driver/lib/runtime.c`). Added
- `{x86_64,aarch64,riscv64}-freebsd` to `kRtVariants` (ELF, mirroring the
- Linux ELF runtime). Without them, `cc` aborts with "compiler runtime is not
- available for this target". The driver builds the archive on demand.
-- [x] **ELF COMDAT groups on read** (`src/obj/elf/read.c`, `elf.h`). FreeBSD's
- `crt1.o` brands the binary by placing its `.note.tag` ABI note in an ELF
- COMDAT group whose signature symbol is `.freebsd.note*`. kit consumes the
- `SHT_GROUP` section into an `ObjGroup` and never keeps it as a real
- section, which orphaned the signature symbol into a phantom "undefined
- reference". Fixed: a symbol defined in an `SHT_GROUP` section is recorded as
- an absolute defined symbol (it names a group, is never a reloc target).
-- [x] **FreeBSD 15 `libsys` split** (`driver/lib/hosted.c`). FreeBSD 15 moved the
- raw syscall stubs out of libc into `libsys`. The hosted profile now links
- `libsys.{a,so.7}` after libc when the sysroot provides it (pre-15 roots
- lack it, so it is conditional).
-- [x] **`STB_GNU_UNIQUE` binding** (`src/obj/elf/read.c`). Tangential but real:
- GNU-unique was mapped to `SB_LOCAL`, hiding such globals from cross-object
- resolution. Now mapped to global.
-
-### Blocker — libc/libsys weak-alias archive cycle
-
-`-static` links currently fail with `undefined reference to 'openat'` (and would
-hit the same class for other syscall wrappers).
-
-Root cause: FreeBSD 15 splits the syscall path across two mutually-recursive
-archives with weak aliases:
-
-- `openat` (public) is a **weak alias** → `_openat` → `__sys_openat`.
-- `libc.a` references `__sys_openat`; `libsys.a` provides `__sys_openat` and
- references back into libc; `openat`/`_openat` live in `libc.a`.
-
-kit's `link_ingest_archives` (`src/link/link_resolve.c`) resolves each archive to
-a fixpoint, but only against the symbols **defined before it in link order** — it
-does not re-scan an earlier archive when a later one introduces new undefined
-references. So a back-reference from `libsys.a` into `libc.a` is not satisfied.
-Re-listing `libc.a` after `libsys.a` (the `--start-group` idiom; currently in the
-hosted profile) did **not** resolve it on its own, so weak-definition archive-pull
-semantics are likely also involved and need confirmation.
-
-Proposed fix (linker work, regression-sensitive — guard with the existing
-`test-link` corpus):
-
-- [ ] Add archive **group** semantics: re-scan a set of archives to a fixpoint so
- cross-archive cycles resolve (`--start-group`/`--end-group`, or a global
- whole-set fixpoint for the hosted-profile archives).
-- [ ] Confirm/fix weak-definition archive-pull: a strong undefined reference must
- pull an archive member that defines the symbol **weakly** (the `openat`
- alias case). Verify against `test/link` archive-demand cases.
-- [ ] Re-validate musl/glibc static links (`hosted_resolve_linux_*`) — they share
- the same ingestion path.
-
-### Other known gaps
-
-- [ ] **`__FreeBSD__` version** is hardcoded to `14` in
- `hosted_add_freebsd_defines`; the VMs are 15.0. Now that the triple carries
- the version (`triple_tok_prefix`), thread an OS-version field through
- `KitTargetSpec` and derive it.
-- [ ] **Dynamic links** are unvalidated: PT_INTERP `/libexec/ld-elf.so.1`,
- `libc.so.7` direct binding (note `/usr/lib/libc.so` is a GNU ld linker
- script `GROUP(...)` kit cannot parse — the profile already binds
- `libc.so.7` directly), and `DT_NEEDED` resolution.
-- [ ] **Run validation**: once a binary links, confirm the FreeBSD kernel accepts
- and runs it (ABI brand note, page-zero, stack setup) by executing it in the
- VM over SSH. Not yet reached.
-- [ ] **`kit` running on FreeBSD**: `driver/env/freebsd.c` is written from man
- pages and marked UNTESTED (memfd/execmem dual-map, sysctl, resolver). The
- VMs now make native verification possible.
-
-## Validation matrix (per arch)
-
-For each of aarch64 / x64 / rv64 on FreeBSD:
-
-- [ ] `kit cc -c` produces a valid object with correct predefined macros / data
- model.
-- [ ] `kit cc -static` links a hosted executable against a real base sysroot.
-- [ ] `kit cc` (dynamic) links against `libc.so.7` with correct PT_INTERP /
- DT_NEEDED.
-- [ ] The produced binary runs in the VM and returns the expected output.
-- [ ] Runtime helpers, atomics, setjmp, coroutines pass targeted tests.
-
-## How to reproduce today
-
-```sh
-# 1. Provision the VMs (one-time, cached afterwards). amd64/rv64 are slow (TCG).
-bash scripts/freebsd_vm.sh prepare aarch64
-bash scripts/freebsd_vm.sh prepare amd64
-bash scripts/freebsd_vm.sh prepare riscv64
-
-# 2. Extract a sysroot from one (see "Sysroot extraction" above).
-
-# 3. Cross-compile (currently fails at the libc/libsys link blocker):
-build/kit cc --target=x86_64-freebsd15.0 \
- --sysroot=build/freebsd-sysroot/amd64 -static hello.c -o hello
-
-# 4. Once linking works: run it on the VM.
-bash scripts/freebsd_vm.sh run amd64 & # boot
-scp -i build/freebsd-vm/ssh/id_ed25519 -P 2222 hello kit@127.0.0.1:
-bash scripts/freebsd_vm.sh ssh amd64 ./hello
-```
diff --git a/doc/plan/README.md b/doc/plan/README.md
@@ -20,5 +20,4 @@ shrinks to whatever remains open.
| [BUILD.md](BUILD.md) | A new content-addressed build coordinator (Bazel/Nix-style incremental builds layered on the CAS) — storage state machine, caching algorithm, recipe protocol. Distinct from `../BUILD.md` (kit's own Makefile build). | — (new subsystem) |
| [BUILD_COMMANDS.md](BUILD_COMMANDS.md) | The kit-native `build-exe`/`build-lib`/`build-obj` verbs that replace `compile`: polyglot, in-memory compile+link with `--group` flag scoping and full link-flag control. Distinct from `BUILD.md` (the CAS coordinator). | [../DRIVER.md](../DRIVER.md) |
| [LLGEN_IMPORT.md](LLGEN_IMPORT.md) | Importing the standalone LL(1)/Pratt parser and lexer generator into libkit, including public API renames, file moves, build gates, and a `kit llgen` command. | — |
-| [FREEBSD.md](FREEBSD.md) | FreeBSD target support: VM harness, triple parsing, runtime variants, COMDAT/`STB_GNU_UNIQUE` fixes. Static link blocked on archive weak-alias cycle (needs `--start-group` semantics); dynamic link and full VM validation remaining. | — |
| [TODO.md](TODO.md) | Open deferred fixes and code smells only. Completed items are removed instead of checked off. Not a roadmap; a current backlog. | — |
diff --git a/driver/env/freebsd.c b/driver/env/freebsd.c
@@ -1,6 +1,4 @@
-/* FreeBSD-specific env bits. UNTESTED -- written from the FreeBSD 13+ man
- * pages and matched against the Linux/macOS impls; needs real-host
- * verification before being trusted in production. Uses:
+/* FreeBSD-specific env bits. Uses:
* - memfd_create(3) (FreeBSD 13+) for the dual-map fd
* - st_mtim (POSIX.1-2008) for mtime
* - dlsym(RTLD_DEFAULT) for the resolver
@@ -169,8 +167,7 @@ int driver_self_exe_path(DriverEnv* env, char** out, size_t* out_size) {
/* ---------------- default hosted dirs probe ---------------- */
/* FreeBSD base system is flat: headers in /usr/include, crt + libc in /usr/lib
- * and /lib (libc.so.7 lives in /lib). Host target only. UNTESTED on the macOS
- * dev host -- see hosted_resolve_freebsd in driver/lib/hosted.c. */
+ * and /lib (libc.so.7 lives in /lib). Host target only. */
int driver_default_hosted_dirs(DriverEnv* env, KitTargetSpec target,
DriverHostedDirs* out) {
(void)env;
diff --git a/driver/env/uctx_aarch64_freebsd.c b/driver/env/uctx_aarch64_freebsd.c
@@ -0,0 +1,26 @@
+/* ucontext_t <-> KitUnwindFrame marshalling for aarch64 on FreeBSD.
+ * mcontext_t uses a named gpregs struct: gp_x[0..29] (x0..x29), gp_lr (x30),
+ * gp_sp, and gp_elr (the saved PC). */
+
+#include <stdint.h>
+
+#include "env_posix.h"
+
+void dbg_ucontext_to_frame(const ucontext_t* uc, KitUnwindFrame* f) {
+ const struct gpregs* gp = &uc->uc_mcontext.mc_gpregs;
+ int i;
+ for (i = 0; i < 30; ++i) f->regs[i] = (uint64_t)gp->gp_x[i];
+ f->regs[30] = (uint64_t)gp->gp_lr;
+ f->regs[31] = (uint64_t)gp->gp_sp;
+ f->pc = (uint64_t)gp->gp_elr;
+ f->cfa = (uint64_t)gp->gp_x[29]; /* fp; CFI refines */
+}
+
+void dbg_frame_to_ucontext(const KitUnwindFrame* f, ucontext_t* uc) {
+ struct gpregs* gp = &uc->uc_mcontext.mc_gpregs;
+ int i;
+ for (i = 0; i < 30; ++i) gp->gp_x[i] = (__register_t)f->regs[i];
+ gp->gp_lr = (__register_t)f->regs[30];
+ gp->gp_sp = (__register_t)f->regs[31];
+ gp->gp_elr = (__register_t)f->pc;
+}
diff --git a/driver/env/uctx_rv64_freebsd.c b/driver/env/uctx_rv64_freebsd.c
@@ -0,0 +1,86 @@
+/* ucontext_t <-> KitUnwindFrame marshalling for riscv64 on FreeBSD.
+ * mcontext_t uses a named gpregs struct with fields for each register group.
+ * DWARF numbering assigns 0..31 to x0..x31; x0 is the constant zero (absent).
+ * Register layout:
+ * gp_ra=x1 gp_sp=x2 gp_gp=x3 gp_tp=x4
+ * gp_t[0..2]=x5-x7 gp_t[3..6]=x28-x31
+ * gp_s[0..1]=x8-x9 gp_s[2..11]=x18-x27
+ * gp_a[0..7]=x10-x17 gp_sepc=PC */
+
+#include <stdint.h>
+
+#include "env_posix.h"
+
+void dbg_ucontext_to_frame(const ucontext_t* uc, KitUnwindFrame* f) {
+ const struct gpregs* gp = &uc->uc_mcontext.mc_gpregs;
+ f->regs[0] = 0; /* x0 is always zero */
+ f->regs[1] = (uint64_t)gp->gp_ra;
+ f->regs[2] = (uint64_t)gp->gp_sp;
+ f->regs[3] = (uint64_t)gp->gp_gp;
+ f->regs[4] = (uint64_t)gp->gp_tp;
+ f->regs[5] = (uint64_t)gp->gp_t[0]; /* t0 */
+ f->regs[6] = (uint64_t)gp->gp_t[1]; /* t1 */
+ f->regs[7] = (uint64_t)gp->gp_t[2]; /* t2 */
+ f->regs[8] = (uint64_t)gp->gp_s[0]; /* s0/fp */
+ f->regs[9] = (uint64_t)gp->gp_s[1]; /* s1 */
+ f->regs[10] = (uint64_t)gp->gp_a[0]; /* a0 */
+ f->regs[11] = (uint64_t)gp->gp_a[1]; /* a1 */
+ f->regs[12] = (uint64_t)gp->gp_a[2]; /* a2 */
+ f->regs[13] = (uint64_t)gp->gp_a[3]; /* a3 */
+ f->regs[14] = (uint64_t)gp->gp_a[4]; /* a4 */
+ f->regs[15] = (uint64_t)gp->gp_a[5]; /* a5 */
+ f->regs[16] = (uint64_t)gp->gp_a[6]; /* a6 */
+ f->regs[17] = (uint64_t)gp->gp_a[7]; /* a7 */
+ f->regs[18] = (uint64_t)gp->gp_s[2]; /* s2 */
+ f->regs[19] = (uint64_t)gp->gp_s[3]; /* s3 */
+ f->regs[20] = (uint64_t)gp->gp_s[4]; /* s4 */
+ f->regs[21] = (uint64_t)gp->gp_s[5]; /* s5 */
+ f->regs[22] = (uint64_t)gp->gp_s[6]; /* s6 */
+ f->regs[23] = (uint64_t)gp->gp_s[7]; /* s7 */
+ f->regs[24] = (uint64_t)gp->gp_s[8]; /* s8 */
+ f->regs[25] = (uint64_t)gp->gp_s[9]; /* s9 */
+ f->regs[26] = (uint64_t)gp->gp_s[10]; /* s10 */
+ f->regs[27] = (uint64_t)gp->gp_s[11]; /* s11 */
+ f->regs[28] = (uint64_t)gp->gp_t[3]; /* t3 */
+ f->regs[29] = (uint64_t)gp->gp_t[4]; /* t4 */
+ f->regs[30] = (uint64_t)gp->gp_t[5]; /* t5 */
+ f->regs[31] = (uint64_t)gp->gp_t[6]; /* t6 */
+ f->pc = (uint64_t)gp->gp_sepc;
+ f->cfa = (uint64_t)gp->gp_s[0]; /* s0/fp; CFI refines */
+}
+
+void dbg_frame_to_ucontext(const KitUnwindFrame* f, ucontext_t* uc) {
+ struct gpregs* gp = &uc->uc_mcontext.mc_gpregs;
+ gp->gp_ra = (__register_t)f->regs[1];
+ gp->gp_sp = (__register_t)f->regs[2];
+ gp->gp_gp = (__register_t)f->regs[3];
+ gp->gp_tp = (__register_t)f->regs[4];
+ gp->gp_t[0] = (__register_t)f->regs[5];
+ gp->gp_t[1] = (__register_t)f->regs[6];
+ gp->gp_t[2] = (__register_t)f->regs[7];
+ gp->gp_s[0] = (__register_t)f->regs[8];
+ gp->gp_s[1] = (__register_t)f->regs[9];
+ gp->gp_a[0] = (__register_t)f->regs[10];
+ gp->gp_a[1] = (__register_t)f->regs[11];
+ gp->gp_a[2] = (__register_t)f->regs[12];
+ gp->gp_a[3] = (__register_t)f->regs[13];
+ gp->gp_a[4] = (__register_t)f->regs[14];
+ gp->gp_a[5] = (__register_t)f->regs[15];
+ gp->gp_a[6] = (__register_t)f->regs[16];
+ gp->gp_a[7] = (__register_t)f->regs[17];
+ gp->gp_s[2] = (__register_t)f->regs[18];
+ gp->gp_s[3] = (__register_t)f->regs[19];
+ gp->gp_s[4] = (__register_t)f->regs[20];
+ gp->gp_s[5] = (__register_t)f->regs[21];
+ gp->gp_s[6] = (__register_t)f->regs[22];
+ gp->gp_s[7] = (__register_t)f->regs[23];
+ gp->gp_s[8] = (__register_t)f->regs[24];
+ gp->gp_s[9] = (__register_t)f->regs[25];
+ gp->gp_s[10] = (__register_t)f->regs[26];
+ gp->gp_s[11] = (__register_t)f->regs[27];
+ gp->gp_t[3] = (__register_t)f->regs[28];
+ gp->gp_t[4] = (__register_t)f->regs[29];
+ gp->gp_t[5] = (__register_t)f->regs[30];
+ gp->gp_t[6] = (__register_t)f->regs[31];
+ gp->gp_sepc = (__register_t)f->pc;
+}
diff --git a/driver/env/uctx_x86_64_freebsd.c b/driver/env/uctx_x86_64_freebsd.c
@@ -0,0 +1,55 @@
+/* ucontext_t <-> KitUnwindFrame marshalling for x86_64 on FreeBSD.
+ * mcontext_t has named fields (mc_rax, mc_rdx, …, mc_rip, mc_rsp).
+ * DWARF register numbering (System V x86-64): rax=0 rdx=1 rcx=2 rbx=3
+ * rsi=4 rdi=5 rbp=6 rsp=7 r8=8 r9=9 r10=10 r11=11 r12=12 r13=13 r14=14
+ * r15=15 rip=16. */
+
+#include <stdint.h>
+#include <string.h>
+
+#include "env_posix.h"
+
+void dbg_ucontext_to_frame(const ucontext_t* uc, KitUnwindFrame* f) {
+ const mcontext_t* mc = &uc->uc_mcontext;
+ memset(f, 0, sizeof(*f));
+ f->regs[0] = (uint64_t)mc->mc_rax;
+ f->regs[1] = (uint64_t)mc->mc_rdx;
+ f->regs[2] = (uint64_t)mc->mc_rcx;
+ f->regs[3] = (uint64_t)mc->mc_rbx;
+ f->regs[4] = (uint64_t)mc->mc_rsi;
+ f->regs[5] = (uint64_t)mc->mc_rdi;
+ f->regs[6] = (uint64_t)mc->mc_rbp;
+ f->regs[7] = (uint64_t)mc->mc_rsp;
+ f->regs[8] = (uint64_t)mc->mc_r8;
+ f->regs[9] = (uint64_t)mc->mc_r9;
+ f->regs[10] = (uint64_t)mc->mc_r10;
+ f->regs[11] = (uint64_t)mc->mc_r11;
+ f->regs[12] = (uint64_t)mc->mc_r12;
+ f->regs[13] = (uint64_t)mc->mc_r13;
+ f->regs[14] = (uint64_t)mc->mc_r14;
+ f->regs[15] = (uint64_t)mc->mc_r15;
+ f->regs[16] = (uint64_t)mc->mc_rip;
+ f->pc = (uint64_t)mc->mc_rip;
+ f->cfa = (uint64_t)mc->mc_rsp;
+}
+
+void dbg_frame_to_ucontext(const KitUnwindFrame* f, ucontext_t* uc) {
+ mcontext_t* mc = &uc->uc_mcontext;
+ mc->mc_rax = (__register_t)f->regs[0];
+ mc->mc_rdx = (__register_t)f->regs[1];
+ mc->mc_rcx = (__register_t)f->regs[2];
+ mc->mc_rbx = (__register_t)f->regs[3];
+ mc->mc_rsi = (__register_t)f->regs[4];
+ mc->mc_rdi = (__register_t)f->regs[5];
+ mc->mc_rbp = (__register_t)f->regs[6];
+ mc->mc_rsp = (__register_t)f->regs[7];
+ mc->mc_r8 = (__register_t)f->regs[8];
+ mc->mc_r9 = (__register_t)f->regs[9];
+ mc->mc_r10 = (__register_t)f->regs[10];
+ mc->mc_r11 = (__register_t)f->regs[11];
+ mc->mc_r12 = (__register_t)f->regs[12];
+ mc->mc_r13 = (__register_t)f->regs[13];
+ mc->mc_r14 = (__register_t)f->regs[14];
+ mc->mc_r15 = (__register_t)f->regs[15];
+ mc->mc_rip = (__register_t)f->pc;
+}
diff --git a/driver/lib/hosted.c b/driver/lib/hosted.c
@@ -184,12 +184,21 @@ static int hosted_add_linux_defines(DriverHostedPlan* plan, int gnu) {
return 0;
}
-static int hosted_add_freebsd_defines(DriverHostedPlan* plan) {
+static const char* freebsd_version_str(uint8_t major) {
+ /* Static strings indexed by major version; 0 = unspecified → "15". */
+ static const char* const tab[21] = {
+ "15",
+ "1","2","3","4","5","6","7","8","9","10",
+ "11","12","13","14","15","16","17","18","19","20"
+ };
+ return (major < 21) ? tab[major] : "15";
+}
+
+static int hosted_add_freebsd_defines(DriverHostedPlan* plan, uint8_t version_major) {
/* __FreeBSD__ must be present and version-encoded or the base headers fail to
- * compile; the major can't be derived from the triple, so 14 is a documented
- * conservative default. */
+ * compile. The version is derived from the triple; 0 falls back to 15. */
if (hosted_add_clang_compat_defines(plan) != 0 ||
- hosted_add_define(plan, "__FreeBSD__", "14") != 0 ||
+ hosted_add_define(plan, "__FreeBSD__", freebsd_version_str(version_major)) != 0 ||
hosted_add_define(plan, "__ELF__", "1") != 0 ||
hosted_add_define(plan, "__unix__", "1") != 0 ||
hosted_add_define(plan, "__unix", "1") != 0 ||
@@ -420,7 +429,7 @@ static int hosted_resolve_freebsd(const DriverHostedRequest* req,
static_link = req->static_link && hosted_libdir_has(req->env, dirs, "libc.a");
plan->profile_name = static_link ? "freebsd-static" : "freebsd-dynamic";
if (!static_link) plan->interp_path = "/libexec/ld-elf.so.1";
- if (hosted_add_freebsd_defines(plan) != 0 ||
+ if (hosted_add_freebsd_defines(plan, req->target.os_version_major) != 0 ||
hosted_add_incdirs(plan, req->env, dirs) != 0) {
driver_errf(req->tool, "out of memory");
return 1;
diff --git a/driver/lib/target.c b/driver/lib/target.c
@@ -377,8 +377,15 @@ int driver_target_from_triple(const char* triple, KitTargetSpec* out) {
break;
}
if (triple_tok_prefix(parts[i], plen[i], "freebsd")) {
+ const char* ver = parts[i] + 7; /* skip "freebsd" */
+ size_t rem = plen[i] - 7;
+ unsigned v = 0;
+ size_t j;
+ for (j = 0; j < rem && ver[j] >= '0' && ver[j] <= '9'; ++j)
+ v = v * 10 + (unsigned)(ver[j] - '0');
t.os = KIT_OS_FREEBSD;
t.obj = KIT_OBJ_ELF;
+ t.os_version_major = (uint8_t)(v > 255 ? 0 : v);
os_set = 1;
break;
}
diff --git a/include/kit/core.h b/include/kit/core.h
@@ -200,6 +200,7 @@ typedef struct KitTargetSpec {
uint8_t wchar_size; /* sizeof(wchar_t): 2 on Windows, else 4 */
uint8_t long_double_format; /* KitLongDoubleFormat */
uint8_t emits_eh_frame; /* 1 when os != KIT_OS_FREESTANDING, else 0 */
+ uint8_t os_version_major; /* OS major version (FreeBSD: 14/15/…); 0 = unspecified */
} KitTargetSpec;
typedef struct KitTargetFeature {
diff --git a/mk/env.mk b/mk/env.mk
@@ -95,6 +95,7 @@ endif
DRIVER_ENV_OS_CFLAGS :=
DRIVER_ENV_HINT_SRC :=
HOST_LDLIBS :=
+HOST_ENV_LDFLAGS :=
ifeq ($(HOST_OS),darwin)
DRIVER_ENV_OS_CFLAGS := -D_XOPEN_SOURCE=600 -D_DARWIN_C_SOURCE=1
@@ -109,6 +110,9 @@ DRIVER_ENV_HINT_SRC := driver/env/linux_exec_hint_default.c
endif
else ifeq ($(HOST_OS),freebsd)
DRIVER_ENV_OS_SRC := driver/env/freebsd.c
+HOST_LDLIBS += -lpthread
+# Export all globals so libc.so can resolve symbols like `environ` from the exe
+HOST_ENV_LDFLAGS += -rdynamic
else ifeq ($(HOST_OS),windows)
# windows.c subsumes posix.c / posix_dbg.c / jit_tls_posix.c and folds in
# its own CONTEXT-based register marshalling -- there's no POSIX overlap
@@ -143,6 +147,14 @@ DRIVER_ENV_UCTX_SRC := driver/env/uctx_x86_64_linux.c
else ifeq ($(HOST_ARCH),rv64)
DRIVER_ENV_UCTX_SRC := driver/env/uctx_rv64_linux.c
endif
+else ifeq ($(HOST_OS),freebsd)
+ifeq ($(HOST_ARCH),aarch64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_freebsd.c
+else ifeq ($(HOST_ARCH),x86_64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_x86_64_freebsd.c
+else ifeq ($(HOST_ARCH),rv64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_rv64_freebsd.c
+endif
endif
ifneq ($(HOST_OS),windows)
diff --git a/mk/flags.mk b/mk/flags.mk
@@ -36,7 +36,7 @@ endif
CFLAGS_COMMON = $(HOST_OPTFLAGS) $(HOST_MODE_CPPFLAGS) $(HOST_MODE_CFLAGS) \
-std=c11 -Wpedantic -Wall -Wextra -Werror
HOST_CFLAGS = $(CFLAGS_COMMON) $(HOST_SYSROOT_CFLAGS)
-HOST_LDFLAGS = $(HOST_SYSROOT_LDFLAGS) $(HOST_MODE_LDFLAGS)
+HOST_LDFLAGS = $(HOST_SYSROOT_LDFLAGS) $(HOST_MODE_LDFLAGS) $(HOST_ENV_LDFLAGS)
DEPFLAGS = -MMD -MP
diff --git a/scripts/freebsd_sysroot.sh b/scripts/freebsd_sysroot.sh
@@ -64,7 +64,9 @@ SYSROOT_MEMBERS=(
./usr/lib/libssp_nonshared.a ./usr/lib/libc_nonshared.a
./usr/lib/libcompiler_rt.a ./usr/lib/libgcc.a ./usr/lib/libgcc_eh.a
./usr/lib/libgcc_s.so
+ ./usr/lib/libpthread.a ./usr/lib/libthr.a
./lib/libc.so.7 ./lib/libsys.so.7 ./lib/libgcc_s.so.1
+ ./lib/libpthread.so.3 ./lib/libthr.so.3
)
fetch_txz() {
@@ -107,6 +109,18 @@ build_sysroot() {
# gate on libc.a instead.
tar -xf "$txz" -C "$dst" "${SYSROOT_MEMBERS[@]}" 2>/dev/null || true
[ -f "$dst/usr/lib/libc.a" ] || die "extraction incomplete for $arch (no usr/lib/libc.a)"
+ # FreeBSD uses libthr as the real thread library; libpthread is a symlink.
+ # Recreate the symlinks so -lpthread/-lthr resolve correctly.
+ if [ -f "$dst/lib/libthr.so.3" ]; then
+ [ -e "$dst/usr/lib/libthr.so" ] || ln -s ../../lib/libthr.so.3 "$dst/usr/lib/libthr.so"
+ [ -e "$dst/usr/lib/libpthread.so" ] || ln -s ../../lib/libthr.so.3 "$dst/usr/lib/libpthread.so"
+ fi
+ if [ -f "$dst/lib/libpthread.so.3" ]; then
+ [ -e "$dst/usr/lib/libpthread.so" ] || ln -s ../../lib/libpthread.so.3 "$dst/usr/lib/libpthread.so"
+ fi
+ if [ -f "$dst/usr/lib/libthr.a" ] && [ ! -e "$dst/usr/lib/libpthread.a" ]; then
+ ln -s libthr.a "$dst/usr/lib/libpthread.a"
+ fi
printf 'sysroot ready: %s\n' "$dst"
}
diff --git a/scripts/freebsd_vm.sh b/scripts/freebsd_vm.sh
@@ -39,6 +39,7 @@ commands:
run <arch> [qemu...] run VM in foreground with host-only SSH forwarding
wait-ssh <arch> wait for SSH and print uname
ssh <arch> [cmd...] SSH into a running VM
+ scp <arch> <src> <dst> copy a file into the running VM (dst is remote path)
run-batch <arch> <dir> <res>
ship a staging dir into the VM, run its run-remote.sh,
and bring per-binary res/<id>.{rc,out,err} back into
@@ -631,6 +632,17 @@ ssh_arch() {
exec ssh "${args[@]}" "$SSH_USER@127.0.0.1" "$@"
}
+scp_arch() {
+ local arch="$1" src="$2" dst="$3" port args
+ arch="$(canon_arch "$arch")"
+ port="$(ssh_port "$arch")"
+ # shellcheck disable=SC2207
+ args=($(ssh_args "$arch"))
+ exec scp -P "$port" -i "$SSH_KEY" \
+ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
+ "$src" "$SSH_USER@127.0.0.1:$dst"
+}
+
# run_batch ARCH STAGEDIR RESULTSDIR
# Run a whole staging dir in a single SSH session and bring per-binary results
# back. Pure transport: tar the stagedir in on stdin, the guest extracts it and
@@ -685,6 +697,7 @@ case "$cmd" in
run) [ $# -ge 2 ] || { usage; exit 2; }; arch="$2"; shift 2; run_arch "$arch" "$@" ;;
wait-ssh) [ $# -eq 2 ] || { usage; exit 2; }; wait_ssh "$2" ;;
ssh) [ $# -ge 2 ] || { usage; exit 2; }; arch="$2"; shift 2; ssh_arch "$arch" "$@" ;;
+ scp) [ $# -eq 4 ] || { usage; exit 2; }; scp_arch "$2" "$3" "$4" ;;
run-batch) [ $# -eq 4 ] || { usage; exit 2; }; run_batch_arch "$2" "$3" "$4" ;;
-h|--help|help|"") usage ;;
*) usage; exit 2 ;;
diff --git a/scripts/kit_freebsd.sh b/scripts/kit_freebsd.sh
@@ -0,0 +1,140 @@
+#!/usr/bin/env bash
+# scripts/kit_freebsd.sh — cross-compile the kit binary for FreeBSD.
+#
+# Builds kit for a FreeBSD target arch using the sysroot from
+# scripts/freebsd_sysroot.sh and the host clang/lld cross-compilation
+# toolchain. Outputs to build/kit-freebsd-<arch>/kit.
+#
+# usage:
+# scripts/kit_freebsd.sh [arch] build (default arch: aarch64)
+# scripts/kit_freebsd.sh run [arch] build then run in the FreeBSD VM
+# scripts/kit_freebsd.sh scp [arch] scp the binary to the running VM
+#
+# arches: aarch64|arm64|aa64 amd64|x64 riscv64|rv64
+#
+# env:
+# KIT_FREEBSD_RELEASE (default: 15.0-RELEASE)
+# LLVM_AR path to llvm-ar (default: llvm-ar)
+# JOBS make -j value (default: nproc)
+
+set -eu
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+RELEASE_TAG="${KIT_FREEBSD_RELEASE:-15.0-RELEASE}"
+LLVM_AR="${LLVM_AR:-llvm-ar}"
+JOBS="${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)}"
+
+die() { printf 'kit_freebsd: %s\n' "$*" >&2; exit 1; }
+
+canon_arch() {
+ case "${1:-}" in
+ aarch64|arm64|aa64) echo aarch64 ;;
+ amd64|x64|x86_64) echo x86_64 ;;
+ riscv64|rv64) echo riscv64 ;;
+ *) die "unknown arch '${1:-}' (want aarch64|amd64|riscv64)" ;;
+ esac
+}
+
+# FreeBSD triple — clang accepts aarch64-unknown-freebsd15.0 etc.
+clang_triple() {
+ local ver="${RELEASE_TAG%%-*}" major="${RELEASE_TAG%%.0*}"
+ major="${major%%-*}"
+ case "$(canon_arch "$1")" in
+ aarch64) echo "aarch64-unknown-freebsd${major}" ;;
+ x86_64) echo "x86_64-unknown-freebsd${major}" ;;
+ riscv64) echo "riscv64-unknown-freebsd${major}" ;;
+ esac
+}
+
+# mk/env.mk HOST_UNAME and HOST_ARCH_RAW overrides
+make_host_uname() { echo FreeBSD; }
+make_host_arch_raw() {
+ case "$(canon_arch "$1")" in
+ aarch64) echo aarch64 ;;
+ x86_64) echo amd64 ;;
+ riscv64) echo riscv64 ;;
+ esac
+}
+
+cmd="${1:-aarch64}"
+case "$cmd" in
+ run|scp) arch="${2:-aarch64}"; do_run="$cmd" ;;
+ *) arch="$cmd"; do_run="" ;;
+esac
+arch="$(canon_arch "$arch")"
+triple="$(clang_triple "$arch")"
+sysroot="$("$ROOT/scripts/freebsd_sysroot.sh" path "$arch" 2>/dev/null)"
+[ -f "$sysroot/usr/lib/libc.a" ] || {
+ printf 'sysroot missing; running: scripts/freebsd_sysroot.sh %s\n' "$arch" >&2
+ "$ROOT/scripts/freebsd_sysroot.sh" "$arch"
+ sysroot="$("$ROOT/scripts/freebsd_sysroot.sh" path "$arch" 2>/dev/null)"
+}
+build_dir="$ROOT/build/kit-freebsd-$arch"
+
+printf 'cross-compile kit for %s (%s) -> %s/kit\n' "$arch" "$triple" "$build_dir" >&2
+
+# GNU make splits CC on whitespace before parsing, so a CC with spaces can't
+# be passed on the command line directly. Write a small wrapper script and
+# pass that as CC instead.
+mkdir -p "$build_dir"
+cc_wrapper="$build_dir/cross_cc"
+cat > "$cc_wrapper" << 'WRAPPER_EOF'
+#!/usr/bin/env bash
+# Cross-compile wrapper: --target and --sysroot always; -fuse-ld=lld only
+# when linking (i.e. when -c / -E / -S / --precompile is NOT in the args).
+WRAPPER_TRIPLE="__TRIPLE__"
+WRAPPER_SYSROOT="__SYSROOT__"
+linking=1
+for a in "$@"; do
+ case "$a" in -c|-E|-S|--precompile) linking=0; break ;; esac
+done
+extra=""
+[ "$linking" -eq 1 ] && extra="-fuse-ld=lld"
+# shellcheck disable=SC2086
+exec clang --target="$WRAPPER_TRIPLE" --sysroot="$WRAPPER_SYSROOT" $extra "$@"
+WRAPPER_EOF
+# Substitute the placeholders (no spaces in sysroot/triple so sed is safe)
+sed -i "s|__TRIPLE__|$triple|g; s|__SYSROOT__|$sysroot|g" "$cc_wrapper"
+chmod +x "$cc_wrapper"
+
+host_uname="$(make_host_uname)"
+host_arch_raw="$(make_host_arch_raw "$arch")"
+
+make -C "$ROOT" bin -j"$JOBS" \
+ CC="$cc_wrapper" \
+ AR="$LLVM_AR" \
+ HOST_UNAME="$host_uname" \
+ HOST_ARCH_RAW="$host_arch_raw" \
+ BUILD_DIR="$build_dir" \
+ RELEASE=1
+
+out="$build_dir/kit"
+printf 'built: %s\n' "$out" >&2
+file "$out" >&2
+
+if [ -z "$do_run" ]; then
+ exit 0
+fi
+
+# VM arch tag for freebsd_vm.sh
+vm_arch() {
+ case "$(canon_arch "$1")" in
+ aarch64) echo aarch64 ;;
+ x86_64) echo amd64 ;;
+ riscv64) echo riscv64 ;;
+ esac
+}
+vm_tag="$(vm_arch "$arch")"
+
+case "$do_run" in
+ scp)
+ printf 'copying to VM...\n' >&2
+ "$ROOT/scripts/freebsd_vm.sh" scp "$vm_tag" "$out" /home/kit/kit
+ printf 'done\n' >&2
+ ;;
+ run)
+ printf 'copying and running in VM...\n' >&2
+ "$ROOT/scripts/freebsd_vm.sh" scp "$vm_tag" "$out" /home/kit/kit
+ "$ROOT/scripts/freebsd_vm.sh" ssh "$vm_tag" '/home/kit/kit version 2>&1 || true'
+ ;;
+esac