= FreeBSD x86_64 Native Backend and Cross-Compilation — Design
:author: Graeme Geldenhuys
:revdate: 2026-06-24
:toc: left
:toclevels: 3
:source-highlighter: rouge
:icons: font
:sectanchors:
:sectnums:

== Context

Blaise's default backend is the native x86_64 code generator with an
in-process assembler and ELF linker.  No external assembler or linker is
invoked: source compiles directly to a runnable ELF executable.  Today the
only supported target is `linux-x86_64`.

This document specifies the addition of a `freebsd-x86_64` target and, more
broadly, the work required to make the native backend a genuine
*cross-compiler* — producing a FreeBSD (and later Windows) binary while
running on a Linux host, with no FreeBSD machine, no QEMU, and no external
toolchain in the loop.

FreeBSD/x86_64 is the right first cross-target: it shares the System V AMD64
ABI and the ELF container with Linux, so the instruction selector, register
allocator, ABI lowering, and the bulk of the ELF linker are reused unchanged.
The differences are confined to a small, well-bounded surface.

=== Is this possible?

Yes.  The conclusion of the investigation below is that a FreeBSD x86_64
backend is achievable with *no changes to instruction selection or the
register allocator*, and that the existing architecture has already been
deliberately staged for it.  The work is real but bounded — it is
parameterisation and one new system-call surface, not a second backend.

The single largest decision is how the FreeBSD binary reaches the kernel.
That decision (libc-linked vs. direct-syscall) shapes the whole effort and is
discussed in <<syscall-strategy>>.

== Current architecture: what already generalises

The codebase has been written with the second target in mind.  The following
are already target-parameterised and require *no* per-OS work:

[cols="2,4",options="header"]
|===
| Component | State

| Target model (`blaise.codegen.target.pas`)
| `TTargetOS` already enumerates `osLinux, osFreeBSD, osWindows, osMacOS`;
  `TTargetCPU` has `cpuX86_64`.  `ParseTargetName` already accepts
  `"freebsd-x86_64"`.  The host/`--target` plumbing exists.

| Instruction selection / regalloc / ABI (`blaise.codegen.native.x86_64.pas`)
| System V AMD64 ABI is shared between Linux and FreeBSD x86_64.  Argument
  classification, the record-return classifier
  (`ClassifyRecordReturn` in `blaise.codegen.native.backend.pas`), SSE vs
  integer eightbyte rules — all identical.  No change expected.

| ELF object/relocation core (`blaise.linker.elf.pas`)
| The relocation set handled
  (`R_X86_64_64/PC32/PLT32/GOTPCREL/RELATIVE/TPOFF32/JUMP_SLOT/GLOB_DAT/...`)
  is architecture-defined, not OS-defined.  Section merging, symbol
  resolution, and address layout are OS-agnostic.

| Linker target record (`TLinkTarget`)
| Already carries `OSABI` (with `ELFOSABI_FREEBSD = 9` already defined),
  `EMachine`, `BaseAddr`, `PageSize` as per-target knobs rather than
  hard-coded constants.

| OPDF debug section
| The `.opdf.*` sections are non-allocatable and ride through the linker
  unchanged into the final ELF (the merger explicitly keeps them).  The
  format is container-level identical on any ELF target.
|===

The design intent is recorded in the source: `blaise.codegen.target.pas`
states that "FreeBSD/x86_64 shares the System V AMD64 ABI with Linux, so the
x86_64 instruction selection will be reused; the OS differences are confined
to the link line."  The linker header comment notes the same for `OSABI` and
the `-pre`-existing FreeBSD constant.

== Where Linux assumptions are baked in

The OS-specific assumptions are concentrated in three places.  These are the
entire FreeBSD work surface.

=== The runtime reaches the kernel through glibc

`compiler/src/main/pascal/rtl.platform.posix.pas` is *pure Pascal* but is not
syscall-direct: every primitive is an `external name` binding to a libc
symbol — `open`, `read`, `write`, `lseek`, `close`, `fstat`, `stat`,
`mkdir`, `clock_gettime`, `fork`, `execvp`, `waitpid`, `nanosleep`,
`getenv`, `exit`, etc.

Consequences for FreeBSD:

* *Symbol level — portable.* The POSIX names are the same on FreeBSD libc.
  The Pascal source needs no change for the call sites.
* *Struct level — NOT portable.* The hand-rolled `TStatBuf` and `TTm`
  records in this unit use *Linux* `struct stat` / `struct tm` field
  ordering and sizes.  FreeBSD's `struct stat` has a different layout
  (e.g. `st_ino` is 64-bit and field order differs; FreeBSD 12+ changed
  `ino_t`/`dev_t` widths).  `TStatBuf.Size`, `.Mtime`, `.Mode` would read
  garbage offsets if reused verbatim.  `struct tm` is largely compatible
  but `tm_gmtoff`/`tm_zone` placement must be verified.
* *Constant level — NOT portable.* `O_CREAT`, `O_TRUNC`, `O_APPEND`,
  `S_IFDIR`, `CLOCK_REALTIME`, `WNOHANG`, `SEEK_*` are hard-coded Linux
  values in the `const` block.  Several differ on FreeBSD (notably the
  `O_*` flag bits and `WNOHANG`).

=== The entry/startup model assumes glibc

`blaise.codegen.native.x86_64.pas` (`EmitProgram`) emits an exported
`main(argc, argv)`.  The runtime now ships its *own* `_start` (the
`runtime.start` RTL unit, since master commit 626ee4c9) which reads
`argc`/`argv` off the stack and calls `__libc_start_main(main, ...)`; the
internal linker (`LinkViaInternalLinker` in `blaise.codegen.native.driver.pas`)
no longer scans for the host CRT objects (`Scrt1.o`, `crti.o`, `crtn.o`).  It
links:

* the program object and the source-built RTL objects (including
  `runtime.start`, which provides `_start`);
* in the default dynamic mode, against `libc.so.6` via the interpreter
  `/lib64/ld-linux-x86-64.so.2` (both strings hard-coded in
  `TLinker.BuildDynamic`) — so `__libc_start_main` is still resolved from
  glibc on the Linux target.

`__libc_start_main`, `libc.so.6`, and the glibc loader path are glibc-specific.
None exist on FreeBSD, whose run-time linker (`/libexec/ld-elf.so.1`) and libc
differ.  Because the runtime already owns `_start` and the linker already has a
static `ET_EXEC` / `_start` path (Strategy B), FreeBSD needs only a FreeBSD
`_start` variant that calls `main` directly and exits via syscall — the
link-time-swap seam is present (Step 3).

=== Toolchain discovery is Linux-pathed

`uToolchain.pas` and the driver:

* `FindCrtObjects` searches Linux library directories only.
* The RTL is no longer a pre-built per-host archive.  `EnsureRTLObjects`
  (`blaise.codegen.driver.pas`) compiles the embedded RTL source
  (`compiler/src/main/pascal/runtime.*`, `rtl.platform.*`) in-process and links
  the resulting `.o` files; the legacy `FindRTLArchive`/`blaise_rtl.a` path is
  gone.  The RTL unit list it compiles is Linux-specific (it names
  `rtl.platform.layout.linux`); a FreeBSD program must compile the FreeBSD
  adapter set instead (Step 5).
* The internal linker now honours `--target` (Step 1): it builds the resolved
  toolkit's `TLinkTarget` rather than always `LinuxX86_64Target()`.  What
  remains Linux-specific is the driver's unconditional `FindCrtObjects` /
  dynamic-libc link line (Step 6 selects the static, CRT-free path for FreeBSD).

[#syscall-strategy]
== The central decision: how does the FreeBSD binary reach the kernel?

This is the one decision that shapes everything.  Two viable strategies:

=== Strategy A — link against FreeBSD libc (cross-link)

Keep the existing libc-binding model.  Produce a dynamically-linked ELF that
imports FreeBSD `libc.so.7`, uses interpreter `/libexec/ld-elf.so.1`, sets
`OSABI = ELFOSABI_FREEBSD`, and is started by FreeBSD `crt1.o`.

* *Pros:* minimal new code; reuses the entire dynamic-link path; the RTL
  stays libc-based; matches how a normal FreeBSD binary is built.
* *Cons (for a true Linux-hosted cross-compile):* the Linux host does not
  have FreeBSD's `crt1.o` / `crti.o` / `crtn.o` or a `libc.so.7` import
  stub.  To cross-link you must stage a *FreeBSD sysroot* on the Linux host
  (a copy of FreeBSD's `/usr/lib` CRT objects + libc) and point
  `FindCrtObjects`/the interp/needed-lib at it.  The CRT objects are
  redistributable; this is the standard cross-toolchain approach.

=== Strategy B — direct syscalls, no libc (freestanding)

Bypass libc entirely on FreeBSD: emit our own `_start` that reads
`argc/argv` off the stack, and replace the `external name 'open'` bindings
with a FreeBSD syscall stub layer.  Produce a *static* `ET_EXEC`
(the linker's Phase B static path already exists for exactly this
"hand-written object talks to the kernel through raw syscalls" case).

* *Pros:* genuine zero-dependency cross-compile from Linux with *nothing*
  staged — no FreeBSD sysroot, no CRT objects, no libc.  This is the purest
  expression of the stated goal ("cross-compile a FreeBSD binary from my
  Linux system").  The produced binary is also self-contained on the target.
* *Cons:* requires a FreeBSD syscall layer in the RTL, and — critically —
  the runtime's libc dependency is *deeper than file I/O*.  An audit of the
  `external name` bindings across `runtime/` shows the RTL also binds:
+
[cols="2,4",options="header"]
!===
! libc symbol(s) ! Why a raw-syscall replacement is non-trivial

! `pthread_create/join/mutex_*` (`runtime.thread.pas`)
! Threading.  Reimplementing on raw `thr_new`/`_umtx_op` FreeBSD syscalls is
  real work, not a thin stub.

! `mmap/munmap/mremap` (`runtime.mem.pas`)
! The memory manager.  `mmap`/`munmap` map to syscalls cleanly, but FreeBSD
  has no `mremap`; the allocator's in-place-grow path needs a fallback.

! `memcpy/memset/memcmp`
! Hot primitives.  Either bind to a freestanding implementation or emit
  inline — cannot assume libc is present.

! `localtime_r/gmtime_r/timegm` (`rtl.platform.posix.pas`)
! Timezone-aware date math.  No syscall equivalent; needs a pure-Pascal
  tz/calendar implementation or shipping the rules.

! `setjmp`/`longjmp` (`_blaise_longjmp`, `runtime.setjmp.pas`)
! Exception unwinding.  Hand-written inline assembly (the former
  `blaise_setjmp_x86_64.s`, since migrated into `runtime.setjmp.pas`) — portable,
  but must be verified for the FreeBSD stack/`jmp_buf` layout.

! `atexit`
! Finalisation.  Replaceable with our own registry plus an `exit`-time flush.
!===
+
FreeBSD's syscall ABI itself is straightforward (number in `%rax`, args in
`rdi, rsi, rdx, r10, r8, r9` — same registers as Linux, *different syscall
numbers*; errors via the carry flag, not a negative return).  `struct stat`
layout still differs (same issue as Strategy A).  The honest cost is the
non-I/O leaves above, not the syscall mechanism.

=== Decision: Strategy B (direct syscalls)

*Strategy B is chosen.*  It is the only route that delivers the stated goal —
cross-compiling a runnable FreeBSD binary from a Linux host with no external
tools and nothing staged — and the static `ET_EXEC` / `_start` path it needs
already exists in the linker (Phase B).  Strategy A is recorded above only as
the fallback shape for users who later want a conventional libc-linked FreeBSD
binary; it is not on this implementation path.

The audit above (the libc surface beyond file I/O — threads, the memory
manager, libc string primitives, timezone-aware date math) is *not* a reason
to avoid Strategy B; it is the *work breakdown* for Strategy B.  Each libc leaf
becomes a method on a target-keyed kernel port and is reimplemented hardest-
last:

. *Easy leaves first* — `open/read/write/close/lseek/fstat/stat/mkdir/unlink/
  rename/getcwd/chdir/getenv/exit/nanosleep/pipe/dup2/fork/execvp/waitpid`
  map directly to FreeBSD syscalls.
. *`memcpy/memset/memcmp`* — bind to freestanding implementations (or emit
  inline); never assume libc is present.
. *`localtime_r/gmtime_r/timegm`* — a pure-Pascal calendar/timezone
  implementation (no syscall equivalent).
. *Memory manager (`mmap/munmap`, no `mremap` on FreeBSD)* — syscalls plus a
  grow-path fallback.
. *Threads (`pthread_*`)* — FreeBSD `thr_new`/`_umtx_op`; the hardest leaf,
  done last.
. *`setjmp/longjmp`* — already hand-written asm; verify the FreeBSD
  `jmp_buf`/stack layout.
. *`atexit`* — our own registry flushed at `exit`.

=== No `{$IFDEF}`, no scattered target code — ports and adapters

A hard requirement on this work: FreeBSD support must NOT be expressed with
`{$IFDEF}` directives or `include`-file substitution, and target-specific code
must not be scattered across units.  Blaise already rejects that model —
`rtl.platform.pas` is explicitly `{$IFDEF}`-free and selects behaviour by
subclassing.  This backend completes that idiom.

The full structural design lives in
`docs/native-target-architecture.adoc`.  In brief, each target is a *family of
adapter classes* produced by an Abstract Factory (`TTargetToolkit`) resolved by
name from a `TTargetRegistry`.  The two per-target seams Strategy B needs:

* *The kernel leaf* — the raw syscalls + `_start` entry stub.  This is a
  *link-time symbol swap*, NOT a runtime port: the FreeBSD RTL archive links a
  syscall-stub object that resolves the same `external name` symbols (`open`,
  `read`, …) that libc resolves on Linux.  (A runtime `TKernelABI` virtual port
  was tried and reverted — it destabilised self-hosting for zero present
  benefit; see the kernel-leaf note in the architecture doc.)
* `TPlatformLayout` — `struct stat` layout and the OS constants (`O_*`,
  `CLOCK_*`, `WNOHANG`, `SEEK_*`), moved behind a per-target runtime port
  (Step 0b, done) instead of leaking into a "shared" adapter.

Target divergence therefore lives in *which adapter object is instantiated and
which RTL archive is linked* — and the test runner can select the FreeBSD
adapter to test it natively on a FreeBSD host, or structurally from Linux.
The implementation steps below assume the structural refactors in the
architecture doc (toolkit/registry — Step 0a done; `TPlatformLayout` — Step 0b
done) are already in place.

== Implementation plan

Each step states a verification check, per the project's goal-driven
convention.  Steps 0a–0b are the behaviour-preserving structural refactor from
`docs/native-target-architecture.adoc` (no FreeBSD code, no behaviour change —
verified against the existing Linux suite/fixpoints; *both are done*).  Steps
1–9 add the FreeBSD target as a new adapter family on those clean seams.
Strategy B (direct syscalls, static `ET_EXEC`) is assumed throughout; there is
no dynamic-link / sysroot work on this path.

=== Step 0a — Toolkit + registry (refactor, no behaviour change)

* Introduce `TTargetToolkit` (Abstract Factory) and `TTargetRegistry`
  (name → toolkit).  Register the existing Linux target as
  `TLinuxX86_64Toolkit`.  Re-express `--target` resolution and
  `TargetHasNativeBackend` via the registry instead of the current hard-wired
  `LinuxX86_64Target()` / `case`.
* *Verify:* full suite + all fixpoints unchanged (pure refactor).

=== Step 0b — Extract `TPlatformLayout` (refactor, no behaviour change)

* Move the `TStatBuf`/`TTm` record layouts and the `O_*`/`S_*`/`CLOCK_*`/
  `WNOHANG`/`SEEK_*` constants out of `rtl.platform.posix.pas` and behind a
  `TPlatformLayout` port.  The Linux adapter supplies today's values verbatim.
  No `{$IFDEF}`; the layout object is selected at startup.
* *Verify:* full suite unchanged (the Linux layout reproduces current values).

=== Step 0c — Kernel leaf: link-time swap, not a runtime port (revised)

* *No refactor in this step.*  A runtime `TKernelABI` virtual port was attempted
  and reverted: routing the RTL's kernel calls through a 30-method virtual class
  + a new global destabilised self-hosting (segfaulting binaries, non-converging
  across bootstrap stages) for zero present benefit — the Linux adapter was pure
  libc delegation.  See the kernel-leaf note in
  `docs/native-target-architecture.adoc`.
* The RTL keeps its `external name 'open'`/`'read'`/… bindings unchanged.  The
  FreeBSD kernel leaf is introduced as a *link-time symbol-stub object* when the
  FreeBSD RTL archive is built (Step 4), resolving those same symbols with raw
  syscalls.  Nothing in the Linux RTL changes.
* The ARC-walker lift into `TNativeBackend` template methods is *deferred* —
  it is arm64 groundwork (FreeBSD reuses the x86_64 backend unchanged) and does
  not block this target.

=== Step 1 — FreeBSD link target + toolkit skeleton

NOTE: Done.  The FreeBSD toolkit is registered and the internal linker honours
`--target freebsd-x86_64` (stamps `EI_OSABI = 9`); test
`TLinkerE2ETests.TestLink_FreeBSDTarget_StampsOSABI`.

* Add `TFreeBSDX86_64Toolkit` and register it.  Its `MakeLinkTarget` returns a
  FreeBSD `TLinkTarget` with `OSABI := ELFOSABI_FREEBSD`,
  `EMachine := EM_X86_64`, and the FreeBSD load base / page size
  (`0x200000` alignment — confirm against a reference FreeBSD `ET_EXEC`).
  `MakeBackend` reuses `TX86_64Backend` (shared System V ABI).
* `blaise.codegen.target.pas`: `TargetHasNativeBackend` returns `True` for
  `(osFreeBSD, cpuX86_64)` via the registry.
* *Verify:* `--target freebsd-x86_64` no longer errors "backend not yet
  implemented"; a unit test asserts the emitted ELF header `EI_OSABI` == 9.

=== Step 2 — FreeBSD `TPlatformLayout` adapter

NOTE: Done.  `TPlatformLayoutFreeBSDX86_64` in
`compiler/src/main/pascal/rtl.platform.layout.freebsd.pas`, pinned to FreeBSD
14.x amd64 (also valid for 13.x — the `ino_t`/`dev_t` widening landed in
FreeBSD 12 and has been stable since).  `struct stat` offsets `st_mode` = 24,
`st_mtim.tv_sec` = 64, `st_size` = 112, `sizeof` = 224 (all differing from
Linux); `O_CREAT`/`O_TRUNC`/`O_APPEND` = `$200`/`$400`/`$008` (differing); the
remaining `S_*`/`SEEK_*`/`CLOCK_REALTIME`/`WNOHANG` share Linux's values.  The
unit is a standalone sibling of `rtl.platform.layout.linux` — nothing in the
default Linux build graph `uses` it, so it is composed only when the FreeBSD
RTL is built (Step 5).  Test `cp.test.platformlayout.freebsd` asserts every
constant and validates the `struct stat` accessors by planting sentinels at the
FreeBSD offsets (catching an offset typo on the Linux host, ahead of the
emulation lane).

* Add `TPlatformLayoutFreeBSDX86_64`: the FreeBSD `struct stat` field offsets
  (note FreeBSD 12+ widened `ino_t`/`dev_t`; pin to a target major version),
  and the FreeBSD `O_*` / `S_IFDIR` / `CLOCK_REALTIME` /
  `WNOHANG` / `SEEK_*` constant values.  (`struct tm` is not abstracted — its
  used fields share an identical layout on Linux/FreeBSD amd64.)
* *Verify:* a FreeBSD `fstat`-based `FileExists`/`FileAge`/`DirectoryExists`
  returns correct results under emulation (Step 8) — static checks cannot
  catch a wrong struct offset.

=== Step 3 — Freestanding `_start` entry stub

NOTE: Done.  `runtime.start.static.freebsd.pas` (the FreeBSD sibling of
`runtime.start.static.linux`) provides `_start`: it captures `%rsp`, aligns the
stack, calls the Pascal `_BlaiseStartC` which parses `argc`/`argv`/`envp`,
captures `environ`, calls `main`, and exits via the FreeBSD `exit` syscall.  The
minimum syscall primitives it needs (`_exit`, `write`, the `environ` global) are
in `runtime.syscall.freebsd.pas` (`SYS_exit` = 1, `SYS_write` = 4); the rest of
the leaf grows there in Step 4.  Step 3 deliberately omits TLS setup — a trivial
program performs no threadvar access — so the auxv `PT_TLS` walk (whose FreeBSD
`AT_*` tags differ from Linux) arrives with the threads work in Step 4, exactly
as it did on Linux.  Test `TLinkerE2ETests.TestLink_FreeBSDStart_StaticExecShape`
links the `_start` fixture with the FreeBSD target and asserts the static
`ET_EXEC` shape: `e_type` = ET_EXEC, entry == `_start`, no `PT_INTERP`.

* The FreeBSD kernel-stub object provides `_start` (not `__libc_start_main`):
  read `argc` at `[%rsp]`, `argv` at `[%rsp+8]`, align the stack, call
  `_SetArgs`, run the body, then `exit` via syscall.  The linker's static
  `ET_EXEC` / `_start` path (Phase B) already supports this — no GOT/PLT, no
  interp, no CRT.
* *Verify:* `freebsd-x86_64` link produces an `ET_EXEC` with entry `_start`
  and no `PT_INTERP`; linker unit test asserts the ELF shape.

=== Step 4 — FreeBSD syscall leaf (the libc-replacement work)

* Provide a FreeBSD syscall trampoline as inline-assembly Pascal stubs (the RTL
  is now pure Pascal with `asm … end` bodies — there are no `.s` files; this
  mirrors the Linux direct-syscall leaf `runtime.syscall.linux.pas` already
  shipped under `--static`): number in `%rax`, args in
  `rdi/rsi/rdx/r10/r8/r9`, `syscall`, then branch on the *carry flag* to
  translate FreeBSD's CF-error convention into the errno-style negative return
  the existing Pascal call sites (`if Fd < 0 ...`) expect.
* Add a FreeBSD syscall-number table (e.g. open=5, read=3, write=4, close=6,
  exit=1, plus `fstat`/`lseek`/`mmap`/`munmap`/… — FreeBSD numbers, *not*
  Linux's).
* Implement the syscall-stub leaves (exporting `open`/`read`/`write`/… as raw
  syscalls) against the trampoline, then the harder leaves per the work
  breakdown above (`memcpy/memset/memcmp` freestanding; pure-Pascal
  `localtime_r/gmtime_r/timegm`; `mmap`-based allocator with a no-`mremap` grow
  fallback; `thr_new`/`_umtx_op` threads last; verify the FreeBSD `jmp_buf` for
  setjmp/longjmp; an `atexit` registry).
* *Verify:* a FreeBSD hello-world + file-I/O smoke test produce correct output
  under emulation.

=== Step 5 — Per-target RTL

* The compiler must link a FreeBSD-built RTL (the FreeBSD `TPlatformLayout` and
  kernel-stub) into a `--target freebsd-x86_64` program, not the host RTL.
* The RTL-unification track (`docs/native-target-architecture.adoc`) has already
  landed its end-state: there is no per-target `.a`.  `EnsureRTLObjects`
  compiles the embedded RTL source in-process and links the `.o` files.  So this
  step is a *unit-list* change, not an archive change: `EnsureRTLObjects` must
  select the FreeBSD adapter set (`rtl.platform.layout.freebsd` in place of
  `…layout.linux`, plus the FreeBSD kernel-stub / `runtime.start.static.freebsd`
  in place of their Linux counterparts) when the target is `freebsd-x86_64`,
  driven off `AOpts.Target` rather than the hard-coded `RTL_UNITS` list.
* *Verify:* a `--target freebsd-x86_64` program links the FreeBSD layout +
  syscall-stub and contains no Linux/libc RTL code.

=== Step 6 — Drive target selection through the linker

* `LinkViaInternalLinker` already builds the resolved toolkit's `TLinkTarget`
  (Step 1), but the driver still drives the Linux dynamic-libc link line.  For
  the Strategy-B static path, when the target is `freebsd-x86_64` skip dynamic
  mode entirely (static `ET_EXEC`, freestanding `_start`, no interp, no libc
  `NEEDED`).
* *Verify:* `--target freebsd-x86_64` end-to-end emits a FreeBSD `ET_EXEC` on
  a Linux host with no external tools invoked.

=== Step 7 — `--target` CLI surface

* Ensure `Blaise.pas` threads `--target freebsd-x86_64` into `AOpts.Target`
  and that defaulting (no `--target`) still resolves to the host.
* *Verify:* `blaise --source hello.pas --target freebsd-x86_64 --output hello`
  produces a file whose ELF header is FreeBSD/x86_64.

=== Step 8 — Cross-compile verification under emulation

The native fixpoint and e2e suites run host binaries; FreeBSD output cannot
execute on the Linux CI host.  Add a verification lane:

* *Static checks (host):* assert the emitted ELF's `EI_OSABI`, `e_type`,
  entry symbol (`_start`), and absence of `PT_INTERP` via the existing ELF
  reader.  These run on every host, including Linux CI, and via the registry
  can cover *every* registered target's codegen/link shape.
* *Dynamic checks (emulated/native):* run the produced FreeBSD binary under a
  FreeBSD VM or `qemu-user` + a FreeBSD root in CI, executing a hello-world
  and a file-I/O smoke test.  This is the only way to catch `struct stat` /
  syscall-number mistakes that static checks miss.  On a FreeBSD host the test
  runner self-targets (`HostTarget()` → FreeBSD toolkit) and exercises the
  adapter natively — see the testing section of
  `docs/native-target-architecture.adoc`.
* *Verify:* CI lane green: FreeBSD hello-world prints `Hello` and exits 0
  under emulation.

== The broader cross-compilation picture (Windows, and Linux-hosted everything)

The user's end goal is "cross-compile from pretty much any platform to
another target" — Linux → FreeBSD → Windows, all from Linux.  FreeBSD is the
near case because it is ELF + System V ABI.  Recording the generalisation
here so the FreeBSD work is shaped to extend rather than be re-done:

* *Container abstraction.* FreeBSD reuses the ELF writer.  Windows needs a
  *PE/COFF* writer and macOS a *Mach-O* writer behind the same `TLinker`
  symbol/relocation core (the linker header already anticipates "a sibling
  writer behind the same TLinker symbol/relocation core").  This is the
  large future item; FreeBSD does not touch it.
* *ABI abstraction.* Windows x64 uses a *different calling convention*
  (RCX/RDX/R8/R9, shadow space, different callee-saved set).  The record
  classifier already has an `osWindows` branch (`rcWin64Agg`), so the seam
  exists, but Windows is materially more backend work than FreeBSD.
* *Syscall/startup abstraction.* Each OS gets its own entry stub and either
  a syscall leaf (Strategy B) or an import-library leaf (Win32: `kernel32`).
  Building the FreeBSD direct-syscall leaf as a clean, link-time-swapped
  syscall-stub object (Step 4) is the template every later OS follows.
* *Host independence.* Strategy B's "no host CRT, no host libc" property is
  what makes Linux-hosted cross-compilation tool-free.  Every target that
  can be driven by direct syscalls (Linux, FreeBSD) should support it;
  targets that cannot (Windows) fall back to a staged import library /
  sysroot, same as Strategy A.

== Risks and mitigations

[cols="2,1,3",options="header"]
|===
| Risk | Likelihood | Mitigation

| FreeBSD `struct stat` layout wrong → silent garbage in file sizes/dates.
| High.
| This is the most error-prone item.  Pin the layout against FreeBSD's
  `sys/stat.h` for the targeted major version; add an emulated CI test that
  stats a known file and asserts its size — static checks cannot catch this.

| FreeBSD syscall numbers differ from Linux (Strategy B).
| High (if reused naively).
| Use a FreeBSD-specific number table; do NOT share Linux's.  Error
  reporting is via carry flag, not negative return — the trampoline must
  translate.

| No FreeBSD execution environment in CI → bugs ship untested.
| Medium.
| Stand up `qemu-user` + FreeBSD root, or a FreeBSD CI runner, for the
  dynamic lane (Step 8).  Without it, only static ELF-shape assertions are
  possible.

| FreeBSD major-version ABI drift (`ino_t`/`dev_t` widening in FreeBSD 12).
| Medium.
| Target a specific FreeBSD major (e.g. 13/14) and document it; the struct
  layout is version-pinned.

| Self-hosting on FreeBSD (a FreeBSD-native `blaise`) is a further step.
| Low for this task.
| Out of scope here: this design delivers Linux-hosted *cross-compilation* to
  FreeBSD.  A self-hosting FreeBSD stage-1 is a later milestone once the
  cross output is proven.
|===

== Summary

A `freebsd-x86_64` native backend is feasible and the codebase is already
staged for it: the instruction selector, register allocator, System V ABI
lowering, ELF relocation core, and OPDF debug sections are all reused
unchanged.  The entire FreeBSD-specific surface is (1) a FreeBSD `TLinkTarget`
(OSABI, base, page size), (2) a FreeBSD `TPlatformLayout` (`struct stat` layout
and OS constants), and (3) a FreeBSD kernel-stub object (raw syscalls + `_start`
entry stub) that the FreeBSD RTL archive links in place of libc.

The chosen kernel-access strategy is *direct syscalls into a static `ET_EXEC`*
(Strategy B): the linker's static path already supports it, and it is the only
route that delivers the stated goal — cross-compiling a runnable FreeBSD binary
from a Linux host with no external tools and nothing staged.  Its cost is the
libc-replacement leaves (threads, allocator, libc string ops, date math),
which are implemented in that per-target syscall-stub object (a link-time swap,
not a runtime port), done hardest-last.  A conventional libc-linked path
(Strategy A) is documented only as a fallback shape and is not on this
implementation path.

Structurally, FreeBSD is *one new adapter family* — composed by an Abstract
Factory toolkit, resolved from a registry, with zero `{$IFDEF}` and no target
code scattered across units (see `docs/native-target-architecture.adoc`).
The same shape generalises: Windows (PE/COFF + Win64 ABI + `kernel32` imports)
and macOS (Mach-O) become further adapter families behind the same linker core,
container writer behind a Bridge, and Template-Method backend base.
