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
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue