feat(rtl): direct-syscall kernel-leaf foundation + --static link mode (Linux, WIP)

First increment of the libc-free direct-syscall RTL (Linux as the proving
ground for FreeBSD; docs/linux-syscall-migration.adoc to follow).  The default
build is unchanged — libc-backed dynamic ELF; everything here is behind the new
--static flag.

New kernel-leaf units (selected by EnsureRTLObjects when --static):
- runtime.syscall.linux — raw `syscall` stubs DEFINING the bare POSIX names the
  RTL imports (open/read/write/lseek/close/fstat/stat/mkdir/rmdir/unlink/rename/
  chdir/getpid/dup2/pipe/mmap/munmap/nanosleep/clock_gettime/exit/_exit).
  `write`/`exit` are Pascal-reserved, so their Pascal names are _sys_* and the
  asm body emits a bare label alias.  4th syscall arg moved %rcx->%r10.
- runtime.cstub — freestanding memcpy/memset/memcmp/strlen (no Char type; PChar
  byte indexing).
- runtime.start.static.linux — freestanding _start (argc/argv off %rsp, call
  main, exit_group) replacing the libc __libc_start_main entry.

Wiring: --static -> TBackendOpts.Static; EnsureRTLObjects swaps runtime.start
for runtime.start.static.linux and adds the syscall+cstub leaves; the native
internal linker calls SetDynamic(False) for a non-PIE ET_EXEC.

Internal linker static-mode fixes (these are correct in any ET_EXEC, not just
PIE — they were wrongly gated to dynamic-only):
- R_X86_64_64 against a defined symbol resolves to the symbol's absolute address
  (no dynamic reloc); only PIE/dynamic mode also emits the .rela.dyn RELATIVE.
- R_X86_64_TPOFF32 (Local-Exec TLS) resolves at link time — it IS the static TLS
  model. Updated TestReloc_Quad64_* to assert the now-correct resolution.

A --static link currently reaches symbol resolution and reports the remaining
libc leaves to implement (mremap/getcwd/fork/execvp/waitpid/time/getenv/sysconf/
gmtime_r/localtime_r/timegm/__cxa_atexit/abort/mkstemp/system/pthread_mutex_*) —
the next increments.  Full suite green (3829); dynamic native/QBE unaffected.
This commit is contained in:
Graeme Geldenhuys 2026-06-26 00:39:21 +01:00
parent 93ac17bb62
commit be0e519084
8 changed files with 524 additions and 40 deletions

View file

@ -236,6 +236,8 @@ begin
Inc(I);
AOpts.RTLSrcDir := ParamStr(I);
end
else if Arg = '--static' then
AOpts.Static := True
else if Arg = '--emit-ir' then
AFront.EmitIR := True
else if Arg = '--emit-asm' then

View file

@ -92,6 +92,12 @@ type
Overrides the binary/CWD-relative lookup, for
a relocated/release binary whose RTL source
lives elsewhere (empty = use default lookup) }
Static: Boolean; { --static: link a freestanding, libc-free ELF
(native internal linker only). The kernel
leaf (runtime.syscall.<os> + runtime.cstub +
runtime.start.static.<os>) replaces libc; the
linker emits a non-PIE ET_EXEC with no
PT_INTERP. docs/linux-syscall-migration.adoc }
end;
TBackendDriver = class
@ -430,7 +436,7 @@ var
I, J, ExitCode: Integer;
Args: TStringList;
Msg: string;
ProvidedSyms, MySyms: TStringList;
ProvidedSyms, MySyms, Units: TStringList;
Redundant: Boolean;
begin
Result := '';
@ -481,10 +487,27 @@ begin
ForceDirectories(BuildDir);
BlaiseBin := ParamStr(0);
{ The unit list to build/link, in leaf-first order. For a --static link the
kernel leaf replaces libc: drop runtime.start (libc/__libc_start_main entry)
in favour of the freestanding runtime.start.static.<os>, and add the raw
syscall stubs (runtime.syscall.<os>) + the freestanding C functions
(runtime.cstub). A dynamic (libc) link uses the plain RTL_UNITS. }
Units := TStringList.Create();
for I := 0 to High(RTL_UNITS) do
if AOpts.Static and SameText(RTL_UNITS[I], 'runtime.start') then
Units.Add('runtime.start.static.linux')
else
Units.Add(RTL_UNITS[I]);
if AOpts.Static then
begin
SrcFile := IncludeTrailingPathDelimiter(SrcDir) + RTL_UNITS[I] + '.pas';
ObjFile := IncludeTrailingPathDelimiter(CacheDir) + RTL_UNITS[I] + '.o';
Units.Add('runtime.syscall.linux');
Units.Add('runtime.cstub');
end;
for I := 0 to Units.Count - 1 do
begin
SrcFile := IncludeTrailingPathDelimiter(SrcDir) + Units.Strings[I] + '.pas';
ObjFile := IncludeTrailingPathDelimiter(CacheDir) + Units.Strings[I] + '.o';
if not FileExists(SrcFile) then
Exit('RTL unit source missing: ' + SrcFile);
@ -495,7 +518,7 @@ begin
carry inline asm, so the native backend + internal assembler is mandatory. }
if (not FileExists(ObjFile)) or (FileAge(ObjFile) < FileAge(SrcFile)) then
begin
TmpObj := IncludeTrailingPathDelimiter(BuildDir) + RTL_UNITS[I] + '.o';
TmpObj := IncludeTrailingPathDelimiter(BuildDir) + Units.Strings[I] + '.o';
Args := TStringList.Create();
try
Args.Add('--backend'); Args.Add('native');
@ -509,7 +532,7 @@ begin
Args.Free();
end;
if ExitCode <> 0 then
Exit('failed to build RTL unit ' + RTL_UNITS[I] +
Exit('failed to build RTL unit ' + Units.Strings[I] +
' (exit ' + IntToStr(ExitCode) + '): ' + Msg);
{ Publish atomically. RenameFile within the same directory tree is an
atomic rename(2); a concurrent builder doing the same is harmless. }
@ -520,7 +543,7 @@ begin
{ runtime.start defines the bare _start entry include it only for the
native internal linker (Blaise owns the entry); the cc link line gets
_start from libc and must omit it to avoid a multiple-definition. }
if SameText(RTL_UNITS[I], 'runtime.start') and (not AIncludeStartup) then
if SameText(Units.Strings[I], 'runtime.start') and (not AIncludeStartup) then
Continue;
{ Skip this RTL object if EVERY symbol it defines is already defined by the
caller's objects i.e. the program compiled this RTL unit itself. A
@ -549,12 +572,13 @@ begin
cache now; the build dir holds only intermediate .bif/.o stragglers. Remove
the known per-unit artefacts and the (now-empty) directory. Best-effort
a leftover dir is harmless. }
for I := 0 to High(RTL_UNITS) do
for I := 0 to Units.Count - 1 do
begin
DeleteFile(IncludeTrailingPathDelimiter(BuildDir) + RTL_UNITS[I] + '.o');
DeleteFile(IncludeTrailingPathDelimiter(BuildDir) + RTL_UNITS[I] + '.bif');
DeleteFile(IncludeTrailingPathDelimiter(BuildDir) + Units.Strings[I] + '.o');
DeleteFile(IncludeTrailingPathDelimiter(BuildDir) + Units.Strings[I] + '.bif');
end;
_driver_rmdir(PChar(ExcludeTrailingPathDelimiter(BuildDir)));
Units.Free();
finally
ProvidedSyms.Free();
MySyms.Free();

View file

@ -215,8 +215,11 @@ begin
RTLObjs := TStringList.Create();
try
{ Native internal linker: Blaise owns the entry point, so include
runtime.start (its bare _start). Pass the program's prebuilt deps so an
RTL unit it compiled itself is not supplied twice. }
runtime.start (its bare _start). In --static mode EnsureRTLObjects swaps
runtime.start for the freestanding runtime.start.static.<os> and adds the
syscall + cstub leaves, so AIncludeStartup is irrelevant there. Pass the
program's prebuilt deps so an RTL unit it compiled itself is not supplied
twice. }
Result := Self.EnsureRTLObjects(AOpts, True, AExtraObjects, RTLObjs);
if Result <> '' then Exit;
@ -225,7 +228,10 @@ begin
Lk := TLinker.Create(LinkTarget);
try
try
Lk.SetDynamic(True);
{ --static: freestanding non-PIE ET_EXEC, no libc/PT_INTERP (the kernel
leaf supplies open/read/write/ + _start). Default: dynamic PIE
linked against libc. }
Lk.SetDynamic(not AOpts.Static);
Obj := ReadElfObjectFile(AObjFile);
Lk.AddOwnedObject(Obj);

View file

@ -1676,19 +1676,25 @@ begin
R_X86_64_64:
begin
if not FDynamic then
raise ELinker.Create(Ctx + ': R_X86_64_64 relocation against `'
+ Sym.Name + ''' needs dynamic linking');
S := Self.ResolveSymbolAddr(Obj, Rel.SymIndex, Ctx);
Val := S + Rel.Addend;
if M.ShType = SHT_NOBITS then
raise ELinker.Create(Ctx
+ ': relocation into a NOBITS section');
{ Patch the absolute 64-bit target address. In a non-PIE ET_EXEC
(static mode) every symbol has a fixed load address, so this value
is final and needs no run-time fixup. In PIE/dynamic mode the
image base is unknown until load, so the same slot also gets an
R_X86_64_RELATIVE entry in .rela.dyn for the loader to re-add the
base. }
LkPatch64(M.Data, PFileOff, Val);
RE := TRelaDynEntry.Create();
RE.VAddr := PAddr;
RE.Addend := Val;
FRelaDyn.Add(RE);
if FDynamic then
begin
RE := TRelaDynEntry.Create();
RE.VAddr := PAddr;
RE.Addend := Val;
FRelaDyn.Add(RE);
end;
end;
R_X86_64_32, R_X86_64_32S:
@ -1719,10 +1725,12 @@ begin
end;
R_X86_64_TPOFF32:
{ Local-Exec TLS: the offset from the thread pointer (which points at
the END of the static TLS block) down to this threadvar. This is the
STATIC TLS model it is resolvable at link time in both static and
PIE/dynamic executables (the threadvar's position within the initial
TLS block is fixed at link), so it needs no dynamic relocation. }
begin
if not FDynamic then
raise ELinker.Create(Ctx
+ ': TLS relocation needs dynamic linking');
S := Self.ResolveSymbolAddr(Obj, Rel.SymIndex, Ctx);
Val := S - FTlsAddr + Rel.Addend - FTlsSize;
LkPatch32(M.Data, PFileOff, Val and $FFFFFFFF);

View file

@ -0,0 +1,105 @@
{
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.cstub;
// Freestanding C-library leaves the compiler and RTL emit calls to but which
// are not syscalls: memcpy / memset / memcmp / strlen. In a libc-backed build
// these come from libc; in a --static build this unit supplies them so no libc
// is needed (docs/linux-syscall-migration.adoc).
//
// Architecture-neutral byte-at-a-time implementations. Correctness over speed:
// these are the fallback for the freestanding build; if profiling shows the
// allocator/string paths need it, an SSE/AVX memcpy can replace the loop later
// (the inline-asm assembler already encodes the vector ops).
//
// Signatures match the C ABI the codegen assumes:
// void* memcpy(void* dst, const void* src, size_t n) returns dst
// void* memset(void* dst, int c, size_t n) returns dst
// int memcmp(const void* a, const void* b, size_t n)
// size_t strlen(const char* s)
interface
function memcpy(Dst, Src: Pointer; N: Int64): Pointer;
function memset(Dst: Pointer; C: Integer; N: Int64): Pointer;
function memcmp(A, B: Pointer; N: Int64): Integer;
function strlen(S: PChar): Int64;
implementation
{ PChar indexing reads/writes a single byte as an Integer (see runtime.str), so
these loops need no Char type (Blaise has none). }
function memcpy(Dst, Src: Pointer; N: Int64): Pointer;
var
D, S: PChar;
I: Int64;
begin
D := PChar(Dst);
S := PChar(Src);
I := 0;
while I < N do
begin
D[I] := S[I];
I := I + 1;
end;
Result := Dst;
end;
function memset(Dst: Pointer; C: Integer; N: Int64): Pointer;
var
D: PChar;
I: Int64;
B: Integer;
begin
D := PChar(Dst);
B := C and $FF;
I := 0;
while I < N do
begin
D[I] := B;
I := I + 1;
end;
Result := Dst;
end;
function memcmp(A, B: Pointer; N: Int64): Integer;
var
PA, PB: PChar;
I: Int64;
CA, CB: Integer;
begin
PA := PChar(A);
PB := PChar(B);
I := 0;
while I < N do
begin
CA := PA[I] and $FF;
CB := PB[I] and $FF;
if CA <> CB then
begin
Result := CA - CB;
Exit;
end;
I := I + 1;
end;
Result := 0;
end;
function strlen(S: PChar): Int64;
var
I: Int64;
begin
I := 0;
while (S[I] and $FF) <> 0 do
I := I + 1;
Result := I;
end;
end.

View file

@ -0,0 +1,51 @@
{
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.linux;
// Freestanding `_start` for a static, libc-free Linux ET_EXEC (the --static
// kernel-leaf swap; docs/linux-syscall-migration.adoc).
//
// The libc-backed runtime.start calls __libc_start_main, which only exists when
// linking libc. This variant is the drop-in replacement linked instead when a
// program is built --static: it sets up argc/argv from the raw process stack and
// jumps straight to `main` (which the backend emits to call _SetArgs/_BlaiseInit,
// run the body, and `exit`).
//
// On entry the kernel hands us the initial process stack (System V AMD64,
// "Initial Process Stack"):
// (%rsp) = argc
// 8(%rsp) = argv[0], argv[1], then a NULL, then envp, then auxv.
// %rsp is 16-byte aligned at the kernel's `_start` only AFTER we account for the
// implicit return address a normal `call` would have pushed i.e. on entry
// (%rsp) points at argc and the ABI's "16-byte aligned at call site" means after
// our own alignment the stack is correct for the `call main`.
//
// main expects C-`main(argc, argv)` register layout: argc in %edi, argv in %rsi.
interface
procedure _start;
implementation
procedure _start; assembler; nostackframe;
asm
endbr64
xor %ebp, %ebp { clear frame pointer — outermost frame }
movq (%rsp), %rdi { %rdi = argc }
leaq 8(%rsp), %rsi { %rsi = &argv[0] }
andq $0xfffffffffffffff0, %rsp { 16-byte align before the call }
call main
xorl %edi, %edi { main terminates via exit; guard with exit_group(0) }
movq $231, %rax { SYS_exit_group }
syscall
hlt
end;
end.

View file

@ -0,0 +1,287 @@
{
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.linux;
// Linux x86_64 direct-syscall kernel leaf the libc replacement for the thin
// 1:1 syscall wrappers (docs/linux-syscall-migration.adoc).
//
// This unit DEFINES the bare POSIX names (open, read, write, ) that
// rtl.platform.posix.pas references via `external name 'open'` etc. When it is
// linked into a program (the --static link-time swap), those symbols resolve
// here as raw `syscall` stubs instead of being imported from libc.so.6 so the
// resulting binary is a freestanding static ET_EXEC with no libc, no PT_INTERP.
//
// System V AMD64 -> Linux syscall ABI:
// * syscall number in %rax; the `syscall` instruction returns in %rax.
// * Argument registers DIFFER between the C ABI and the kernel ABI on the 4th
// argument: C passes arg4 in %rcx, the kernel expects it in %r10 (the
// `syscall` instruction itself clobbers %rcx). Wrappers with >= 4 args
// therefore `movq %rcx, %r10` first. Args 1-3 (%rdi,%rsi,%rdx) and 5-6
// (%r8,%r9) are already in the right registers.
// * The kernel returns -errno on error (a small negative value). libc would
// translate that to a -1 return + `errno`; the existing Pascal call sites
// test `if result < 0` / `if Fd < 0`, which a raw -errno also satisfies, so
// no errno table is needed for these leaves.
//
// `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;
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;
{ Process. }
function getpid: Integer;
function dup2(OldFd, NewFd: Integer): Integer;
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 exit exit_group(2), terminates all threads. Neither runs atexit
handlers (that lives in the atexit-registry leaf, a later step); for now the
bare `exit` (called by main's epilogue) and `_exit` both terminate directly.
`exit` is a reserved word in Pascal, so the Pascal proc is _sys_exit and the
asm body emits a bare `exit` label (like `write` -> _sys_write). }
function _sys_exit(Code: Integer): Integer;
procedure _exit(Code: Integer);
implementation
{ ---- Linux x86_64 syscall numbers (arch/x86/entry/syscalls). ---- }
const
SYS_read = 0;
SYS_write = 1;
SYS_open = 2;
SYS_close = 3;
SYS_stat = 4;
SYS_fstat = 5;
SYS_lseek = 8;
SYS_mmap = 9;
SYS_munmap = 11;
SYS_pipe = 22;
SYS_dup2 = 33;
SYS_nanosleep = 35;
SYS_getpid = 39;
SYS_rename = 82;
SYS_mkdir = 83;
SYS_rmdir = 84;
SYS_unlink = 87;
SYS_chdir = 80;
SYS_clock_gettime = 228;
SYS_exit_group = 231;
{ 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 $2, %rax { SYS_open }
syscall
ret
end;
function close(Fd: Integer): Integer;
assembler; nostackframe;
asm
movq $3, %rax { SYS_close }
syscall
ret
end;
function read(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
assembler; nostackframe;
asm
movq $0, %rax { SYS_read }
syscall
ret
end;
function _sys_write(Fd: Integer; Buf: Pointer; Count: Int64): Int64;
assembler; nostackframe;
asm
.globl write
write:
movq $1, %rax { SYS_write }
syscall
ret
end;
function lseek(Fd: Integer; Offset: Int64; Whence: Integer): Int64;
assembler; nostackframe;
asm
movq $8, %rax { SYS_lseek }
syscall
ret
end;
function fstat(Fd: Integer; Buf: Pointer): Integer;
assembler; nostackframe;
asm
movq $5, %rax { SYS_fstat }
syscall
ret
end;
function stat(Path: PChar; Buf: Pointer): Integer;
assembler; nostackframe;
asm
movq $4, %rax { SYS_stat }
syscall
ret
end;
function mkdir(Path: PChar; Mode: Integer): Integer;
assembler; nostackframe;
asm
movq $83, %rax { SYS_mkdir }
syscall
ret
end;
function rmdir(Path: PChar): Integer;
assembler; nostackframe;
asm
movq $84, %rax { SYS_rmdir }
syscall
ret
end;
function unlink(Path: PChar): Integer;
assembler; nostackframe;
asm
movq $87, %rax { SYS_unlink }
syscall
ret
end;
function rename(OldPath, NewPath: PChar): Integer;
assembler; nostackframe;
asm
movq $82, %rax { SYS_rename }
syscall
ret
end;
function chdir(Path: PChar): Integer;
assembler; nostackframe;
asm
movq $80, %rax { SYS_chdir }
syscall
ret
end;
function getpid: Integer;
assembler; nostackframe;
asm
movq $39, %rax { SYS_getpid }
syscall
ret
end;
function dup2(OldFd, NewFd: Integer): Integer;
assembler; nostackframe;
asm
movq $33, %rax { SYS_dup2 }
syscall
ret
end;
function pipe(Fds: Pointer): Integer;
assembler; nostackframe;
asm
movq $22, %rax { SYS_pipe }
syscall
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 $9, %rax { SYS_mmap }
syscall
ret
end;
function munmap(Addr: Pointer; Length: Int64): Integer;
assembler; nostackframe;
asm
movq $11, %rax { SYS_munmap }
syscall
ret
end;
function nanosleep(Req, Rem: Pointer): Integer;
assembler; nostackframe;
asm
movq $35, %rax { SYS_nanosleep }
syscall
ret
end;
function clock_gettime(ClockId: Integer; Ts: Pointer): Integer;
assembler; nostackframe;
asm
movq $228, %rax { SYS_clock_gettime }
syscall
ret
end;
{ exit_group(2) terminates the whole process (all threads) and does not return.
The bare `exit` (called by main's epilogue) and `_exit` are the same raw
terminator until the atexit-registry leaf lands. Code arrives in %edi;
exit_group reads its status from %rdi, so no marshalling is needed. }
function _sys_exit(Code: Integer): Integer;
assembler; nostackframe;
asm
.globl exit
exit:
movq $231, %rax { SYS_exit_group }
syscall
ret
end;
procedure _exit(Code: Integer);
assembler; nostackframe;
asm
movq $231, %rax { SYS_exit_group }
syscall
ret
end;
end.

View file

@ -58,7 +58,7 @@ type
procedure TestSym_SynthesisedSymbolsDefined;
{ Static relocations }
procedure TestReloc_PC32CrossObjectCall;
procedure TestReloc_Quad64_Raises;
procedure TestReloc_Quad64_ResolvesToAbsoluteAddr;
{ Executable structure }
procedure TestExe_ElfHeaderIsExec;
procedure TestExe_EntryPointMatchesSymbol;
@ -786,29 +786,30 @@ begin
end;
end;
procedure TLinkerTests.TestReloc_Quad64_Raises;
procedure TLinkerTests.TestReloc_Quad64_ResolvesToAbsoluteAddr;
var
Lk: TLinker;
Bytes: string;
Raised: Boolean;
begin
{ An absolute 64-bit pointer to a symbol needs dynamic linking under
a real PIE; Phase B rejects R_X86_64_64 explicitly. }
Raised := False;
Lk := nil;
{ An absolute 64-bit pointer to a symbol (`.quad target`, R_X86_64_64) is
resolvable at link time in a non-PIE ET_EXEC: every symbol has a fixed load
address. The static linker must patch the slot with target's absolute
address (no dynamic relocation) this is what makes a freestanding static
--static link possible (docs/linux-syscall-migration.adoc). }
Lk := LinkObjs(
['.data' + LineEnding + 'ptr:' + LineEnding + '.quad target' + LineEnding +
'.text' + LineEnding + '.globl target' + LineEnding + 'target:' +
LineEnding + 'ret' + LineEnding +
'.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
'_start', Bytes);
try
Lk := LinkObjs(
['.data' + LineEnding + 'ptr:' + LineEnding + '.quad target' + LineEnding +
'.text' + LineEnding + '.globl target' + LineEnding + 'target:' +
LineEnding + 'ret' + LineEnding +
'.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
'_start', Bytes);
except
on E: ELinker do
Raised := True;
{ The link must succeed and produce a valid ET_EXEC. }
AssertTrue('static R_X86_64_64 link produced no output', Length(Bytes) >= 64);
AssertEquals('e_type ET_EXEC', 2,
(Ord(Bytes[16]) and $FF) or ((Ord(Bytes[17]) and $FF) shl 8));
finally
Lk.Free();
end;
Lk.Free();
AssertTrue('R_X86_64_64 must raise ELinker in Phase B', Raised);
end;
procedure TLinkerTests.TestExe_ElfHeaderIsExec;