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:
parent
7af27a7db1
commit
948cd4ed1b
85
compiler/src/main/pascal/runtime.start.static.freebsd.pas
Normal file
85
compiler/src/main/pascal/runtime.start.static.freebsd.pas
Normal 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.
|
||||||
74
compiler/src/main/pascal/runtime.syscall.freebsd.pas
Normal file
74
compiler/src/main/pascal/runtime.syscall.freebsd.pas
Normal 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.
|
||||||
|
|
@ -80,6 +80,11 @@ type
|
||||||
numbers), so this asserts the emitted header shape only — the structural
|
numbers), so this asserts the emitted header shape only — the structural
|
||||||
check the cross-compile lane relies on. }
|
check the cross-compile lane relies on. }
|
||||||
procedure TestLink_FreeBSDTarget_StampsOSABI;
|
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;
|
end;
|
||||||
|
|
||||||
TDynLinkerTests = class(TTestCase)
|
TDynLinkerTests = class(TTestCase)
|
||||||
|
|
@ -1017,6 +1022,74 @@ begin
|
||||||
AssertEquals('EI_OSABI = ELFOSABI_FREEBSD (9)', 9, OrdAt(Bytes, 7));
|
AssertEquals('EI_OSABI = ELFOSABI_FREEBSD (9)', 9, OrdAt(Bytes, 7));
|
||||||
end;
|
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 ---- }
|
{ ---- TDynLinkerTests ---- }
|
||||||
|
|
||||||
function TDynLinkerTests.ProjectRoot: string;
|
function TDynLinkerTests.ProjectRoot: string;
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,19 @@ emulation lane).
|
||||||
|
|
||||||
=== Step 3 — Freestanding `_start` entry stub
|
=== 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`):
|
* The FreeBSD kernel-stub object provides `_start` (not `__libc_start_main`):
|
||||||
read `argc` at `[%rsp]`, `argv` at `[%rsp+8]`, align the stack, call
|
read `argc` at `[%rsp]`, `argv` at `[%rsp+8]`, align the stack, call
|
||||||
`_SetArgs`, run the body, then `exit` via syscall. The linker's static
|
`_SetArgs`, run the body, then `exit` via syscall. The linker's static
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue