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.
This commit is contained in:
Graeme Geldenhuys 2026-06-26 23:58:08 +01:00
parent 7af27a7db1
commit 948cd4ed1b
4 changed files with 245 additions and 0 deletions

View file

@ -0,0 +1,85 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit runtime.start.static.freebsd;
// Freestanding _start for a static, libc-free FreeBSD ET_EXEC (the FreeBSD
// kernel-leaf swap; docs/freebsd-x86_64-backend-design.adoc, Step 3).
//
// The FreeBSD sibling of runtime.start.static.linux. A tiny asm trampoline
// captures the initial stack pointer and tail-calls the Pascal _BlaiseStartC,
// which parses argc / argv / envp off the kernel's initial stack, captures
// `environ`, calls main(argc, argv), and exits via the FreeBSD `exit` syscall.
//
// FreeBSD's process entry stack has the SAME shape as Linux: %rsp points at
// argc, followed by argv[0..argc-1], a NULL, envp[], a NULL, then the ELF auxv.
// (FreeBSD additionally passes an rtld cleanup pointer in %rdx for dynamic
// binaries; it is 0 for a static ET_EXEC and we ignore it.)
//
// Step 3 is deliberately minimal: it does NOT set up TLS / the thread pointer.
// A trivial program performs no threadvar (%fs-relative) access, so this start
// runs hello-world and file I/O. TLS-block construction + the auxv PT_TLS walk
// (whose FreeBSD AT_* tags differ from Linux) arrive with the threads leaf in
// Step 4, mirroring how runtime.start.static.linux gained TLS alongside its
// threads work.
interface
uses
runtime.syscall.freebsd; { _exit + the `environ` global }
procedure _start;
implementation
type
PPointer = ^Pointer;
PInt64 = ^Int64;
{ Call the program's `main(argc, argv)` (emitted by the backend) and return its
result. The asm thunk tail-jumps to the bare `main` symbol; argc/argv are
already in %edi/%rsi (SysV), exactly what main expects. }
function MainTrampoline(Argc: Integer; Argv: Pointer): Integer;
assembler; nostackframe;
asm
jmp main
end;
{ The C-level entry: SP points at the kernel's initial stack (argc at [SP]). }
procedure _BlaiseStartC(SP: Pointer);
var
Argc: Int64;
Argv, Envp: Pointer;
Ret: Integer;
begin
Argc := PInt64(SP)^;
Argv := Pointer(PChar(SP) + 8);
{ envp = &argv[argc+1] }
Envp := Pointer(PChar(Argv) + (Argc + 1) * 8);
environ := Envp;
Ret := MainTrampoline(Integer(Argc), Argv);
_exit(Ret);
end;
{ The kernel entry. Capture %rsp (points at argc), align, and call the Pascal
core. Never returns. }
procedure _start; assembler; nostackframe;
asm
endbr64
xor %ebp, %ebp
movq %rsp, %rdi { SP -> first arg }
andq $0xfffffffffffffff0, %rsp
call _BlaiseStartC
xorl %edi, %edi
movq $1, %rax { SYS_exit(0) guard }
syscall
hlt
end;
end.

View file

@ -0,0 +1,74 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit runtime.syscall.freebsd;
// FreeBSD x86_64 direct-syscall kernel leaf — the libc replacement for a static,
// libc-free FreeBSD ET_EXEC (docs/freebsd-x86_64-backend-design.adoc, Step 3/4).
//
// This is the FreeBSD sibling of runtime.syscall.linux. It DEFINES the bare
// POSIX names that rtl.platform.posix references via `external name 'write'`
// etc.; the FreeBSD RTL composition links it (the link-time swap) in place of
// the Linux leaf, so the resulting binary reaches the kernel with raw
// `syscall`s and needs no libc, no PT_INTERP.
//
// Step 3 scope: the minimum the freestanding _start needs — `_exit`, `write`,
// and the `environ` global. The remaining file/process/thread leaves grow here
// in Step 4.
//
// System V AMD64 -> FreeBSD syscall ABI:
// * syscall number in %rax; args in %rdi,%rsi,%rdx,%r10,%r8,%r9 (the 4th C
// arg %rcx must be moved to %r10 — the `syscall` instruction clobbers %rcx).
// * The syscall NUMBERS differ from Linux (exit=1, write=4, read=3, open=5,
// close=6 — see sys/syscall.h); they are NOT Linux's.
// * Errors are reported via the CARRY FLAG (errno returned in %rax), not the
// -errno convention Linux uses. The error-translating wrappers that need it
// arrive in Step 4; `_exit` never returns and `write` on success returns the
// byte count, so neither needs CF handling here.
//
// `nostackframe` lets each asm body own the frame so the registers hold the raw
// incoming arguments unmodified.
interface
{ The current process environment (envp), captured by _start. rtl.platform.posix
/ getenv walk this NULL-terminated array. Defined here (as on Linux) so the
kernel leaf owns it. }
var
environ: Pointer;
{ write(fd, buf, count) — SYS_write = 4. The Pascal name is _sys_write because
`write` collides with the Write/WriteLn builtin; the asm body emits a bare
`write` label so the linker symbol matches posix's `external name 'write'`. }
function _sys_write(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
{ _exit(code) — SYS_exit = 1. Terminates the process; never returns. The
unit-level routine name `_exit` already emits the unmangled `_exit` symbol that
satisfies posix's `external name '_exit'`. }
procedure _exit(Code: Integer);
implementation
function _sys_write(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
assembler; nostackframe;
asm
.globl write
write:
movq $4, %rax { SYS_write }
syscall
ret
end;
procedure _exit(Code: Integer); assembler; nostackframe;
asm
movq $1, %rax { SYS_exit }
syscall
ret
end;
end.

View file

@ -80,6 +80,11 @@ type
numbers), so this asserts the emitted header shape only — the structural
check the cross-compile lane relies on. }
procedure TestLink_FreeBSDTarget_StampsOSABI;
{ Step 3: linking the freestanding FreeBSD _start with the FreeBSD target
yields a static ET_EXEC whose entry point is _start and which has no
PT_INTERP — the Strategy-B shape a cross-compiled FreeBSD binary needs.
Asserts header shape only (it cannot run on the Linux host). }
procedure TestLink_FreeBSDStart_StaticExecShape;
end;
TDynLinkerTests = class(TTestCase)
@ -1017,6 +1022,74 @@ begin
AssertEquals('EI_OSABI = ELFOSABI_FREEBSD (9)', 9, OrdAt(Bytes, 7));
end;
procedure TLinkerE2ETests.TestLink_FreeBSDStart_StaticExecShape;
const
{ A self-contained FreeBSD _start: capture the stack, then exit(0) via
SYS_exit = 1. Mirrors runtime.start.static.freebsd's entry shape without
pulling in main/_SetArgs, so it links with no undefined symbols. }
FixtureAsm =
'.text' + LineEnding +
'.globl _start' + LineEnding +
'_start:' + LineEnding +
' endbr64' + LineEnding +
' xorl %ebp, %ebp' + LineEnding +
' movq %rsp, %rdi' + LineEnding +
' xorl %edi, %edi' + LineEnding +
' movq $1, %rax' + LineEnding + { SYS_exit }
' syscall' + LineEnding +
' hlt' + LineEnding;
var
Lk: TLinker;
Obj: TElfObjectFile;
Bytes: string;
Entry, StartAddr: Int64;
PhOff, PhEntSz, PhCount, J, PType: Integer;
FoundInterp: Boolean;
begin
{ LinkToBytes returns the binary in memory — ReadFile would truncate at the
first NUL, which an ELF header hits at byte 8. }
Lk := TLinker.Create(FreeBSDX86_64Target());
try
Obj := ParseElfObject(AssembleToBytes(FixtureAsm), 'fbstart.o');
Lk.AddOwnedObject(Obj);
Bytes := Lk.LinkToBytes('_start');
StartAddr := Lk.AddrOfSymbol('_start');
finally
Lk.Free();
end;
AssertTrue('output too small to be an ELF', Length(Bytes) >= 64);
{ e_type at offset 16 must be ET_EXEC (2) — a static, non-PIE executable. }
AssertEquals('e_type ET_EXEC', 2, OrdAt(Bytes, 16) or (OrdAt(Bytes, 17) shl 8));
{ EI_OSABI byte 7 = ELFOSABI_FREEBSD. }
AssertEquals('EI_OSABI FreeBSD', 9, OrdAt(Bytes, 7));
{ e_entry (offset 24, 8 bytes LE) must equal the address of _start. }
Entry := Int64(OrdAt(Bytes, 24)) or (Int64(OrdAt(Bytes, 25)) shl 8) or
(Int64(OrdAt(Bytes, 26)) shl 16) or (Int64(OrdAt(Bytes, 27)) shl 24) or
(Int64(OrdAt(Bytes, 28)) shl 32) or (Int64(OrdAt(Bytes, 29)) shl 40) or
(Int64(OrdAt(Bytes, 30)) shl 48) or (Int64(OrdAt(Bytes, 31)) shl 56);
AssertEquals('entry point is _start', StartAddr, Entry);
{ No PT_INTERP (type 3) among the program headers — a freestanding binary has
no dynamic loader. }
PhOff := OrdAt(Bytes, 32) or (OrdAt(Bytes, 33) shl 8) or
(OrdAt(Bytes, 34) shl 16) or (OrdAt(Bytes, 35) shl 24);
PhEntSz := OrdAt(Bytes, 54) or (OrdAt(Bytes, 55) shl 8);
PhCount := OrdAt(Bytes, 56) or (OrdAt(Bytes, 57) shl 8);
FoundInterp := False;
for J := 0 to PhCount - 1 do
begin
PType := OrdAt(Bytes, PhOff + J * PhEntSz) or
(OrdAt(Bytes, PhOff + J * PhEntSz + 1) shl 8) or
(OrdAt(Bytes, PhOff + J * PhEntSz + 2) shl 16) or
(OrdAt(Bytes, PhOff + J * PhEntSz + 3) shl 24);
if PType = 3 then FoundInterp := True;
end;
AssertTrue('static ET_EXEC must have no PT_INTERP', not FoundInterp);
end;
{ ---- TDynLinkerTests ---- }
function TDynLinkerTests.ProjectRoot: string;

View file

@ -368,6 +368,19 @@ emulation lane).
=== 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