diff --git a/compiler/src/main/pascal/Blaise.pas b/compiler/src/main/pascal/Blaise.pas index b92d98d..63517f1 100644 --- a/compiler/src/main/pascal/Blaise.pas +++ b/compiler/src/main/pascal/Blaise.pas @@ -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 diff --git a/compiler/src/main/pascal/blaise.codegen.driver.pas b/compiler/src/main/pascal/blaise.codegen.driver.pas index 17f4267..d029270 100644 --- a/compiler/src/main/pascal/blaise.codegen.driver.pas +++ b/compiler/src/main/pascal/blaise.codegen.driver.pas @@ -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. + runtime.cstub + + runtime.start.static.) 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., and add the raw + syscall stubs (runtime.syscall.) + 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(); diff --git a/compiler/src/main/pascal/blaise.codegen.native.driver.pas b/compiler/src/main/pascal/blaise.codegen.native.driver.pas index 1b3b5d5..658dce5 100644 --- a/compiler/src/main/pascal/blaise.codegen.native.driver.pas +++ b/compiler/src/main/pascal/blaise.codegen.native.driver.pas @@ -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. 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); diff --git a/compiler/src/main/pascal/blaise.linker.elf.pas b/compiler/src/main/pascal/blaise.linker.elf.pas index 5741996..77c88dc 100644 --- a/compiler/src/main/pascal/blaise.linker.elf.pas +++ b/compiler/src/main/pascal/blaise.linker.elf.pas @@ -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); diff --git a/compiler/src/main/pascal/runtime.cstub.pas b/compiler/src/main/pascal/runtime.cstub.pas new file mode 100644 index 0000000..1c8d2c2 --- /dev/null +++ b/compiler/src/main/pascal/runtime.cstub.pas @@ -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. diff --git a/compiler/src/main/pascal/runtime.start.static.linux.pas b/compiler/src/main/pascal/runtime.start.static.linux.pas new file mode 100644 index 0000000..29e5187 --- /dev/null +++ b/compiler/src/main/pascal/runtime.start.static.linux.pas @@ -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. diff --git a/compiler/src/main/pascal/runtime.syscall.linux.pas b/compiler/src/main/pascal/runtime.syscall.linux.pas new file mode 100644 index 0000000..12670c1 --- /dev/null +++ b/compiler/src/main/pascal/runtime.syscall.linux.pas @@ -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. diff --git a/compiler/src/test/pascal/cp.test.linker.pas b/compiler/src/test/pascal/cp.test.linker.pas index b814ea1..fb41320 100644 --- a/compiler/src/test/pascal/cp.test.linker.pas +++ b/compiler/src/test/pascal/cp.test.linker.pas @@ -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;