blaise/docs/freebsd-x86_64-backend-design.adoc
Graeme Geldenhuys 821ce90caf docs: RTL unification plan — one binary, embedded source, no external .a
Records the runtime end-state: blaise embeds the RTL Pascal source once and
compiles it per --target on demand (selecting that target's TPlatformLayout +
kernel-stub adapter set), linking in-process — so a single binary cross-compiles
to any registered target with no blaise_rtl.a, no ar, no make install, and no
per-target .a files. The archive is documented as the transitional mechanism.

Adds an RTL-unification track to the migration sequence (gated on the .s→inline-
asm migration, which is in progress: blaise_atomic + blaise_setjmp done,
blaise_start + blaise_utf8 remaining). Compile-per-target adopted first;
in-memory/unit-cache RTL object cache deferred until build time is measured.

Reconciles the 'two selection moments' and FreeBSD Step 5 with the no-archive
end-state.
2026-06-25 23:08:30 +01:00

506 lines
25 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

= 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
`runtime/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)` and relies on the CRT's `_start` →
`__libc_start_main(main, ...)` to drive it. The internal linker
(`LinkViaInternalLinker` in `blaise.codegen.native.driver.pas`) links:
* the host CRT objects discovered by `FindCrtObjects` in `uToolchain.pas`
(`Scrt1.o`, `crti.o`, `crtbeginS.o`, `crtendS.o`, `crtn.o`), searched
under `/usr/lib/x86_64-linux-gnu`, `/usr/lib/gcc/...`;
* the program object and `blaise_rtl.a`;
* in dynamic mode, against `libc.so.6` via the interpreter
`/lib64/ld-linux-x86-64.so.2` (both strings hard-coded in
`TLinker.BuildDynamic`).
`__libc_start_main`, `Scrt1.o`, `libc.so.6`, and the glibc loader path are
glibc-specific. None exist on FreeBSD, whose libc startup convention,
CRT objects (`crt1.o`), and run-time linker (`/libexec/ld-elf.so.1`)
differ.
=== Toolchain discovery is Linux-pathed
`uToolchain.pas`:
* `FindCrtObjects` searches Linux library directories only.
* `FindRTLArchive`/`ResolveToolchain` locate one `blaise_rtl.a` — built for
the host. A FreeBSD binary needs a FreeBSD-built `blaise_rtl.a`.
* `TLinker` is always constructed via `LinuxX86_64Target()`; the driver
never selects a FreeBSD `TLinkTarget`.
[#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_*` (`blaise_thread.pas`)
! Threading. Reimplementing on raw `thr_new`/`_umtx_op` FreeBSD syscalls is
real work, not a thin stub.
! `mmap/munmap/mremap` (`blaise_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`, `blaise_setjmp_x86_64.s`)
! Exception unwinding. Already hand-written assembly — 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 0a0b 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
19 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
* 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
* Add `TLayoutFreeBSDX86_64`: the FreeBSD `struct stat` field offsets (note
FreeBSD 12+ widened `ino_t`/`dev_t`; pin to a target major version),
`struct tm` layout, and the FreeBSD `O_*` / `S_IFDIR` / `CLOCK_REALTIME` /
`WNOHANG` / `SEEK_*` constant values.
* *Verify:* a FreeBSD `fstat`-based `FileExists`/`FileAge`/`DirectoryExists`
returns correct results under emulation (Step 9) — static checks cannot
catch a wrong struct offset.
=== Step 3 — Freestanding `_start` entry stub
* 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 (a small `.s` stub like
`blaise_setjmp_x86_64.s`, or compiler-emitted): 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.
* *Transitional* (matches today's archive model): build a FreeBSD
`blaise_rtl-freebsd-x86_64.a` (RTL compiled with `--target freebsd-x86_64`,
composing the FreeBSD adapter set) and have `FindRTLArchive` select the
archive matching the *target*, not the host.
* *End-state* (per the RTL-unification track in
`docs/native-target-architecture.adoc`): no per-target `.a` — the compiler
compiles the embedded RTL source for `freebsd-x86_64` in-process and links it.
Implement whichever is current when this step is reached; the FreeBSD adapter
set is the same either way.
* *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` currently always builds `LinuxX86_64Target` and
calls `FindCrtObjects` unconditionally. Drive it from the resolved toolkit:
select the FreeBSD `TLinkTarget`, and for the Strategy-B static path skip
CRT discovery and dynamic mode entirely (static `_start`, no interp).
* *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.