From 948cd4ed1b36b61fc0df2ca494999be95fa8295d Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Fri, 26 Jun 2026 23:58:08 +0100 Subject: [PATCH] feat(rtl): FreeBSD freestanding _start + minimal syscall leaf (Step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../pascal/runtime.start.static.freebsd.pas | 85 +++++++++++++++++++ .../main/pascal/runtime.syscall.freebsd.pas | 74 ++++++++++++++++ compiler/src/test/pascal/cp.test.linker.pas | 73 ++++++++++++++++ docs/freebsd-x86_64-backend-design.adoc | 13 +++ 4 files changed, 245 insertions(+) create mode 100644 compiler/src/main/pascal/runtime.start.static.freebsd.pas create mode 100644 compiler/src/main/pascal/runtime.syscall.freebsd.pas diff --git a/compiler/src/main/pascal/runtime.start.static.freebsd.pas b/compiler/src/main/pascal/runtime.start.static.freebsd.pas new file mode 100644 index 0000000..00e2700 --- /dev/null +++ b/compiler/src/main/pascal/runtime.start.static.freebsd.pas @@ -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. diff --git a/compiler/src/main/pascal/runtime.syscall.freebsd.pas b/compiler/src/main/pascal/runtime.syscall.freebsd.pas new file mode 100644 index 0000000..93eff32 --- /dev/null +++ b/compiler/src/main/pascal/runtime.syscall.freebsd.pas @@ -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. diff --git a/compiler/src/test/pascal/cp.test.linker.pas b/compiler/src/test/pascal/cp.test.linker.pas index fb41320..8c052e2 100644 --- a/compiler/src/test/pascal/cp.test.linker.pas +++ b/compiler/src/test/pascal/cp.test.linker.pas @@ -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; diff --git a/docs/freebsd-x86_64-backend-design.adoc b/docs/freebsd-x86_64-backend-design.adoc index b1baa22..b12a233 100644 --- a/docs/freebsd-x86_64-backend-design.adoc +++ b/docs/freebsd-x86_64-backend-design.adoc @@ -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