feat(rtl): FreeBSD syscall leaf — file/process leaves (Step 4a)

Grow runtime.syscall.freebsd.pas from the Step-3 minimum (_exit/write) to
the file/process syscall leaves, mirroring runtime.syscall.linux but with
FreeBSD syscall numbers and FreeBSD's carry-flag error convention.

Added leaves: open close read lseek fstat stat mkdir rmdir unlink rename
chdir chmod getpid dup2 pipe mmap munmap nanosleep clock_gettime fork
execve wait4 kill getcwd.  Each emits the raw `syscall` (number in %rax,
args rdi/rsi/rdx/r10/r8/r9, %rcx->%r10 for >=4 args) and translates
FreeBSD's error convention (CF set, positive errno in %rax) into the
-errno the Pascal call sites in rtl.platform.posix expect:

    movq $N, %rax ; syscall ; jae .Lok ; negq %rax ; .Lok: ret

Syscall numbers are the FreeBSD 14 ino64 ABI (mmap=477 lseek=478
fstat=551, NOT the freebsd6/11 compat 197/199/189).  FreeBSD's ino64 ABI
has no plain `stat` syscall, so stat(path,buf) is a wrapper over
fstatat(AT_FDCWD,path,buf,0)=552; pipe(fds) wraps pipe2(fds,0)=542.  All
numbers verified against release/14.0.0 sys/sys/syscall.h.

The unit is standalone (only runtime.start.static.freebsd references it,
itself outside the Linux build graph), so the Linux self-host is
untouched.  Test TestLink_FreeBSDSyscallLeaf_SymbolsAndNumbers links a
fixture and asserts the bare POSIX symbols are defined and the FreeBSD
syscall-number bytes + negq (CF xlate) + mov %rcx,%r10 are present.
Scope: file/process only; memcpy/date/allocator/threads are Step 4b/4c.
This commit is contained in:
Graeme Geldenhuys 2026-07-01 02:15:01 +01:00
parent 77197739de
commit 2662535579
2 changed files with 517 additions and 20 deletions

View file

@ -17,43 +17,170 @@ unit runtime.syscall.freebsd;
// 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.
// Step 4a scope: the file/process syscall leaves (open/close/read/write/lseek,
// stat/fstat/mkdir/rmdir/unlink/rename/chdir/chmod, getpid/dup2/pipe/fork/
// execve/wait4/kill/getcwd, mmap/munmap/nanosleep/clock_gettime), plus the
// pre-existing `_exit`, `_sys_write` and `environ`. The harder leaves
// (memcpy/memset, date math, the mmap allocator, threads) grow here in Steps
// 4b/4c.
//
// 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.
// close=6 see sys/syscall.h); they are NOT Linux's. FreeBSD 12 renumbered
// stat/fstat/lseek/mmap for the 64-bit-inode ("ino64") ABI; this leaf uses
// the ino64 numbers to match the FreeBSD 14 224-byte `struct stat` that the
// rtl.platform.layout.freebsd adapter expects.
// * ERROR CONVENTION the key FreeBSD difference from Linux. FreeBSD reports
// errors via the CARRY FLAG: on error CF is set and %rax holds a POSITIVE
// errno; on success CF is clear and %rax is the result. The Pascal call
// sites in rtl.platform.posix expect the Linux -errno convention (they test
// `if Fd < 0`, `< 0`, `<> 0`). So every leaf that can fail translates:
// after `syscall`, `jae` past a `negq %rax` when CF is clear (success
// leave %rax untouched); when CF is set, `negq %rax` turns the positive
// errno into -errno. Each body uses a UNIQUE local label (.Lok_xxx) so the
// forward-local jumps do not collide when the unit assembles to one object.
//
// `nostackframe` lets each asm body own the frame so the registers hold the raw
// incoming arguments unmodified.
interface
{ File descriptors. }
function open(Path: PChar; Flags: Integer; Mode: Integer): Integer;
function close(Fd: Integer): Integer;
function read(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
{ `write` collides with the Write/WriteLn builtin as a Pascal identifier, so the
Pascal name is _sys_write; the asm body additionally emits a bare `write`
label (.globl) so the linker symbol matches rtl.platform.posix's
`external name 'write'`. Every other leaf's name is a valid Pascal identifier
and needs no alias. }
function _sys_write(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
function lseek(Fd: Integer; Offset: Int64; Whence: Integer): Int64;
{ Filesystem. }
function fstat(Fd: Integer; Buf: Pointer): Integer;
{ FreeBSD's ino64 ABI has NO plain `stat` syscall it is expressed as
fstatat(AT_FDCWD, path, buf, 0). This wrapper sets up that call so the bare
`stat` symbol posix imports still resolves. }
function stat(Path: PChar; Buf: Pointer): Integer;
function mkdir(Path: PChar; Mode: Integer): Integer;
function rmdir(Path: PChar): Integer;
function unlink(Path: PChar): Integer;
function rename(OldPath, NewPath: PChar): Integer;
function chdir(Path: PChar): Integer;
function chmod(Path: PChar; Mode: Integer): Integer;
{ Process. }
function getpid: Integer;
function dup2(OldFd, NewFd: Integer): Integer;
{ FreeBSD deprecated the 2-arg pipe(2) (legacy 42); this calls pipe2(fds, 0). }
function pipe(Fds: Pointer): Integer;
{ Memory. }
function mmap(Addr: Pointer; Length: Int64; Prot, Flags, Fd: Integer;
Offset: Int64): Pointer;
function munmap(Addr: Pointer; Length: Int64): Integer;
{ Time. }
function nanosleep(Req, Rem: Pointer): Integer;
function clock_gettime(ClockId: Integer; Ts: Pointer): Integer;
{ Process control + raw primitives the higher-level wrappers build on. }
function fork: Integer;
function execve(Path: PChar; Argv, Envp: Pointer): Integer;
function wait4(Pid: Integer; Status: Pointer; Options: Integer;
Rusage: Pointer): Integer;
function kill(Pid, Sig: Integer): Integer;
{ Raw __getcwd(2): fills Buf (up to Size); returns 0 on success or -errno. The
wrapper in the posix layer adapts that to libc's buffer-pointer contract. }
function sys_getcwd(Buf: PChar; Size: Int64): Int64;
{ _exit(2) 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);
{ 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
{ ---- FreeBSD x86_64 syscall numbers (FreeBSD 14 sys/sys/syscall.h). ----
Verified against
https://raw.githubusercontent.com/freebsd/freebsd-src/release/14.0.0/sys/sys/syscall.h
ino64 variants used where FreeBSD 12 renumbered (fstat=551, lseek=478,
mmap=477; stat has no ino64 syscall expressed via fstatat=552). }
const
SYS_exit = 1;
SYS_fork = 2;
SYS_read = 3;
SYS_write = 4;
SYS_open = 5;
SYS_close = 6;
SYS_wait4 = 7;
SYS_unlink = 10;
SYS_chdir = 12;
SYS_chmod = 15;
SYS_getpid = 20;
SYS_kill = 37;
SYS_munmap = 73;
SYS_dup2 = 90;
SYS_rename = 128;
SYS_mkdir = 136;
SYS_rmdir = 137;
SYS_getcwd = 326; { __getcwd }
SYS_clock_gettime = 232;
SYS_nanosleep = 240;
SYS_mmap = 477; { ino64 (legacy freebsd6 mmap = 197) }
SYS_lseek = 478; { ino64 (legacy lseek = 199) }
SYS_fstat = 551; { ino64 (legacy freebsd11_fstat = 189) }
SYS_fstatat = 552; { ino64 (legacy freebsd11_fstatat = 493) }
SYS_pipe2 = 542; { pipe(2) legacy 42 deprecated }
SYS_execve = 59;
AT_FDCWD = -100; { dirfd for path-relative *at() syscalls at cwd }
{ The asm bodies use literal immediates (the assembler needs a literal, not a
symbol); the const block above documents the number-to-name mapping. }
function open(Path: PChar; Flags: Integer; Mode: Integer): Integer;
assembler; nostackframe;
asm
movq $5, %rax { SYS_open }
syscall
jae .Lok_open { CF clear = success }
negq %rax { CF set = error: errno -> -errno }
.Lok_open:
ret
end;
function close(Fd: Integer): Integer;
assembler; nostackframe;
asm
movq $6, %rax { SYS_close }
syscall
jae .Lok_close
negq %rax
.Lok_close:
ret
end;
function read(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
assembler; nostackframe;
asm
movq $3, %rax { SYS_read }
syscall
jae .Lok_read
negq %rax
.Lok_read:
ret
end;
{ write CAN fail; posix's write-loop expects a negative return on error, so it
gets CF-translation too. }
function _sys_write(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
assembler; nostackframe;
asm
@ -61,9 +188,260 @@ asm
write:
movq $4, %rax { SYS_write }
syscall
jae .Lok_write
negq %rax
.Lok_write:
ret
end;
function lseek(Fd: Integer; Offset: Int64; Whence: Integer): Int64;
assembler; nostackframe;
asm
movq $478, %rax { SYS_lseek (ino64) }
syscall
jae .Lok_lseek
negq %rax
.Lok_lseek:
ret
end;
function fstat(Fd: Integer; Buf: Pointer): Integer;
assembler; nostackframe;
asm
movq $551, %rax { SYS_fstat (ino64) }
syscall
jae .Lok_fstat
negq %rax
.Lok_fstat:
ret
end;
{ stat(path, buf) -> fstatat(AT_FDCWD, path, buf, 0). Incoming: %rdi=Path,
%rsi=Buf. Shift to the fstatat argument layout: dirfd(%rdi)=AT_FDCWD,
path(%rsi)=Path, buf(%rdx)=Buf, flag(%r10)=0. arg4 goes straight to %r10
(the syscall's 4th kernel register); no %rcx move is needed since we build
the arguments here rather than receive four C args. }
function stat(Path: PChar; Buf: Pointer): Integer;
assembler; nostackframe;
asm
movq %rsi, %rdx { buf -> arg3 }
movq %rdi, %rsi { path -> arg2 }
movq $-100, %rdi { AT_FDCWD -> arg1 (dirfd) }
xorq %r10, %r10 { flag = 0 -> arg4 }
movq $552, %rax { SYS_fstatat (ino64) }
syscall
jae .Lok_stat
negq %rax
.Lok_stat:
ret
end;
function mkdir(Path: PChar; Mode: Integer): Integer;
assembler; nostackframe;
asm
movq $136, %rax { SYS_mkdir }
syscall
jae .Lok_mkdir
negq %rax
.Lok_mkdir:
ret
end;
function chmod(Path: PChar; Mode: Integer): Integer;
assembler; nostackframe;
asm
movq $15, %rax { SYS_chmod }
syscall
jae .Lok_chmod
negq %rax
.Lok_chmod:
ret
end;
function rmdir(Path: PChar): Integer;
assembler; nostackframe;
asm
movq $137, %rax { SYS_rmdir }
syscall
jae .Lok_rmdir
negq %rax
.Lok_rmdir:
ret
end;
function unlink(Path: PChar): Integer;
assembler; nostackframe;
asm
movq $10, %rax { SYS_unlink }
syscall
jae .Lok_unlink
negq %rax
.Lok_unlink:
ret
end;
function rename(OldPath, NewPath: PChar): Integer;
assembler; nostackframe;
asm
movq $128, %rax { SYS_rename }
syscall
jae .Lok_rename
negq %rax
.Lok_rename:
ret
end;
function chdir(Path: PChar): Integer;
assembler; nostackframe;
asm
movq $12, %rax { SYS_chdir }
syscall
jae .Lok_chdir
negq %rax
.Lok_chdir:
ret
end;
function getpid: Integer;
assembler; nostackframe;
asm
movq $20, %rax { SYS_getpid }
syscall
ret { getpid never fails; no CF translation }
end;
function dup2(OldFd, NewFd: Integer): Integer;
assembler; nostackframe;
asm
movq $90, %rax { SYS_dup2 }
syscall
jae .Lok_dup2
negq %rax
.Lok_dup2:
ret
end;
{ pipe(fds) -> pipe2(fds, 0). Incoming: %rdi=Fds; add flags=0 in %rsi. }
function pipe(Fds: Pointer): Integer;
assembler; nostackframe;
asm
xorq %rsi, %rsi { flags = 0 }
movq $542, %rax { SYS_pipe2 }
syscall
jae .Lok_pipe
negq %rax
.Lok_pipe:
ret
end;
{ mmap has 6 args; arg4 (Flags) arrives in %rcx (C ABI) but the kernel wants it
in %r10. Move it before the syscall. }
function mmap(Addr: Pointer; Length: Int64; Prot, Flags, Fd: Integer;
Offset: Int64): Pointer;
assembler; nostackframe;
asm
movq %rcx, %r10
movq $477, %rax { SYS_mmap (ino64) }
syscall
jae .Lok_mmap
negq %rax
.Lok_mmap:
ret
end;
function munmap(Addr: Pointer; Length: Int64): Integer;
assembler; nostackframe;
asm
movq $73, %rax { SYS_munmap }
syscall
jae .Lok_munmap
negq %rax
.Lok_munmap:
ret
end;
function nanosleep(Req, Rem: Pointer): Integer;
assembler; nostackframe;
asm
movq $240, %rax { SYS_nanosleep }
syscall
jae .Lok_nanosleep
negq %rax
.Lok_nanosleep:
ret
end;
function clock_gettime(ClockId: Integer; Ts: Pointer): Integer;
assembler; nostackframe;
asm
movq $232, %rax { SYS_clock_gettime }
syscall
jae .Lok_clock_gettime
negq %rax
.Lok_clock_gettime:
ret
end;
function fork: Integer;
assembler; nostackframe;
asm
movq $2, %rax { SYS_fork }
syscall
jae .Lok_fork
negq %rax
.Lok_fork:
ret
end;
function execve(Path: PChar; Argv, Envp: Pointer): Integer;
assembler; nostackframe;
asm
movq $59, %rax { SYS_execve }
syscall
jae .Lok_execve
negq %rax
.Lok_execve:
ret
end;
{ wait4 has 4 args; arg4 (Rusage) arrives in %rcx -> move to %r10. }
function wait4(Pid: Integer; Status: Pointer; Options: Integer;
Rusage: Pointer): Integer;
assembler; nostackframe;
asm
movq %rcx, %r10
movq $7, %rax { SYS_wait4 }
syscall
jae .Lok_wait4
negq %rax
.Lok_wait4:
ret
end;
function kill(Pid, Sig: Integer): Integer;
assembler; nostackframe;
asm
movq $37, %rax { SYS_kill }
syscall
jae .Lok_kill
negq %rax
.Lok_kill:
ret
end;
function sys_getcwd(Buf: PChar; Size: Int64): Int64;
assembler; nostackframe;
asm
movq $326, %rax { SYS___getcwd }
syscall
jae .Lok_getcwd
negq %rax
.Lok_getcwd:
ret
end;
{ _exit(code) SYS_exit = 1. Terminates the process; never returns, so no
CF translation is needed. }
procedure _exit(Code: Integer); assembler; nostackframe;
asm
movq $1, %rax { SYS_exit }

View file

@ -85,6 +85,13 @@ type
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;
{ Step 4a: a fixture that mirrors the runtime.syscall.freebsd file/process
leaves (open/read/write/mmap/stat) links under the FreeBSD target with the
bare POSIX symbols DEFINED, and the linked .text carries the FreeBSD
ino64 syscall NUMBERS (open=5, write=4, mmap=477, fstatat=552) plus the
CF-error-translation idiom (jae + negq). Standalone no FreeBSD
emulation, header/byte shape only. }
procedure TestLink_FreeBSDSyscallLeaf_SymbolsAndNumbers;
end;
TDynLinkerTests = class(TTestCase)
@ -1090,6 +1097,118 @@ begin
AssertTrue('static ET_EXEC must have no PT_INTERP', not FoundInterp);
end;
procedure TLinkerE2ETests.TestLink_FreeBSDSyscallLeaf_SymbolsAndNumbers;
const
{ Mirrors the runtime.syscall.freebsd leaf bodies byte-for-byte (a handful of
representative leaves): each is `movq $N, %rax; syscall; jae ok; negq %rax;
ok: ret`, with mmap's `movq %rcx, %r10` and stat lowered to
fstatat(AT_FDCWD, path, buf, 0). Names are the bare POSIX symbols posix
imports. A leading _start keeps the linker's entry resolvable. }
FixtureAsm =
'.text' + LineEnding +
'.globl _start' + LineEnding +
'_start:' + LineEnding +
' callq open' + LineEnding +
' callq read' + LineEnding +
' callq write' + LineEnding +
' callq mmap' + LineEnding +
' callq stat' + LineEnding +
' xorl %edi, %edi' + LineEnding +
' movq $1, %rax' + LineEnding + { SYS_exit }
' syscall' + LineEnding +
' hlt' + LineEnding +
'.globl open' + LineEnding +
'open:' + LineEnding +
' movq $5, %rax' + LineEnding + { SYS_open }
' syscall' + LineEnding +
' jae .Lok_open' + LineEnding +
' negq %rax' + LineEnding +
'.Lok_open:' + LineEnding +
' ret' + LineEnding +
'.globl read' + LineEnding +
'read:' + LineEnding +
' movq $3, %rax' + LineEnding + { SYS_read }
' syscall' + LineEnding +
' jae .Lok_read' + LineEnding +
' negq %rax' + LineEnding +
'.Lok_read:' + LineEnding +
' ret' + LineEnding +
'.globl write' + LineEnding +
'write:' + LineEnding +
' movq $4, %rax' + LineEnding + { SYS_write }
' syscall' + LineEnding +
' jae .Lok_write' + LineEnding +
' negq %rax' + LineEnding +
'.Lok_write:' + LineEnding +
' ret' + LineEnding +
'.globl mmap' + LineEnding +
'mmap:' + LineEnding +
' movq %rcx, %r10' + LineEnding +
' movq $477, %rax' + LineEnding + { SYS_mmap (ino64) }
' syscall' + LineEnding +
' jae .Lok_mmap' + LineEnding +
' negq %rax' + LineEnding +
'.Lok_mmap:' + LineEnding +
' ret' + LineEnding +
'.globl stat' + LineEnding +
'stat:' + LineEnding +
' movq %rsi, %rdx' + LineEnding +
' movq %rdi, %rsi' + LineEnding +
' movq $-100, %rdi' + LineEnding + { AT_FDCWD }
' xorq %r10, %r10' + LineEnding +
' movq $552, %rax' + LineEnding + { SYS_fstatat (ino64) }
' syscall' + LineEnding +
' jae .Lok_stat' + LineEnding +
' negq %rax' + LineEnding +
'.Lok_stat:' + LineEnding +
' ret' + LineEnding;
{ Encoded `movq $imm32, %rax` = 48 C7 C0 <imm32-LE>. These byte strings must
appear in the linked .text if the correct syscall number was emitted. }
function MovRaxImm(AImm: Integer): string;
begin
Result := Chr($48) + Chr($C7) + Chr($C0) +
Chr(AImm and $FF) + Chr((AImm shr 8) and $FF) +
Chr((AImm shr 16) and $FF) + Chr((AImm shr 24) and $FF);
end;
var
Lk: TLinker;
Obj: TElfObjectFile;
Bytes: string;
begin
Lk := TLinker.Create(FreeBSDX86_64Target());
try
Obj := ParseElfObject(AssembleToBytes(FixtureAsm), 'fbsys.o');
Lk.AddOwnedObject(Obj);
Bytes := Lk.LinkToBytes('_start');
{ 1. The bare POSIX leaf symbols must be DEFINED (non-zero address). }
AssertTrue('open defined', Lk.AddrOfSymbol('open') > 0);
AssertTrue('read defined', Lk.AddrOfSymbol('read') > 0);
AssertTrue('write defined', Lk.AddrOfSymbol('write') > 0);
AssertTrue('mmap defined', Lk.AddrOfSymbol('mmap') > 0);
AssertTrue('stat defined', Lk.AddrOfSymbol('stat') > 0);
finally
Lk.Free();
end;
{ 2. FreeBSD ino64 syscall NUMBERS present in the linked .text as the
`movq $N, %rax` encoding NOT Linux's numbers. }
AssertTrue('SYS_open=5 movq imm present', Pos(MovRaxImm(5), Bytes) >= 0);
AssertTrue('SYS_read=3 movq imm present', Pos(MovRaxImm(3), Bytes) >= 0);
AssertTrue('SYS_write=4 movq imm present', Pos(MovRaxImm(4), Bytes) >= 0);
AssertTrue('SYS_mmap=477 movq imm present', Pos(MovRaxImm(477), Bytes) >= 0);
AssertTrue('SYS_fstatat=552 movq imm present (stat via fstatat)',
Pos(MovRaxImm(552), Bytes) >= 0);
{ 3. The CF-error-translation idiom: negq %rax (48 F7 D8) must appear. This
is the FreeBSD-specific errno translation the Linux leaf does not emit. }
AssertTrue('negq %rax (CF translation) present',
Pos(Chr($48) + Chr($F7) + Chr($D8), Bytes) >= 0);
{ mmap's arg4 shuffle: movq %rcx, %r10 (49 89 CA). }
AssertTrue('movq %rcx,%r10 (mmap arg4) present',
Pos(Chr($49) + Chr($89) + Chr($CA), Bytes) >= 0);
end;
{ ---- TDynLinkerTests ---- }
function TDynLinkerTests.ProjectRoot: string;