2026-06-24 20:51:40 +03:00
|
|
|
|
= 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
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
`compiler/src/main/pascal/rtl.platform.posix.pas` is *pure Pascal* but is not
|
2026-06-24 20:51:40 +03:00
|
|
|
|
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
|
2026-06-27 01:48:14 +03:00
|
|
|
|
`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
|
2026-06-24 20:51:40 +03:00
|
|
|
|
`/lib64/ld-linux-x86-64.so.2` (both strings hard-coded in
|
2026-06-27 01:48:14 +03:00
|
|
|
|
`TLinker.BuildDynamic`) — so `__libc_start_main` is still resolved from
|
|
|
|
|
|
glibc on the Linux target.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
`__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).
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
=== Toolchain discovery is Linux-pathed
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
`uToolchain.pas` and the driver:
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
* `FindCrtObjects` searches Linux library directories only.
|
2026-06-27 01:48:14 +03:00
|
|
|
|
* 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).
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
[#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
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
! `pthread_create/join/mutex_*` (`runtime.thread.pas`)
|
2026-06-24 20:51:40 +03:00
|
|
|
|
! Threading. Reimplementing on raw `thr_new`/`_umtx_op` FreeBSD syscalls is
|
|
|
|
|
|
real work, not a thin stub.
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
! `mmap/munmap/mremap` (`runtime.mem.pas`)
|
2026-06-24 20:51:40 +03:00
|
|
|
|
! 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.
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
! `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.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
! `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
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
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.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
== Implementation plan
|
|
|
|
|
|
|
|
|
|
|
|
Each step states a verification check, per the project's goal-driven
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
convention. Steps 0a–0b are the behaviour-preserving structural refactor from
|
2026-06-24 20:51:40 +03:00
|
|
|
|
`docs/native-target-architecture.adoc` (no FreeBSD code, no behaviour change —
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
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.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
=== 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).
|
|
|
|
|
|
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
=== Step 0c — Kernel leaf: link-time swap, not a runtime port (revised)
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
* *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.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
=== Step 1 — FreeBSD link target + toolkit skeleton
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
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`.
|
|
|
|
|
|
|
2026-06-24 20:51:40 +03:00
|
|
|
|
* 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
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
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.)
|
2026-06-24 20:51:40 +03:00
|
|
|
|
* *Verify:* a FreeBSD `fstat`-based `FileExists`/`FileAge`/`DirectoryExists`
|
2026-06-27 01:48:14 +03:00
|
|
|
|
returns correct results under emulation (Step 8) — static checks cannot
|
2026-06-24 20:51:40 +03:00
|
|
|
|
catch a wrong struct offset.
|
|
|
|
|
|
|
|
|
|
|
|
=== Step 3 — Freestanding `_start` entry stub
|
|
|
|
|
|
|
feat(rtl): FreeBSD freestanding _start + minimal syscall leaf (Step 3)
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.
2026-06-27 01:58:08 +03:00
|
|
|
|
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`.
|
|
|
|
|
|
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
* 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.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
* *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)
|
|
|
|
|
|
|
2026-07-01 11:52:27 +03:00
|
|
|
|
NOTE: Done (4a/4b/4c). 4a: the file/process leaves grew in
|
|
|
|
|
|
`runtime.syscall.freebsd` with the FreeBSD ino64 syscall numbers and the
|
|
|
|
|
|
carry-flag -> -errno translation. 4b: `getrandom` + an `mremap`-failure stub
|
|
|
|
|
|
(the allocator already falls back to alloc-copy-free), and
|
|
|
|
|
|
`runtime.libc2.freebsd` (atexit/exit/abort/date-math/mkstemp/system) — with
|
|
|
|
|
|
`memcpy/memset/memcmp/strlen` and the x86-64 `jmp_buf` reused unchanged as
|
|
|
|
|
|
target-invariant. 4c: `thr_new`/`_umtx_op`/`thr_exit`/`sysarch` leaves, the
|
|
|
|
|
|
FreeBSD `%fs`-base TLS setup in `runtime.start.static.freebsd`, and the new
|
|
|
|
|
|
`runtime.thread.static.freebsd` (pthread_* on thr_new + _umtx_op). All units
|
|
|
|
|
|
are standalone (nothing in the Linux build graph uses them), so the Linux
|
|
|
|
|
|
self-host is untouched; verified via the four fixpoints, the full suite, and the
|
|
|
|
|
|
`cp.test.linker` FreeBSD fixtures. End-to-end execution under emulation is
|
|
|
|
|
|
Step 8.
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
* 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
|
2026-06-24 20:51:40 +03:00
|
|
|
|
`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).
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
* 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).
|
2026-06-24 20:51:40 +03:00
|
|
|
|
* *Verify:* a FreeBSD hello-world + file-I/O smoke test produce correct output
|
|
|
|
|
|
under emulation.
|
|
|
|
|
|
|
2026-06-25 13:49:23 +03:00
|
|
|
|
=== 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.
|
2026-06-27 01:48:14 +03:00
|
|
|
|
* 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.
|
2026-06-25 13:49:23 +03:00
|
|
|
|
* *Verify:* a `--target freebsd-x86_64` program links the FreeBSD layout +
|
|
|
|
|
|
syscall-stub and contains no Linux/libc RTL code.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
=== Step 6 — Drive target selection through the linker
|
|
|
|
|
|
|
2026-06-27 01:48:14 +03:00
|
|
|
|
* `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`).
|
2026-06-24 20:51:40 +03:00
|
|
|
|
* *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`).
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
Building the FreeBSD direct-syscall leaf as a clean, link-time-swapped
|
|
|
|
|
|
syscall-stub object (Step 4) is the template every later OS follows.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
* *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`
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
(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.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
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),
|
docs: kernel leaf is a link-time swap, not a runtime TKernelABI port
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
2026-06-25 03:02:35 +03:00
|
|
|
|
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.
|
2026-06-24 20:51:40 +03:00
|
|
|
|
|
|
|
|
|
|
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.
|