kit

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

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:
Ddoc/plan/FREEBSD.md | 203-------------------------------------------------------------------------------
Mdoc/plan/README.md | 1-
Mdriver/env/freebsd.c | 7++-----
Adriver/env/uctx_aarch64_freebsd.c | 26++++++++++++++++++++++++++
Adriver/env/uctx_rv64_freebsd.c | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/env/uctx_x86_64_freebsd.c | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/lib/hosted.c | 19++++++++++++++-----
Mdriver/lib/target.c | 7+++++++
Minclude/kit/core.h | 1+
Mmk/env.mk | 12++++++++++++
Mmk/flags.mk | 2+-
Mscripts/freebsd_sysroot.sh | 14++++++++++++++
Mscripts/freebsd_vm.sh | 13+++++++++++++
Ascripts/kit_freebsd.sh | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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