Add the FreeBSD entry stub for a static, libc-free ET_EXEC: - runtime.start.static.freebsd.pas — the FreeBSD sibling of runtime.start.static.linux. _start captures %rsp, aligns the stack, and calls the Pascal _BlaiseStartC, which parses argc/argv/envp off the kernel's initial stack (same layout as Linux), captures environ, calls main(argc, argv), and exits via the FreeBSD exit syscall. Deliberately minimal: no TLS/auxv-walk (a trivial program does no threadvar access) — that lands with the threads work in Step 4, mirroring how the Linux static start gained TLS. - runtime.syscall.freebsd.pas — the minimum kernel leaf _start needs: _exit (SYS_exit = 1), write (SYS_write = 4, aliased from _sys_write), and the environ global. The full file/process/thread leaf grows here in Step 4. ABI notes record the FreeBSD differences from Linux: different syscall numbers and the carry-flag error convention (only relevant for the error-translating wrappers that arrive in Step 4). Both units are standalone — nothing in the default Linux build graph uses them, so the Linux RTL and self-hosting are unaffected; the FreeBSD RTL composition links them at Step 5. TLinkerE2ETests.TestLink_FreeBSDStart_StaticExecShape links the _start fixture with the FreeBSD target and asserts the Strategy-B shape: e_type = ET_EXEC, entry == _start, no PT_INTERP. (Uses LinkToBytes, not ReadFile — the latter truncates at the ELF header's first NUL byte.) Step 3 of docs/freebsd-x86_64-backend-design.adoc. Full suite 3850 green; FIXPOINT_OK.
552 lines
28 KiB
Plaintext
552 lines
28 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
|
||
|
||
`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.
|