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.
506 lines
25 KiB
Plaintext
506 lines
25 KiB
Plaintext
= 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 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
|
||
|
||
* 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.
|