2026-06-12 12:10:21 +03:00
|
|
|
{
|
|
|
|
|
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 cp.test.linker;
|
|
|
|
|
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
{ Tests for the internal linker:
|
|
|
|
|
Phase A — blaise.elfreader (ELF relocatable-object parsing and
|
|
|
|
|
ar-archive parsing with GNU long-name support) and TSectionMerger.
|
|
|
|
|
Phase B — TLinker symbol resolution, static PC-relative relocations,
|
|
|
|
|
and non-PIE ET_EXEC emission, including a hand-written syscall-only
|
|
|
|
|
fixture that is linked internally, run, and asserted. }
|
2026-06-12 12:10:21 +03:00
|
|
|
|
|
|
|
|
interface
|
|
|
|
|
|
|
|
|
|
uses
|
2026-07-04 21:44:54 +03:00
|
|
|
SysUtils, process, blaise.testing, Generics.Collections,
|
2026-06-12 12:13:16 +03:00
|
|
|
blaise.elfreader, blaise.linker.elf, blaise.assembler.x86_64;
|
2026-06-12 12:10:21 +03:00
|
|
|
|
|
|
|
|
type
|
|
|
|
|
TElfReaderTests = class(TTestCase)
|
|
|
|
|
private
|
|
|
|
|
function ProjectRoot: string;
|
|
|
|
|
function PadField(const AVal: string; AWidth: Integer): string;
|
|
|
|
|
published
|
|
|
|
|
procedure TestParse_TextSectionBytes;
|
|
|
|
|
procedure TestParse_GlobalFuncSymbol;
|
|
|
|
|
procedure TestParse_QuadRelocation;
|
|
|
|
|
procedure TestParse_BssNoData;
|
|
|
|
|
procedure TestParse_BadMagic_Raises;
|
|
|
|
|
procedure TestArchive_SyntheticLongNames;
|
|
|
|
|
procedure TestArchive_BadMagic_Raises;
|
|
|
|
|
procedure TestArchive_ParsesRTL;
|
|
|
|
|
end;
|
|
|
|
|
|
2026-06-12 12:13:16 +03:00
|
|
|
TSectionMergerTests = class(TTestCase)
|
|
|
|
|
published
|
|
|
|
|
procedure TestMerge_ConcatenatesText;
|
|
|
|
|
procedure TestMerge_AlignmentPadding;
|
|
|
|
|
procedure TestMerge_BssSizesAccumulate;
|
|
|
|
|
procedure TestMerge_SkipsBookkeepingSections;
|
|
|
|
|
end;
|
|
|
|
|
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
TLinkerTests = class(TTestCase)
|
|
|
|
|
private
|
|
|
|
|
function LinkObjs(AObjAsm: array of string;
|
|
|
|
|
const AEntry: string; out ABytes: string): TLinker;
|
|
|
|
|
published
|
|
|
|
|
{ Symbol resolution }
|
|
|
|
|
procedure TestSym_GlobalResolvesToVaddr;
|
feat(linker): wire internal linker into compiler driver + fix TLS relocations
Add --linker internal|external option to the native backend driver,
routing the link step through TLinker instead of shelling out to cc.
FindCrtObjects discovers CRT startup objects (Scrt1.o, crti.o, crtn.o,
crtbeginS.o, crtendS.o) on Debian and RedHat layouts.
Fix SHF_TLS constant from $200 to the correct ELF spec value $400 in
both the reader and writer — the wrong value caused .tbss sections to
be placed twice (once in the normal NOBITS loop, once in the TLS
section), producing doubled negative TPOFF32 offsets and segfaults.
Fix FTlsAddr to use the post-alignment address from PlaceSection rather
than the pre-alignment cursor, which caused off-by-padding TPOFF values.
Change duplicate global symbol handling from error to first-wins
semantics (matching ld behaviour), needed because the main program
object and archive members may both define system runtime functions.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3532 tests pass on both QBE-built and native-built test runners.
2026-06-19 12:43:33 +03:00
|
|
|
procedure TestSym_TwoGlobalsDuplicate_FirstWins;
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
procedure TestSym_StrongUndefined_Raises;
|
|
|
|
|
procedure TestSym_WeakUndefinedResolvesToZero;
|
|
|
|
|
procedure TestSym_SynthesisedSymbolsDefined;
|
|
|
|
|
{ Static relocations }
|
|
|
|
|
procedure TestReloc_PC32CrossObjectCall;
|
2026-07-04 21:44:54 +03:00
|
|
|
procedure TestReloc_Quad64_Raises;
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
{ Executable structure }
|
|
|
|
|
procedure TestExe_ElfHeaderIsExec;
|
|
|
|
|
procedure TestExe_EntryPointMatchesSymbol;
|
|
|
|
|
procedure TestExe_MissingEntry_Raises;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
TLinkerE2ETests = class(TTestCase)
|
|
|
|
|
private
|
|
|
|
|
FScratch: string;
|
|
|
|
|
function ProjectRoot: string;
|
|
|
|
|
function RunBin(const AExe: string; out AStdout: string): Integer;
|
|
|
|
|
protected
|
|
|
|
|
procedure SetUp; override;
|
|
|
|
|
published
|
|
|
|
|
procedure TestRun_SyscallHelloWorld;
|
|
|
|
|
end;
|
|
|
|
|
|
feat(linker): Phase C dynamic linking — GOT/PLT, CRT objects, PIE emission
Implement Phase C of the internal ELF linker: produce position-independent
ET_DYN executables linked against libc through dynamic GOT/PLT dispatch.
New capabilities:
- SetDynamic(True) switches the linker to PIE mode
- AddCrtObject() loads system CRT startup objects (Scrt1.o, crti.o,
crtn.o, crtbeginS.o, crtendS.o)
- CollectExternals scans undefined non-weak symbols and creates PLT entries
- CollectGotSlots creates non-PLT GOT slots for GOTPCREL references to
locally-defined symbols (e.g. main, __dso_handle)
- BuildDynamic generates .interp, .dynstr, .dynsym, SysV .hash, .got
(3 reserved + N PLT + M non-PLT slots), .plt (header + stubs),
.rela.plt (JUMP_SLOT entries)
- LayoutDynamic assigns PIE virtual addresses (base 0): RO metadata →
.text/.plt → RELRO (.init_array, .fini_array, .dynamic, .got) →
.data/.bss, with page-aligned PT_LOAD boundaries
- EmitDynExecutable writes the full PIE byte image with ELF header,
10 program headers (PHDR, INTERP, 4×LOAD, DYNAMIC, TLS, GNU_STACK,
GNU_RELRO), and section data at virtual offsets
Relocation handling extended:
- R_X86_64_64 → R_X86_64_RELATIVE in .rela.dyn
- R_X86_64_32/32S for absolute symbol references
- R_X86_64_GOTPCREL/GOTPCRELX/REX_GOTPCRELX → GOT slot lookup
- R_X86_64_TPOFF32 → TLS offset calculation
Tests: TDynLinkerTests (2 structural), TDynLinkerE2ETests (1 E2E that
links a main() calling write via PLT, runs the binary, asserts exit
code 42 + correct stdout). All 3528 tests pass.
2026-06-19 12:00:08 +03:00
|
|
|
TDynLinkerTests = class(TTestCase)
|
|
|
|
|
private
|
|
|
|
|
function ProjectRoot: string;
|
|
|
|
|
published
|
|
|
|
|
procedure TestDyn_CollectsExternals;
|
|
|
|
|
procedure TestDyn_PieHeaderEmitted;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
TDynLinkerE2ETests = class(TTestCase)
|
|
|
|
|
private
|
|
|
|
|
FScratch: string;
|
|
|
|
|
function ProjectRoot: string;
|
|
|
|
|
function RunBin(const AExe: string; out AStdout: string): Integer;
|
|
|
|
|
protected
|
|
|
|
|
procedure SetUp; override;
|
|
|
|
|
published
|
|
|
|
|
procedure TestRun_DynHelloWorld;
|
|
|
|
|
end;
|
|
|
|
|
|
2026-06-19 12:48:04 +03:00
|
|
|
TInternalLinkerE2ETests = class(TTestCase)
|
|
|
|
|
private
|
|
|
|
|
FScratch: string;
|
|
|
|
|
FCompiler: string;
|
|
|
|
|
FRTLPath: string;
|
|
|
|
|
FStdlibPath: string;
|
|
|
|
|
FCounter: Integer;
|
|
|
|
|
function ProjectRoot: string;
|
|
|
|
|
function CompilerAvailable: Boolean;
|
|
|
|
|
function RunProc(const AExe: string; AArgs: array of string;
|
|
|
|
|
out AStdout: string): Integer;
|
|
|
|
|
function RunProcNoArgs(const AExe: string;
|
|
|
|
|
out AStdout: string): Integer;
|
|
|
|
|
function CompileAndRun(const ASrc: string;
|
|
|
|
|
out AStdout: string; out AExitCode: Integer): Boolean;
|
|
|
|
|
protected
|
|
|
|
|
procedure SetUp; override;
|
|
|
|
|
published
|
|
|
|
|
procedure TestRun_HelloWorld;
|
|
|
|
|
procedure TestRun_StringOps;
|
|
|
|
|
procedure TestRun_ExceptionHandling;
|
|
|
|
|
procedure TestRun_ClassAndVirtual;
|
2026-06-24 03:12:07 +03:00
|
|
|
procedure TestLink_MissingRTL_FailsLoudly;
|
2026-06-19 12:48:04 +03:00
|
|
|
end;
|
|
|
|
|
|
2026-06-12 12:10:21 +03:00
|
|
|
implementation
|
|
|
|
|
|
|
|
|
|
function TElfReaderTests.ProjectRoot: string;
|
|
|
|
|
var
|
|
|
|
|
Dir, Parent: string;
|
|
|
|
|
Steps: Integer;
|
|
|
|
|
begin
|
|
|
|
|
Result := GetEnvironmentVariable('BLAISE_PROJECT_ROOT');
|
|
|
|
|
if Result <> '' then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Result);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Dir := GetCurrentDir();
|
|
|
|
|
for Steps := 0 to 5 do
|
|
|
|
|
begin
|
|
|
|
|
if DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'vendor/qbe') and
|
|
|
|
|
DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'runtime') then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Dir);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Parent := ExtractFileDir(Dir);
|
|
|
|
|
if (Parent = '') or (Parent = Dir) then Break;
|
|
|
|
|
Dir := Parent;
|
|
|
|
|
end;
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(GetCurrentDir());
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TElfReaderTests.PadField(const AVal: string; AWidth: Integer): string;
|
|
|
|
|
begin
|
|
|
|
|
Result := AVal;
|
|
|
|
|
while Length(Result) < AWidth do
|
|
|
|
|
Result := Result + ' ';
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestParse_TextSectionBytes;
|
|
|
|
|
var
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
Sec: TRdSection;
|
|
|
|
|
begin
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(
|
|
|
|
|
'movq %rcx, (%rax)' + LineEnding + 'ret' + LineEnding), 'test.o');
|
|
|
|
|
try
|
|
|
|
|
Sec := Obj.FindSection('.text');
|
|
|
|
|
AssertTrue('.text section missing', Sec <> nil);
|
|
|
|
|
AssertEquals(4, Integer(Sec.Size));
|
|
|
|
|
AssertEquals(Chr($48) + Chr($89) + Chr($08) + Chr($C3), Sec.Data);
|
|
|
|
|
AssertEquals(SHT_PROGBITS, Sec.ShType);
|
|
|
|
|
AssertTrue('SHF_EXECINSTR missing',
|
|
|
|
|
(Sec.Flags and SHF_EXECINSTR) <> 0);
|
|
|
|
|
finally
|
|
|
|
|
Obj.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestParse_GlobalFuncSymbol;
|
|
|
|
|
var
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
I: Integer;
|
|
|
|
|
Sym: TRdSymbol;
|
|
|
|
|
Found: TRdSymbol;
|
|
|
|
|
begin
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(
|
|
|
|
|
'.globl entry' + LineEnding +
|
|
|
|
|
'.type entry, @function' + LineEnding +
|
|
|
|
|
'entry:' + LineEnding + 'ret' + LineEnding), 'test.o');
|
|
|
|
|
try
|
|
|
|
|
Found := nil;
|
|
|
|
|
for I := 0 to Obj.Symbols.Count - 1 do
|
|
|
|
|
begin
|
|
|
|
|
Sym := Obj.Symbols.Get(I);
|
|
|
|
|
if Sym.Name = 'entry' then Found := Sym;
|
|
|
|
|
end;
|
|
|
|
|
AssertTrue('symbol entry missing', Found <> nil);
|
|
|
|
|
AssertEquals(STB_GLOBAL, Found.Bind);
|
|
|
|
|
AssertEquals(STT_FUNC, Found.SymType);
|
|
|
|
|
AssertEquals(Obj.SectionIndexOf('.text'), Found.Shndx);
|
|
|
|
|
AssertEquals(0, Integer(Found.Value));
|
|
|
|
|
finally
|
|
|
|
|
Obj.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestParse_QuadRelocation;
|
|
|
|
|
var
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
Rel: TRdReloc;
|
|
|
|
|
begin
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(
|
|
|
|
|
'.data' + LineEnding +
|
|
|
|
|
'vt:' + LineEnding +
|
|
|
|
|
'.quad some_method + 16' + LineEnding), 'test.o');
|
|
|
|
|
try
|
|
|
|
|
AssertEquals(1, Obj.Relocs.Count);
|
|
|
|
|
Rel := Obj.Relocs.Get(0);
|
|
|
|
|
AssertEquals(R_X86_64_64, Rel.RelocType);
|
|
|
|
|
AssertEquals(Obj.SectionIndexOf('.data'), Rel.TargetSection);
|
|
|
|
|
AssertEquals(0, Integer(Rel.Offset));
|
|
|
|
|
AssertEquals(16, Integer(Rel.Addend));
|
|
|
|
|
AssertEquals('some_method', Obj.Symbols.Get(Rel.SymIndex).Name);
|
|
|
|
|
finally
|
|
|
|
|
Obj.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestParse_BssNoData;
|
|
|
|
|
var
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
Sec: TRdSection;
|
|
|
|
|
begin
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(
|
|
|
|
|
'.section .bss' + LineEnding +
|
|
|
|
|
'buf:' + LineEnding +
|
|
|
|
|
'.skip 64' + LineEnding), 'test.o');
|
|
|
|
|
try
|
|
|
|
|
Sec := Obj.FindSection('.bss');
|
|
|
|
|
AssertTrue('.bss section missing', Sec <> nil);
|
|
|
|
|
AssertEquals(SHT_NOBITS, Sec.ShType);
|
|
|
|
|
AssertEquals(64, Integer(Sec.Size));
|
|
|
|
|
AssertEquals(0, Length(Sec.Data));
|
|
|
|
|
finally
|
|
|
|
|
Obj.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestParse_BadMagic_Raises;
|
|
|
|
|
var
|
|
|
|
|
Raised: Boolean;
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
begin
|
|
|
|
|
Raised := False;
|
|
|
|
|
try
|
|
|
|
|
Obj := ParseElfObject('this is definitely not an ELF file, '
|
|
|
|
|
+ 'but it is at least 64 bytes long for the header check....', 'junk');
|
|
|
|
|
Obj.Free();
|
|
|
|
|
except
|
|
|
|
|
on E: EElfReader do
|
|
|
|
|
Raised := True;
|
|
|
|
|
end;
|
|
|
|
|
AssertTrue('bad magic must raise EElfReader', Raised);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestArchive_SyntheticLongNames;
|
|
|
|
|
var
|
|
|
|
|
LongTab: string;
|
|
|
|
|
Ar: string;
|
|
|
|
|
Members: TList<TArchiveMember>;
|
|
|
|
|
I: Integer;
|
|
|
|
|
begin
|
|
|
|
|
{ Two members: one via the GNU long-name table, one short-named.
|
|
|
|
|
Member data is arbitrary bytes — ParseArchive does not interpret
|
|
|
|
|
member contents. }
|
|
|
|
|
LongTab := 'a_very_long_member_name_indeed.o/' + #10;
|
|
|
|
|
Ar := '!<arch>' + #10
|
|
|
|
|
+ PadField('//', 16) + PadField('', 12) + PadField('', 6)
|
|
|
|
|
+ PadField('', 6) + PadField('', 8)
|
|
|
|
|
+ PadField(IntToStr(Length(LongTab)), 10) + '`' + #10
|
|
|
|
|
+ LongTab { 34 bytes — already even, no pad }
|
|
|
|
|
+ PadField('/0', 16) + PadField('0', 12) + PadField('0', 6)
|
|
|
|
|
+ PadField('0', 6) + PadField('644', 8)
|
|
|
|
|
+ PadField('5', 10) + '`' + #10
|
|
|
|
|
+ 'HELLO' + #10 { pad to even }
|
|
|
|
|
+ PadField('short.o/', 16) + PadField('0', 12) + PadField('0', 6)
|
|
|
|
|
+ PadField('0', 6) + PadField('644', 8)
|
|
|
|
|
+ PadField('4', 10) + '`' + #10
|
|
|
|
|
+ 'DATA';
|
|
|
|
|
Members := TList<TArchiveMember>.Create();
|
|
|
|
|
try
|
|
|
|
|
ParseArchive(Ar, 'synthetic.a', Members);
|
|
|
|
|
AssertEquals(2, Members.Count);
|
|
|
|
|
AssertEquals('a_very_long_member_name_indeed.o', Members.Get(0).Name);
|
|
|
|
|
AssertEquals('HELLO', Members.Get(0).Data);
|
|
|
|
|
AssertEquals('short.o', Members.Get(1).Name);
|
|
|
|
|
AssertEquals('DATA', Members.Get(1).Data);
|
|
|
|
|
finally
|
|
|
|
|
for I := 0 to Members.Count - 1 do
|
|
|
|
|
Members.Get(I).Free();
|
|
|
|
|
Members.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestArchive_BadMagic_Raises;
|
|
|
|
|
var
|
|
|
|
|
Raised: Boolean;
|
|
|
|
|
Members: TList<TArchiveMember>;
|
|
|
|
|
begin
|
|
|
|
|
Raised := False;
|
|
|
|
|
Members := TList<TArchiveMember>.Create();
|
|
|
|
|
try
|
|
|
|
|
try
|
|
|
|
|
ParseArchive('!<arch !<arch !<arch', 'junk.a', Members);
|
|
|
|
|
except
|
|
|
|
|
on E: EElfReader do
|
|
|
|
|
Raised := True;
|
|
|
|
|
end;
|
|
|
|
|
finally
|
|
|
|
|
Members.Free();
|
|
|
|
|
end;
|
|
|
|
|
AssertTrue('bad archive magic must raise EElfReader', Raised);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TElfReaderTests.TestArchive_ParsesRTL;
|
|
|
|
|
var
|
2026-07-04 21:44:54 +03:00
|
|
|
RTLPath: string;
|
2026-06-12 12:10:21 +03:00
|
|
|
Members: TList<TArchiveMember>;
|
|
|
|
|
I: Integer;
|
|
|
|
|
M: TArchiveMember;
|
|
|
|
|
Obj: TElfObjectFile;
|
2026-07-04 21:44:54 +03:00
|
|
|
SawSetjmp: Boolean;
|
2026-06-12 12:10:21 +03:00
|
|
|
begin
|
2026-07-04 21:44:54 +03:00
|
|
|
RTLPath := ProjectRoot() + 'compiler/target/blaise_rtl.a';
|
|
|
|
|
if not FileExists(RTLPath) then
|
2026-06-12 12:10:21 +03:00
|
|
|
begin
|
|
|
|
|
Ignore('<toolchain-missing>');
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Members := TList<TArchiveMember>.Create();
|
|
|
|
|
try
|
2026-07-04 21:44:54 +03:00
|
|
|
ReadArchiveFile(RTLPath, Members);
|
2026-06-12 12:10:21 +03:00
|
|
|
AssertTrue('expected several RTL members, got '
|
|
|
|
|
+ IntToStr(Members.Count), Members.Count >= 5);
|
2026-07-04 21:44:54 +03:00
|
|
|
SawSetjmp := False;
|
2026-06-12 12:10:21 +03:00
|
|
|
for I := 0 to Members.Count - 1 do
|
|
|
|
|
begin
|
|
|
|
|
M := Members.Get(I);
|
2026-07-04 21:44:54 +03:00
|
|
|
if M.Name = 'blaise_setjmp_x86_64.o' then
|
|
|
|
|
SawSetjmp := True;
|
2026-06-12 12:10:21 +03:00
|
|
|
{ Every member must parse as a valid x86-64 relocatable object
|
|
|
|
|
with at least its NULL section + one real section. }
|
|
|
|
|
Obj := ParseElfObject(M.Data, M.Name);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue(M.Name + ': too few sections', Obj.Sections.Count > 1);
|
|
|
|
|
AssertTrue(M.Name + ': no symbols', Obj.Symbols.Count > 0);
|
|
|
|
|
finally
|
|
|
|
|
Obj.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
2026-07-04 21:44:54 +03:00
|
|
|
AssertTrue('long-named member blaise_setjmp_x86_64.o not found '
|
|
|
|
|
+ '(GNU long-name table mishandled?)', SawSetjmp);
|
2026-06-12 12:10:21 +03:00
|
|
|
finally
|
|
|
|
|
for I := 0 to Members.Count - 1 do
|
|
|
|
|
Members.Get(I).Free();
|
|
|
|
|
Members.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
2026-06-12 12:13:16 +03:00
|
|
|
{ ---- TSectionMergerTests ---- }
|
|
|
|
|
|
|
|
|
|
procedure TSectionMergerTests.TestMerge_ConcatenatesText;
|
|
|
|
|
var
|
|
|
|
|
O1, O2: TElfObjectFile;
|
|
|
|
|
Mg: TSectionMerger;
|
|
|
|
|
M: TMergedSection;
|
|
|
|
|
P: TSectionPlacement;
|
|
|
|
|
begin
|
|
|
|
|
O1 := ParseElfObject(AssembleToBytes('ret' + LineEnding), 'a.o');
|
|
|
|
|
O2 := ParseElfObject(AssembleToBytes('nop' + LineEnding
|
|
|
|
|
+ 'ret' + LineEnding), 'b.o');
|
|
|
|
|
Mg := TSectionMerger.Create();
|
|
|
|
|
try
|
|
|
|
|
Mg.AddObject(0, O1);
|
|
|
|
|
Mg.AddObject(1, O2);
|
|
|
|
|
M := Mg.FindMerged('.text');
|
|
|
|
|
AssertTrue('.text missing', M <> nil);
|
|
|
|
|
AssertEquals(Chr($C3) + Chr($90) + Chr($C3), M.Data);
|
|
|
|
|
AssertEquals(3, Integer(M.Size));
|
|
|
|
|
P := Mg.PlacementOf(0, O1.SectionIndexOf('.text'));
|
|
|
|
|
AssertTrue('placement 0 missing', P <> nil);
|
|
|
|
|
AssertEquals(0, Integer(P.Offset));
|
|
|
|
|
P := Mg.PlacementOf(1, O2.SectionIndexOf('.text'));
|
|
|
|
|
AssertTrue('placement 1 missing', P <> nil);
|
|
|
|
|
AssertEquals(1, Integer(P.Offset));
|
|
|
|
|
finally
|
|
|
|
|
Mg.Free();
|
|
|
|
|
O2.Free();
|
|
|
|
|
O1.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TSectionMergerTests.TestMerge_AlignmentPadding;
|
|
|
|
|
var
|
|
|
|
|
O1, O2: TElfObjectFile;
|
|
|
|
|
Mg: TSectionMerger;
|
|
|
|
|
M: TMergedSection;
|
|
|
|
|
P: TSectionPlacement;
|
|
|
|
|
begin
|
|
|
|
|
{ First object contributes 1 byte of .data; the second declares
|
|
|
|
|
.balign 8, so its contribution must start at offset 8 with zero
|
|
|
|
|
padding in between. }
|
|
|
|
|
O1 := ParseElfObject(AssembleToBytes('.data' + LineEnding
|
|
|
|
|
+ '.byte 17' + LineEnding), 'a.o');
|
|
|
|
|
O2 := ParseElfObject(AssembleToBytes('.data' + LineEnding
|
|
|
|
|
+ '.balign 8' + LineEnding + '.byte 34' + LineEnding), 'b.o');
|
|
|
|
|
Mg := TSectionMerger.Create();
|
|
|
|
|
try
|
|
|
|
|
Mg.AddObject(0, O1);
|
|
|
|
|
Mg.AddObject(1, O2);
|
|
|
|
|
M := Mg.FindMerged('.data');
|
|
|
|
|
AssertTrue('.data missing', M <> nil);
|
|
|
|
|
AssertEquals(9, Integer(M.Size));
|
|
|
|
|
AssertEquals(8, Integer(M.Align));
|
|
|
|
|
AssertEquals(17, Ord(M.Data[0]));
|
|
|
|
|
AssertEquals(0, Ord(M.Data[1]));
|
|
|
|
|
AssertEquals(34, Ord(M.Data[8]));
|
|
|
|
|
P := Mg.PlacementOf(1, O2.SectionIndexOf('.data'));
|
|
|
|
|
AssertTrue('placement missing', P <> nil);
|
|
|
|
|
AssertEquals(8, Integer(P.Offset));
|
|
|
|
|
finally
|
|
|
|
|
Mg.Free();
|
|
|
|
|
O2.Free();
|
|
|
|
|
O1.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TSectionMergerTests.TestMerge_BssSizesAccumulate;
|
|
|
|
|
var
|
|
|
|
|
O1, O2: TElfObjectFile;
|
|
|
|
|
Mg: TSectionMerger;
|
|
|
|
|
M: TMergedSection;
|
|
|
|
|
begin
|
|
|
|
|
O1 := ParseElfObject(AssembleToBytes('.section .bss' + LineEnding
|
|
|
|
|
+ '.skip 24' + LineEnding), 'a.o');
|
|
|
|
|
O2 := ParseElfObject(AssembleToBytes('.section .bss' + LineEnding
|
|
|
|
|
+ '.skip 40' + LineEnding), 'b.o');
|
|
|
|
|
Mg := TSectionMerger.Create();
|
|
|
|
|
try
|
|
|
|
|
Mg.AddObject(0, O1);
|
|
|
|
|
Mg.AddObject(1, O2);
|
|
|
|
|
M := Mg.FindMerged('.bss');
|
|
|
|
|
AssertTrue('.bss missing', M <> nil);
|
|
|
|
|
AssertEquals(SHT_NOBITS, M.ShType);
|
|
|
|
|
AssertEquals(64, Integer(M.Size));
|
|
|
|
|
AssertEquals(0, Length(M.Data));
|
|
|
|
|
finally
|
|
|
|
|
Mg.Free();
|
|
|
|
|
O2.Free();
|
|
|
|
|
O1.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TSectionMergerTests.TestMerge_SkipsBookkeepingSections;
|
|
|
|
|
var
|
|
|
|
|
O1: TElfObjectFile;
|
|
|
|
|
Mg: TSectionMerger;
|
|
|
|
|
begin
|
|
|
|
|
O1 := ParseElfObject(AssembleToBytes('ret' + LineEnding), 'a.o');
|
|
|
|
|
Mg := TSectionMerger.Create();
|
|
|
|
|
try
|
|
|
|
|
Mg.AddObject(0, O1);
|
|
|
|
|
AssertTrue('.symtab must not merge', Mg.FindMerged('.symtab') = nil);
|
|
|
|
|
AssertTrue('.strtab must not merge', Mg.FindMerged('.strtab') = nil);
|
|
|
|
|
AssertTrue('.shstrtab must not merge', Mg.FindMerged('.shstrtab') = nil);
|
|
|
|
|
AssertTrue('.note.GNU-stack must not merge',
|
|
|
|
|
Mg.FindMerged('.note.GNU-stack') = nil);
|
|
|
|
|
finally
|
|
|
|
|
Mg.Free();
|
|
|
|
|
O1.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
{ ---- TLinkerTests ---- }
|
|
|
|
|
|
|
|
|
|
{ Assemble each asm string to a relocatable object, hand them all to a
|
|
|
|
|
fresh TLinker (which takes ownership), link to bytes, and return the
|
|
|
|
|
linker so the caller can query resolved addresses. Caller frees. }
|
|
|
|
|
function TLinkerTests.LinkObjs(AObjAsm: array of string;
|
|
|
|
|
const AEntry: string; out ABytes: string): TLinker;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
I: Integer;
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
begin
|
|
|
|
|
Lk := TLinker.Create();
|
|
|
|
|
try
|
|
|
|
|
for I := 0 to High(AObjAsm) do
|
|
|
|
|
begin
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(AObjAsm[I]),
|
|
|
|
|
'obj' + IntToStr(I) + '.o');
|
|
|
|
|
Lk.AddOwnedObject(Obj);
|
|
|
|
|
end;
|
|
|
|
|
ABytes := Lk.LinkToBytes(AEntry);
|
|
|
|
|
Result := Lk;
|
|
|
|
|
except
|
|
|
|
|
Lk.Free();
|
|
|
|
|
raise;
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerTests.TestSym_GlobalResolvesToVaddr;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
Addr: Int64;
|
|
|
|
|
begin
|
|
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'_start', Bytes);
|
|
|
|
|
try
|
|
|
|
|
Addr := Lk.AddrOfSymbol('_start');
|
|
|
|
|
{ _start lands in the executable run just past ELF + 2 phdrs at base
|
|
|
|
|
0x400000; its exact value is layout-dependent but must be a real
|
|
|
|
|
mapped code address above the base. }
|
|
|
|
|
AssertTrue('_start resolved', Addr > $400000);
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
feat(linker): wire internal linker into compiler driver + fix TLS relocations
Add --linker internal|external option to the native backend driver,
routing the link step through TLinker instead of shelling out to cc.
FindCrtObjects discovers CRT startup objects (Scrt1.o, crti.o, crtn.o,
crtbeginS.o, crtendS.o) on Debian and RedHat layouts.
Fix SHF_TLS constant from $200 to the correct ELF spec value $400 in
both the reader and writer — the wrong value caused .tbss sections to
be placed twice (once in the normal NOBITS loop, once in the TLS
section), producing doubled negative TPOFF32 offsets and segfaults.
Fix FTlsAddr to use the post-alignment address from PlaceSection rather
than the pre-alignment cursor, which caused off-by-padding TPOFF values.
Change duplicate global symbol handling from error to first-wins
semantics (matching ld behaviour), needed because the main program
object and archive members may both define system runtime functions.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3532 tests pass on both QBE-built and native-built test runners.
2026-06-19 12:43:33 +03:00
|
|
|
procedure TLinkerTests.TestSym_TwoGlobalsDuplicate_FirstWins;
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
begin
|
feat(linker): wire internal linker into compiler driver + fix TLS relocations
Add --linker internal|external option to the native backend driver,
routing the link step through TLinker instead of shelling out to cc.
FindCrtObjects discovers CRT startup objects (Scrt1.o, crti.o, crtn.o,
crtbeginS.o, crtendS.o) on Debian and RedHat layouts.
Fix SHF_TLS constant from $200 to the correct ELF spec value $400 in
both the reader and writer — the wrong value caused .tbss sections to
be placed twice (once in the normal NOBITS loop, once in the TLS
section), producing doubled negative TPOFF32 offsets and segfaults.
Fix FTlsAddr to use the post-alignment address from PlaceSection rather
than the pre-alignment cursor, which caused off-by-padding TPOFF values.
Change duplicate global symbol handling from error to first-wins
semantics (matching ld behaviour), needed because the main program
object and archive members may both define system runtime functions.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3532 tests pass on both QBE-built and native-built test runners.
2026-06-19 12:43:33 +03:00
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.globl dup' + LineEnding + 'dup:' + LineEnding + 'ret' + LineEnding,
|
|
|
|
|
'.globl dup' + LineEnding + 'dup:' + LineEnding + 'ret' + LineEnding +
|
|
|
|
|
'.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'_start', Bytes);
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
Lk.Free();
|
feat(linker): wire internal linker into compiler driver + fix TLS relocations
Add --linker internal|external option to the native backend driver,
routing the link step through TLinker instead of shelling out to cc.
FindCrtObjects discovers CRT startup objects (Scrt1.o, crti.o, crtn.o,
crtbeginS.o, crtendS.o) on Debian and RedHat layouts.
Fix SHF_TLS constant from $200 to the correct ELF spec value $400 in
both the reader and writer — the wrong value caused .tbss sections to
be placed twice (once in the normal NOBITS loop, once in the TLS
section), producing doubled negative TPOFF32 offsets and segfaults.
Fix FTlsAddr to use the post-alignment address from PlaceSection rather
than the pre-alignment cursor, which caused off-by-padding TPOFF values.
Change duplicate global symbol handling from error to first-wins
semantics (matching ld behaviour), needed because the main program
object and archive members may both define system runtime functions.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3532 tests pass on both QBE-built and native-built test runners.
2026-06-19 12:43:33 +03:00
|
|
|
AssertTrue('first-wins duplicate linking must succeed', True);
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerTests.TestSym_StrongUndefined_Raises;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
Raised: Boolean;
|
|
|
|
|
begin
|
|
|
|
|
Raised := False;
|
|
|
|
|
Lk := nil;
|
|
|
|
|
try
|
|
|
|
|
{ _start calls an undefined strong symbol. }
|
|
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.globl _start' + LineEnding + '_start:' + LineEnding +
|
|
|
|
|
'callq missing_fn' + LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'_start', Bytes);
|
|
|
|
|
except
|
|
|
|
|
on E: ELinker do
|
|
|
|
|
Raised := True;
|
|
|
|
|
end;
|
|
|
|
|
Lk.Free();
|
|
|
|
|
AssertTrue('strong undefined reference must raise ELinker', Raised);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerTests.TestSym_WeakUndefinedResolvesToZero;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
Sec: TRdSection;
|
|
|
|
|
SymNull, SymStart, SymWeak: TRdSymbol;
|
|
|
|
|
Rel: TRdReloc;
|
|
|
|
|
TextSec: TMergedSection;
|
|
|
|
|
StartAddr: Int64;
|
|
|
|
|
PatchOff, Disp: Integer;
|
|
|
|
|
Expected: Int64;
|
|
|
|
|
begin
|
|
|
|
|
{ The internal assembler always emits an undefined reference as
|
|
|
|
|
STB_GLOBAL (no STB_WEAK support yet), so weak handling is exercised
|
|
|
|
|
with a hand-built object: a .text holding `E8 00 00 00 00` (a call
|
|
|
|
|
with a zero displacement) plus a PC32 reloc against a weak-undef
|
|
|
|
|
symbol. A strong undef would raise; the weak one must resolve to 0,
|
|
|
|
|
giving disp = 0 + (-4) - P. }
|
|
|
|
|
Obj := TElfObjectFile.Create();
|
|
|
|
|
Obj.SourceName := 'weak.o';
|
|
|
|
|
{ Section 0 is the reserved ELF NULL section, mirroring real objects
|
|
|
|
|
(so a symbol's Shndx=0 means SHN_UNDEF, not "the first section"). }
|
|
|
|
|
Sec := TRdSection.Create();
|
|
|
|
|
Sec.Name := '';
|
|
|
|
|
Sec.ShType := SHT_NULL;
|
|
|
|
|
Obj.Sections.Add(Sec); { section index 0 = NULL }
|
|
|
|
|
|
|
|
|
|
Sec := TRdSection.Create();
|
|
|
|
|
Sec.Name := '.text';
|
|
|
|
|
Sec.ShType := SHT_PROGBITS;
|
|
|
|
|
Sec.Flags := SHF_ALLOC or SHF_EXECINSTR;
|
|
|
|
|
Sec.AddrAlign := 1;
|
|
|
|
|
Sec.Data := Chr($E8) + Chr(0) + Chr(0) + Chr(0) + Chr(0) + Chr($C3);
|
|
|
|
|
Sec.Size := 6;
|
|
|
|
|
Obj.Sections.Add(Sec); { section index 1 = .text }
|
|
|
|
|
|
|
|
|
|
SymNull := TRdSymbol.Create();
|
|
|
|
|
SymNull.Name := '';
|
|
|
|
|
Obj.Symbols.Add(SymNull); { symtab[0] reserved }
|
|
|
|
|
|
|
|
|
|
SymStart := TRdSymbol.Create();
|
|
|
|
|
SymStart.Name := '_start';
|
|
|
|
|
SymStart.Bind := STB_GLOBAL;
|
|
|
|
|
SymStart.SymType := STT_FUNC;
|
|
|
|
|
SymStart.Shndx := 1; { defined in .text }
|
|
|
|
|
SymStart.Value := 0;
|
|
|
|
|
Obj.Symbols.Add(SymStart); { symtab[1] }
|
|
|
|
|
|
|
|
|
|
SymWeak := TRdSymbol.Create();
|
|
|
|
|
SymWeak.Name := 'maybe_absent';
|
|
|
|
|
SymWeak.Bind := STB_WEAK;
|
|
|
|
|
SymWeak.Shndx := SHN_UNDEF;
|
|
|
|
|
Obj.Symbols.Add(SymWeak); { symtab[2] }
|
|
|
|
|
|
|
|
|
|
Rel := TRdReloc.Create();
|
|
|
|
|
Rel.TargetSection := 1; { patches .text (section 1) }
|
|
|
|
|
Rel.Offset := 1; { displacement after 0xE8 }
|
|
|
|
|
Rel.SymIndex := 2; { -> maybe_absent }
|
|
|
|
|
Rel.RelocType := R_X86_64_PC32;
|
|
|
|
|
Rel.Addend := -4;
|
|
|
|
|
Obj.Relocs.Add(Rel);
|
|
|
|
|
|
|
|
|
|
Lk := TLinker.Create();
|
|
|
|
|
try
|
|
|
|
|
Lk.AddOwnedObject(Obj);
|
|
|
|
|
Lk.LinkToBytes('_start'); { must NOT raise }
|
|
|
|
|
StartAddr := Lk.AddrOfSymbol('_start');
|
|
|
|
|
TextSec := Lk.FindMergedText();
|
|
|
|
|
PatchOff := 1; { only object, .text offset 1 }
|
|
|
|
|
Disp := (Ord(TextSec.Data[PatchOff]) and $FF)
|
|
|
|
|
or ((Ord(TextSec.Data[PatchOff + 1]) and $FF) shl 8)
|
|
|
|
|
or ((Ord(TextSec.Data[PatchOff + 2]) and $FF) shl 16)
|
|
|
|
|
or ((Ord(TextSec.Data[PatchOff + 3]) and $FF) shl 24);
|
|
|
|
|
{ S=0, A=-4, P = StartAddr+1 → disp = -4 - (StartAddr+1). }
|
|
|
|
|
Expected := Int64(0) - 4 - (StartAddr + 1);
|
|
|
|
|
AssertEquals('weak-undef PC32 disp (S=0)',
|
|
|
|
|
Integer(Expected and $FFFFFFFF), Disp);
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerTests.TestSym_SynthesisedSymbolsDefined;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
begin
|
|
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.data' + LineEnding + '.globl gv' + LineEnding + 'gv:' + LineEnding +
|
|
|
|
|
'.quad 7' + LineEnding +
|
|
|
|
|
'.text' + LineEnding + '.globl _start' + LineEnding + '_start:' +
|
|
|
|
|
LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'_start', Bytes);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('__bss_start defined', Lk.AddrOfSymbol('__bss_start') > 0);
|
|
|
|
|
AssertTrue('_edata defined', Lk.AddrOfSymbol('_edata') > 0);
|
|
|
|
|
AssertTrue('_end defined', Lk.AddrOfSymbol('_end') > 0);
|
|
|
|
|
AssertTrue('_GLOBAL_OFFSET_TABLE_ defined',
|
|
|
|
|
Lk.AddrOfSymbol('_GLOBAL_OFFSET_TABLE_') > 0);
|
|
|
|
|
{ _edata (end of .data) must not exceed _end (end of bss). }
|
|
|
|
|
AssertTrue('_edata <= _end',
|
|
|
|
|
Lk.AddrOfSymbol('_edata') <= Lk.AddrOfSymbol('_end'));
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerTests.TestReloc_PC32CrossObjectCall;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
TextSec: TMergedSection;
|
|
|
|
|
CalleeAddr, StartAddr: Int64;
|
|
|
|
|
CallSiteVaddr, CallEnd: Int64;
|
|
|
|
|
PatchOff: Integer;
|
|
|
|
|
Disp: Integer;
|
|
|
|
|
Expected: Int64;
|
|
|
|
|
begin
|
|
|
|
|
{ Object 0 defines callee; object 1's _start does `call callee`.
|
|
|
|
|
The 4-byte displacement after the 0xE8 opcode must equal
|
|
|
|
|
callee - (addr_of_displacement + 4). }
|
|
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.globl callee' + LineEnding + 'callee:' + LineEnding + 'ret' + LineEnding,
|
|
|
|
|
'.globl _start' + LineEnding + '_start:' + LineEnding +
|
|
|
|
|
'callq callee' + LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'_start', Bytes);
|
|
|
|
|
try
|
|
|
|
|
CalleeAddr := Lk.AddrOfSymbol('callee');
|
|
|
|
|
StartAddr := Lk.AddrOfSymbol('_start');
|
|
|
|
|
AssertTrue('callee resolved', CalleeAddr > 0);
|
|
|
|
|
AssertTrue('_start resolved', StartAddr > 0);
|
|
|
|
|
|
|
|
|
|
{ The call opcode (0xE8) is the first byte of _start; the 4-byte
|
|
|
|
|
relative displacement follows it. The patched bytes live in the
|
|
|
|
|
merged .text data at an offset relative to .text's own base —
|
|
|
|
|
callee is the first thing in .text, so its address is that base. }
|
|
|
|
|
TextSec := Lk.FindMergedText();
|
|
|
|
|
AssertTrue('.text present', TextSec <> nil);
|
|
|
|
|
CallSiteVaddr := StartAddr; { 0xE8 here }
|
|
|
|
|
CallEnd := CallSiteVaddr + 5; { next insn after the 5-byte call }
|
|
|
|
|
Expected := CalleeAddr - CallEnd;
|
|
|
|
|
|
|
|
|
|
PatchOff := Integer(StartAddr - CalleeAddr) + 1; { skip 0xE8 }
|
|
|
|
|
Disp := (Ord(TextSec.Data[PatchOff]) and $FF)
|
|
|
|
|
or ((Ord(TextSec.Data[PatchOff + 1]) and $FF) shl 8)
|
|
|
|
|
or ((Ord(TextSec.Data[PatchOff + 2]) and $FF) shl 16)
|
|
|
|
|
or ((Ord(TextSec.Data[PatchOff + 3]) and $FF) shl 24);
|
|
|
|
|
AssertEquals('PC32 call displacement', Integer(Expected and $FFFFFFFF), Disp);
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
2026-07-04 21:44:54 +03:00
|
|
|
procedure TLinkerTests.TestReloc_Quad64_Raises;
|
2026-07-02 19:44:49 +03:00
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
2026-07-04 21:44:54 +03:00
|
|
|
Raised: Boolean;
|
2026-07-02 19:44:49 +03:00
|
|
|
begin
|
2026-07-04 21:44:54 +03:00
|
|
|
{ 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;
|
2026-07-02 19:44:49 +03:00
|
|
|
try
|
2026-07-04 21:44:54 +03:00
|
|
|
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;
|
2026-07-02 19:44:49 +03:00
|
|
|
end;
|
2026-07-04 21:44:54 +03:00
|
|
|
Lk.Free();
|
|
|
|
|
AssertTrue('R_X86_64_64 must raise ELinker in Phase B', Raised);
|
2026-07-02 19:44:49 +03:00
|
|
|
end;
|
|
|
|
|
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
procedure TLinkerTests.TestExe_ElfHeaderIsExec;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
begin
|
|
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'_start', Bytes);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('output too small', Length(Bytes) >= 64);
|
|
|
|
|
AssertEquals('ELF magic 0', $7F, Ord(Bytes[0]));
|
|
|
|
|
AssertEquals('ELF magic E', Ord('E'), Ord(Bytes[1]));
|
|
|
|
|
AssertEquals('ELFCLASS64', 2, Ord(Bytes[4]));
|
|
|
|
|
AssertEquals('little-endian', 1, Ord(Bytes[5]));
|
|
|
|
|
{ e_type at offset 16 must be ET_EXEC (2). }
|
|
|
|
|
AssertEquals('e_type ET_EXEC', 2,
|
|
|
|
|
(Ord(Bytes[16]) and $FF) or ((Ord(Bytes[17]) and $FF) shl 8));
|
|
|
|
|
{ e_machine at offset 18 must be EM_X86_64 (62). }
|
|
|
|
|
AssertEquals('e_machine x86-64', 62,
|
|
|
|
|
(Ord(Bytes[18]) and $FF) or ((Ord(Bytes[19]) and $FF) shl 8));
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerTests.TestExe_EntryPointMatchesSymbol;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
Entry, I: Int64;
|
|
|
|
|
begin
|
|
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'_start', Bytes);
|
|
|
|
|
try
|
|
|
|
|
{ e_entry is an 8-byte LE field at offset 24. }
|
|
|
|
|
Entry := 0;
|
|
|
|
|
for I := 0 to 7 do
|
|
|
|
|
Entry := Entry or (Int64(Ord(Bytes[24 + Integer(I)]) and $FF) shl (I * 8));
|
|
|
|
|
AssertEquals('e_entry == addr(_start)', Lk.AddrOfSymbol('_start'), Entry);
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerTests.TestExe_MissingEntry_Raises;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
Raised: Boolean;
|
|
|
|
|
begin
|
|
|
|
|
Raised := False;
|
|
|
|
|
Lk := nil;
|
|
|
|
|
try
|
|
|
|
|
Lk := LinkObjs(
|
|
|
|
|
['.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
|
|
|
|
|
'no_such_entry', Bytes);
|
|
|
|
|
except
|
|
|
|
|
on E: ELinker do
|
|
|
|
|
Raised := True;
|
|
|
|
|
end;
|
|
|
|
|
Lk.Free();
|
|
|
|
|
AssertTrue('missing entry symbol must raise ELinker', Raised);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
{ ---- TLinkerE2ETests ---- }
|
|
|
|
|
|
|
|
|
|
function TLinkerE2ETests.ProjectRoot: string;
|
|
|
|
|
var
|
|
|
|
|
Dir, Parent: string;
|
|
|
|
|
Steps: Integer;
|
|
|
|
|
begin
|
|
|
|
|
Result := GetEnvironmentVariable('BLAISE_PROJECT_ROOT');
|
|
|
|
|
if Result <> '' then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Result);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Dir := GetCurrentDir();
|
|
|
|
|
for Steps := 0 to 5 do
|
|
|
|
|
begin
|
|
|
|
|
if DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'vendor/qbe') and
|
|
|
|
|
DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'runtime') then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Dir);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Parent := ExtractFileDir(Dir);
|
|
|
|
|
if (Parent = '') or (Parent = Dir) then Break;
|
|
|
|
|
Dir := Parent;
|
|
|
|
|
end;
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(GetCurrentDir());
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerE2ETests.SetUp;
|
|
|
|
|
begin
|
|
|
|
|
inherited SetUp();
|
|
|
|
|
FScratch := ProjectRoot() + 'compiler/target/linker-e2e';
|
|
|
|
|
ForceDirectories(FScratch);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TLinkerE2ETests.RunBin(const AExe: string;
|
|
|
|
|
out AStdout: string): Integer;
|
|
|
|
|
var
|
|
|
|
|
Proc: TProcess;
|
|
|
|
|
Chunk: string;
|
|
|
|
|
begin
|
|
|
|
|
Proc := TProcess.Create(nil);
|
|
|
|
|
try
|
|
|
|
|
Proc.Executable := AExe;
|
|
|
|
|
Proc.Execute();
|
|
|
|
|
AStdout := '';
|
|
|
|
|
repeat
|
|
|
|
|
Chunk := Proc.ReadOutput();
|
|
|
|
|
AStdout := AStdout + Chunk;
|
|
|
|
|
until (Chunk = '') and not Proc.Running;
|
|
|
|
|
Proc.WaitOnExit();
|
|
|
|
|
Result := Proc.ExitCode;
|
|
|
|
|
finally
|
|
|
|
|
Proc.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TLinkerE2ETests.TestRun_SyscallHelloWorld;
|
|
|
|
|
const
|
|
|
|
|
{ A freestanding program that talks straight to the kernel: it needs
|
|
|
|
|
no libc, no RTL, and no dynamic linker, so Phase B links and runs
|
|
|
|
|
it on its own. write(1, msg, 14); exit(7). The exit code (7)
|
|
|
|
|
plus the stdout both prove the executable loaded and ran with the
|
|
|
|
|
correct entry point, segment permissions, and a PC-relative
|
|
|
|
|
`leaq msg(%rip)` resolved against the merged .rodata. }
|
|
|
|
|
FixtureAsm =
|
|
|
|
|
'.text' + LineEnding +
|
|
|
|
|
'.globl _start' + LineEnding +
|
|
|
|
|
'_start:' + LineEnding +
|
|
|
|
|
' movq $1, %rax' + LineEnding + { SYS_write }
|
|
|
|
|
' movq $1, %rdi' + LineEnding + { fd = stdout }
|
|
|
|
|
' leaq msg(%rip), %rsi' + LineEnding + { buf (PC-relative) }
|
|
|
|
|
' movq $15, %rdx' + LineEnding + { count (14 chars + newline) }
|
|
|
|
|
' .byte 15' + LineEnding + ' .byte 5' + LineEnding + { syscall }
|
|
|
|
|
' movq $60, %rax' + LineEnding + { SYS_exit }
|
|
|
|
|
' movq $7, %rdi' + LineEnding + { exit code }
|
|
|
|
|
' .byte 15' + LineEnding + ' .byte 5' + LineEnding + { syscall }
|
|
|
|
|
'.section .rodata' + LineEnding +
|
|
|
|
|
'msg:' + LineEnding +
|
|
|
|
|
' .ascii "Hello, linker!\n"' + LineEnding;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
BinPath, Output: string;
|
|
|
|
|
Rc: Integer;
|
|
|
|
|
begin
|
|
|
|
|
BinPath := FScratch + '/hello_syscall';
|
|
|
|
|
Lk := TLinker.Create();
|
|
|
|
|
try
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(FixtureAsm), 'hello.o');
|
|
|
|
|
Lk.AddOwnedObject(Obj);
|
|
|
|
|
Lk.Link('_start', BinPath);
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
AssertTrue('linked binary missing', FileExists(BinPath));
|
|
|
|
|
Rc := RunBin(BinPath, Output);
|
|
|
|
|
AssertEquals('exit code from internally-linked binary', 7, Rc);
|
|
|
|
|
AssertEquals('stdout from internally-linked binary',
|
|
|
|
|
'Hello, linker!' + Chr(10), Output);
|
|
|
|
|
end;
|
|
|
|
|
|
feat(linker): Phase C dynamic linking — GOT/PLT, CRT objects, PIE emission
Implement Phase C of the internal ELF linker: produce position-independent
ET_DYN executables linked against libc through dynamic GOT/PLT dispatch.
New capabilities:
- SetDynamic(True) switches the linker to PIE mode
- AddCrtObject() loads system CRT startup objects (Scrt1.o, crti.o,
crtn.o, crtbeginS.o, crtendS.o)
- CollectExternals scans undefined non-weak symbols and creates PLT entries
- CollectGotSlots creates non-PLT GOT slots for GOTPCREL references to
locally-defined symbols (e.g. main, __dso_handle)
- BuildDynamic generates .interp, .dynstr, .dynsym, SysV .hash, .got
(3 reserved + N PLT + M non-PLT slots), .plt (header + stubs),
.rela.plt (JUMP_SLOT entries)
- LayoutDynamic assigns PIE virtual addresses (base 0): RO metadata →
.text/.plt → RELRO (.init_array, .fini_array, .dynamic, .got) →
.data/.bss, with page-aligned PT_LOAD boundaries
- EmitDynExecutable writes the full PIE byte image with ELF header,
10 program headers (PHDR, INTERP, 4×LOAD, DYNAMIC, TLS, GNU_STACK,
GNU_RELRO), and section data at virtual offsets
Relocation handling extended:
- R_X86_64_64 → R_X86_64_RELATIVE in .rela.dyn
- R_X86_64_32/32S for absolute symbol references
- R_X86_64_GOTPCREL/GOTPCRELX/REX_GOTPCRELX → GOT slot lookup
- R_X86_64_TPOFF32 → TLS offset calculation
Tests: TDynLinkerTests (2 structural), TDynLinkerE2ETests (1 E2E that
links a main() calling write via PLT, runs the binary, asserts exit
code 42 + correct stdout). All 3528 tests pass.
2026-06-19 12:00:08 +03:00
|
|
|
{ ---- TDynLinkerTests ---- }
|
|
|
|
|
|
|
|
|
|
function TDynLinkerTests.ProjectRoot: string;
|
|
|
|
|
var
|
|
|
|
|
Dir, Parent: string;
|
|
|
|
|
Steps: Integer;
|
|
|
|
|
begin
|
|
|
|
|
Result := GetEnvironmentVariable('BLAISE_PROJECT_ROOT');
|
|
|
|
|
if Result <> '' then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Result);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Dir := GetCurrentDir();
|
|
|
|
|
for Steps := 0 to 5 do
|
|
|
|
|
begin
|
|
|
|
|
if DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'vendor/qbe') and
|
|
|
|
|
DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'runtime') then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Dir);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Parent := ExtractFileDir(Dir);
|
|
|
|
|
if (Parent = '') or (Parent = Dir) then Break;
|
|
|
|
|
Dir := Parent;
|
|
|
|
|
end;
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(GetCurrentDir());
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TDynLinkerTests.TestDyn_CollectsExternals;
|
|
|
|
|
const
|
|
|
|
|
MainAsm =
|
|
|
|
|
'.text' + LineEnding +
|
|
|
|
|
'.globl main' + LineEnding +
|
|
|
|
|
'main:' + LineEnding +
|
|
|
|
|
' callq write' + LineEnding +
|
|
|
|
|
' xorl %eax, %eax' + LineEnding +
|
|
|
|
|
' ret' + LineEnding;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
begin
|
|
|
|
|
Lk := TLinker.Create();
|
|
|
|
|
try
|
|
|
|
|
Lk.SetDynamic(True);
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(MainAsm), 'main.o');
|
|
|
|
|
Lk.AddOwnedObject(Obj);
|
|
|
|
|
Bytes := Lk.LinkToBytes('main');
|
|
|
|
|
AssertTrue('output should be non-empty', Length(Bytes) > 64);
|
|
|
|
|
{ e_type at offset 16 must be ET_DYN (3). }
|
|
|
|
|
AssertEquals('e_type ET_DYN', 3,
|
|
|
|
|
(Ord(Bytes[16]) and $FF) or ((Ord(Bytes[17]) and $FF) shl 8));
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TDynLinkerTests.TestDyn_PieHeaderEmitted;
|
|
|
|
|
const
|
|
|
|
|
MainAsm =
|
|
|
|
|
'.text' + LineEnding +
|
|
|
|
|
'.globl main' + LineEnding +
|
|
|
|
|
'main:' + LineEnding +
|
|
|
|
|
' xorl %eax, %eax' + LineEnding +
|
|
|
|
|
' ret' + LineEnding;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
Bytes: string;
|
|
|
|
|
PhOff, PhCount, PhEntSz: Integer;
|
|
|
|
|
J, PType: Integer;
|
|
|
|
|
FoundInterp, FoundDynamic: Boolean;
|
|
|
|
|
begin
|
|
|
|
|
Lk := TLinker.Create();
|
|
|
|
|
try
|
|
|
|
|
Lk.SetDynamic(True);
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(MainAsm), 'main.o');
|
|
|
|
|
Lk.AddOwnedObject(Obj);
|
|
|
|
|
Bytes := Lk.LinkToBytes('main');
|
|
|
|
|
|
|
|
|
|
{ Parse e_phoff (offset 32, 8 bytes LE), e_phentsize (54, 2 bytes),
|
|
|
|
|
e_phnum (56, 2 bytes). }
|
|
|
|
|
PhOff := Ord(Bytes[32]) or (Ord(Bytes[33]) shl 8) or
|
|
|
|
|
(Ord(Bytes[34]) shl 16) or (Ord(Bytes[35]) shl 24);
|
|
|
|
|
PhEntSz := Ord(Bytes[54]) or (Ord(Bytes[55]) shl 8);
|
|
|
|
|
PhCount := Ord(Bytes[56]) or (Ord(Bytes[57]) shl 8);
|
|
|
|
|
AssertTrue('should have at least 4 phdrs', PhCount >= 4);
|
|
|
|
|
AssertEquals('phdr entry size', 56, PhEntSz);
|
|
|
|
|
|
|
|
|
|
FoundInterp := False;
|
|
|
|
|
FoundDynamic := False;
|
|
|
|
|
for J := 0 to PhCount - 1 do
|
|
|
|
|
begin
|
|
|
|
|
PType := Ord(Bytes[PhOff + J * PhEntSz]) or
|
|
|
|
|
(Ord(Bytes[PhOff + J * PhEntSz + 1]) shl 8) or
|
|
|
|
|
(Ord(Bytes[PhOff + J * PhEntSz + 2]) shl 16) or
|
|
|
|
|
(Ord(Bytes[PhOff + J * PhEntSz + 3]) shl 24);
|
|
|
|
|
if PType = 3 then FoundInterp := True; { PT_INTERP }
|
|
|
|
|
if PType = 2 then FoundDynamic := True; { PT_DYNAMIC }
|
|
|
|
|
end;
|
|
|
|
|
AssertTrue('PT_INTERP present', FoundInterp);
|
|
|
|
|
AssertTrue('PT_DYNAMIC present', FoundDynamic);
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
{ ---- TDynLinkerE2ETests ---- }
|
|
|
|
|
|
|
|
|
|
function TDynLinkerE2ETests.ProjectRoot: string;
|
|
|
|
|
var
|
|
|
|
|
Dir, Parent: string;
|
|
|
|
|
Steps: Integer;
|
|
|
|
|
begin
|
|
|
|
|
Result := GetEnvironmentVariable('BLAISE_PROJECT_ROOT');
|
|
|
|
|
if Result <> '' then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Result);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Dir := GetCurrentDir();
|
|
|
|
|
for Steps := 0 to 5 do
|
|
|
|
|
begin
|
|
|
|
|
if DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'vendor/qbe') and
|
|
|
|
|
DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'runtime') then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Dir);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Parent := ExtractFileDir(Dir);
|
|
|
|
|
if (Parent = '') or (Parent = Dir) then Break;
|
|
|
|
|
Dir := Parent;
|
|
|
|
|
end;
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(GetCurrentDir());
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TDynLinkerE2ETests.SetUp;
|
|
|
|
|
begin
|
|
|
|
|
inherited SetUp();
|
|
|
|
|
FScratch := ProjectRoot() + 'compiler/target/linker-e2e';
|
|
|
|
|
ForceDirectories(FScratch);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TDynLinkerE2ETests.RunBin(const AExe: string;
|
|
|
|
|
out AStdout: string): Integer;
|
|
|
|
|
var
|
|
|
|
|
Proc: TProcess;
|
|
|
|
|
Chunk: string;
|
|
|
|
|
begin
|
|
|
|
|
Proc := TProcess.Create(nil);
|
|
|
|
|
try
|
|
|
|
|
Proc.Executable := AExe;
|
|
|
|
|
Proc.Execute();
|
|
|
|
|
AStdout := '';
|
|
|
|
|
repeat
|
|
|
|
|
Chunk := Proc.ReadOutput();
|
|
|
|
|
AStdout := AStdout + Chunk;
|
|
|
|
|
until (Chunk = '') and not Proc.Running;
|
|
|
|
|
Proc.WaitOnExit();
|
|
|
|
|
Result := Proc.ExitCode;
|
|
|
|
|
finally
|
|
|
|
|
Proc.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TDynLinkerE2ETests.TestRun_DynHelloWorld;
|
|
|
|
|
const
|
|
|
|
|
{ A minimal main() that calls write(1, msg, 15) via PLT and returns 42.
|
|
|
|
|
Scrt1.o provides _start which calls __libc_start_main(main,...). }
|
|
|
|
|
MainAsm =
|
|
|
|
|
'.text' + LineEnding +
|
|
|
|
|
'.globl main' + LineEnding +
|
|
|
|
|
'main:' + LineEnding +
|
|
|
|
|
' subq $8, %rsp' + LineEnding +
|
|
|
|
|
' movl $1, %edi' + LineEnding +
|
|
|
|
|
' leaq msg(%rip), %rsi' + LineEnding +
|
|
|
|
|
' movl $16, %edx' + LineEnding +
|
|
|
|
|
' callq write' + LineEnding +
|
|
|
|
|
' movl $42, %eax' + LineEnding +
|
|
|
|
|
' addq $8, %rsp' + LineEnding +
|
|
|
|
|
' ret' + LineEnding +
|
|
|
|
|
'.section .rodata' + LineEnding +
|
|
|
|
|
'msg:' + LineEnding +
|
|
|
|
|
' .ascii "Hello, dynlink!\n"' + LineEnding;
|
|
|
|
|
var
|
|
|
|
|
Lk: TLinker;
|
|
|
|
|
Obj: TElfObjectFile;
|
|
|
|
|
BinPath, Output: string;
|
|
|
|
|
Rc: Integer;
|
|
|
|
|
Crt1, Crti, Crtn, CrtBegin, CrtEnd: string;
|
|
|
|
|
begin
|
|
|
|
|
BinPath := FScratch + '/hello_dyn';
|
|
|
|
|
|
|
|
|
|
{ Locate CRT objects. }
|
|
|
|
|
Crt1 := '/usr/lib/x86_64-linux-gnu/Scrt1.o';
|
|
|
|
|
Crti := '/usr/lib/x86_64-linux-gnu/crti.o';
|
|
|
|
|
Crtn := '/usr/lib/x86_64-linux-gnu/crtn.o';
|
|
|
|
|
CrtBegin := '/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o';
|
|
|
|
|
CrtEnd := '/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o';
|
|
|
|
|
|
|
|
|
|
if not FileExists(Crt1) then
|
|
|
|
|
begin
|
|
|
|
|
{ Try GCC 14. }
|
|
|
|
|
CrtBegin := '/usr/lib/gcc/x86_64-linux-gnu/14/crtbeginS.o';
|
|
|
|
|
CrtEnd := '/usr/lib/gcc/x86_64-linux-gnu/14/crtendS.o';
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
if not FileExists(Crt1) or not FileExists(CrtBegin) then
|
|
|
|
|
Exit;
|
|
|
|
|
|
|
|
|
|
Lk := TLinker.Create();
|
|
|
|
|
try
|
|
|
|
|
Lk.SetDynamic(True);
|
|
|
|
|
Lk.AddCrtObject(Crt1);
|
|
|
|
|
Lk.AddCrtObject(Crti);
|
|
|
|
|
Lk.AddCrtObject(CrtBegin);
|
|
|
|
|
Obj := ParseElfObject(AssembleToBytes(MainAsm), 'main.o');
|
|
|
|
|
Lk.AddOwnedObject(Obj);
|
|
|
|
|
Lk.AddCrtObject(CrtEnd);
|
|
|
|
|
Lk.AddCrtObject(Crtn);
|
|
|
|
|
Lk.Link('_start', BinPath);
|
|
|
|
|
finally
|
|
|
|
|
Lk.Free();
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
AssertTrue('linked binary missing', FileExists(BinPath));
|
|
|
|
|
Rc := RunBin(BinPath, Output);
|
|
|
|
|
AssertEquals('exit code', 42, Rc);
|
|
|
|
|
AssertEquals('stdout', 'Hello, dynlink!' + Chr(10), Output);
|
|
|
|
|
end;
|
|
|
|
|
|
2026-06-19 12:48:04 +03:00
|
|
|
{ ---- TInternalLinkerE2ETests ---- }
|
|
|
|
|
|
2026-06-24 03:12:07 +03:00
|
|
|
{ symlink(2): point the isolated-dir compiler name at the real binary so
|
|
|
|
|
CompilerBinDir() resolves to the isolated dir (no blaise_rtl.a beside it),
|
|
|
|
|
without copying ~3 MB. Returns 0 on success. }
|
|
|
|
|
function _test_symlink(ATarget, ALinkPath: PChar): Integer;
|
|
|
|
|
external name 'symlink';
|
|
|
|
|
|
2026-06-19 12:48:04 +03:00
|
|
|
var
|
|
|
|
|
GIntLinkSkipNoted: Boolean = False;
|
|
|
|
|
|
|
|
|
|
function TInternalLinkerE2ETests.ProjectRoot: string;
|
|
|
|
|
var
|
|
|
|
|
Dir, Parent: string;
|
|
|
|
|
Steps: Integer;
|
|
|
|
|
begin
|
|
|
|
|
Result := GetEnvironmentVariable('BLAISE_PROJECT_ROOT');
|
|
|
|
|
if Result <> '' then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Result);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Dir := GetCurrentDir();
|
|
|
|
|
for Steps := 0 to 5 do
|
|
|
|
|
begin
|
|
|
|
|
if DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'vendor/qbe') and
|
|
|
|
|
DirectoryExists(IncludeTrailingPathDelimiter(Dir) + 'runtime') then
|
|
|
|
|
begin
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(Dir);
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
Parent := ExtractFileDir(Dir);
|
|
|
|
|
if (Parent = '') or (Parent = Dir) then Break;
|
|
|
|
|
Dir := Parent;
|
|
|
|
|
end;
|
|
|
|
|
Result := IncludeTrailingPathDelimiter(GetCurrentDir());
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TInternalLinkerE2ETests.SetUp;
|
|
|
|
|
begin
|
|
|
|
|
inherited SetUp();
|
|
|
|
|
FCompiler := ProjectRoot() + 'compiler/target/blaise';
|
2026-07-04 21:44:54 +03:00
|
|
|
FRTLPath := ProjectRoot() + 'runtime/src/main/pascal';
|
2026-06-19 12:48:04 +03:00
|
|
|
FStdlibPath := ProjectRoot() + 'stdlib/src/main/pascal';
|
|
|
|
|
FScratch := ProjectRoot() + 'compiler/target/intlink_scratch/';
|
|
|
|
|
ForceDirectories(FScratch);
|
|
|
|
|
FCounter := 0;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TInternalLinkerE2ETests.CompilerAvailable: Boolean;
|
|
|
|
|
begin
|
|
|
|
|
Result := FileExists(FCompiler) and
|
2026-07-04 21:44:54 +03:00
|
|
|
FileExists(ProjectRoot() + 'compiler/target/blaise_rtl.a');
|
2026-06-19 12:48:04 +03:00
|
|
|
if (not Result) and (not GIntLinkSkipNoted) then
|
|
|
|
|
begin
|
|
|
|
|
GIntLinkSkipNoted := True;
|
|
|
|
|
WriteLn(StdErr, 'note: TInternalLinkerE2ETests skipped — compiler binary "',
|
2026-07-04 21:44:54 +03:00
|
|
|
FCompiler, '" not found');
|
2026-06-19 12:48:04 +03:00
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TInternalLinkerE2ETests.RunProc(const AExe: string;
|
|
|
|
|
AArgs: array of string; out AStdout: string): Integer;
|
|
|
|
|
var
|
|
|
|
|
Proc: TProcess;
|
|
|
|
|
I: Integer;
|
|
|
|
|
Chunk: string;
|
|
|
|
|
begin
|
|
|
|
|
Proc := TProcess.Create(nil);
|
|
|
|
|
try
|
|
|
|
|
Proc.Executable := AExe;
|
|
|
|
|
for I := 0 to High(AArgs) do
|
|
|
|
|
Proc.Parameters.Add(AArgs[I]);
|
|
|
|
|
Proc.Execute();
|
|
|
|
|
AStdout := '';
|
|
|
|
|
repeat
|
|
|
|
|
Chunk := Proc.ReadOutput();
|
|
|
|
|
AStdout := AStdout + Chunk;
|
|
|
|
|
until (Chunk = '') and not Proc.Running;
|
|
|
|
|
Proc.WaitOnExit();
|
|
|
|
|
Result := Proc.ExitCode;
|
|
|
|
|
finally
|
|
|
|
|
Proc.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TInternalLinkerE2ETests.RunProcNoArgs(const AExe: string;
|
|
|
|
|
out AStdout: string): Integer;
|
|
|
|
|
var
|
|
|
|
|
Proc: TProcess;
|
|
|
|
|
Chunk: string;
|
|
|
|
|
begin
|
|
|
|
|
Proc := TProcess.Create(nil);
|
|
|
|
|
try
|
|
|
|
|
Proc.Executable := AExe;
|
|
|
|
|
Proc.Execute();
|
|
|
|
|
AStdout := '';
|
|
|
|
|
repeat
|
|
|
|
|
Chunk := Proc.ReadOutput();
|
|
|
|
|
AStdout := AStdout + Chunk;
|
|
|
|
|
until (Chunk = '') and not Proc.Running;
|
|
|
|
|
Proc.WaitOnExit();
|
|
|
|
|
Result := Proc.ExitCode;
|
|
|
|
|
finally
|
|
|
|
|
Proc.Free();
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function TInternalLinkerE2ETests.CompileAndRun(const ASrc: string;
|
|
|
|
|
out AStdout: string; out AExitCode: Integer): Boolean;
|
|
|
|
|
var
|
|
|
|
|
SrcFile, OutFile, CompOut: string;
|
|
|
|
|
Rc: Integer;
|
|
|
|
|
begin
|
|
|
|
|
Result := False;
|
|
|
|
|
if not Self.CompilerAvailable() then Exit;
|
|
|
|
|
|
|
|
|
|
FCounter := FCounter + 1;
|
|
|
|
|
SrcFile := FScratch + 'test_il_' + IntToStr(FCounter) + '.pas';
|
|
|
|
|
OutFile := FScratch + 'test_il_' + IntToStr(FCounter);
|
|
|
|
|
|
|
|
|
|
WriteFile(SrcFile, ASrc);
|
|
|
|
|
|
|
|
|
|
Rc := Self.RunProc(FCompiler, [
|
|
|
|
|
'--source', SrcFile,
|
|
|
|
|
'--unit-path', FRTLPath,
|
|
|
|
|
'--unit-path', FStdlibPath,
|
|
|
|
|
'--output', OutFile,
|
|
|
|
|
'--backend', 'native',
|
|
|
|
|
'--assembler', 'internal',
|
|
|
|
|
'--linker', 'internal'
|
|
|
|
|
], CompOut);
|
|
|
|
|
if Rc <> 0 then
|
|
|
|
|
Fail('compile failed (rc=' + IntToStr(Rc) + '): ' + CompOut);
|
|
|
|
|
|
|
|
|
|
AExitCode := Self.RunProcNoArgs(OutFile, AStdout);
|
|
|
|
|
Result := True;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TInternalLinkerE2ETests.TestRun_HelloWorld;
|
|
|
|
|
var
|
|
|
|
|
Out_: string;
|
|
|
|
|
EC: Integer;
|
|
|
|
|
begin
|
|
|
|
|
if not CompileAndRun(
|
|
|
|
|
'program test_hello;' + LineEnding +
|
|
|
|
|
'begin' + LineEnding +
|
|
|
|
|
' WriteLn(''Hello from internal linker'')' + LineEnding +
|
|
|
|
|
'end.',
|
|
|
|
|
Out_, EC) then
|
|
|
|
|
begin
|
|
|
|
|
Ignore('<toolchain-missing>');
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
AssertEquals('exit code', 0, EC);
|
|
|
|
|
AssertEquals('stdout', 'Hello from internal linker' + LineEnding, Out_);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TInternalLinkerE2ETests.TestRun_StringOps;
|
|
|
|
|
var
|
|
|
|
|
Out_: string;
|
|
|
|
|
EC: Integer;
|
|
|
|
|
begin
|
|
|
|
|
if not CompileAndRun(
|
|
|
|
|
'program test_str;' + LineEnding +
|
|
|
|
|
'uses SysUtils;' + LineEnding +
|
|
|
|
|
'begin' + LineEnding +
|
|
|
|
|
' WriteLn(UpperCase(''hello''));' + LineEnding +
|
|
|
|
|
' WriteLn(IntToStr(42));' + LineEnding +
|
|
|
|
|
' WriteLn(Format(''%d+%d=%d'', [1, 2, 3]))' + LineEnding +
|
|
|
|
|
'end.',
|
|
|
|
|
Out_, EC) then
|
|
|
|
|
begin
|
|
|
|
|
Ignore('<toolchain-missing>');
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
AssertEquals('exit code', 0, EC);
|
|
|
|
|
AssertEquals('stdout',
|
|
|
|
|
'HELLO' + LineEnding +
|
|
|
|
|
'42' + LineEnding +
|
|
|
|
|
'1+2=3' + LineEnding, Out_);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TInternalLinkerE2ETests.TestRun_ExceptionHandling;
|
|
|
|
|
var
|
|
|
|
|
Out_: string;
|
|
|
|
|
EC: Integer;
|
|
|
|
|
begin
|
|
|
|
|
if not CompileAndRun(
|
|
|
|
|
'program test_exc;' + LineEnding +
|
|
|
|
|
'uses SysUtils;' + LineEnding +
|
|
|
|
|
'begin' + LineEnding +
|
|
|
|
|
' try' + LineEnding +
|
|
|
|
|
' raise Exception.Create(''boom'');' + LineEnding +
|
|
|
|
|
' except' + LineEnding +
|
|
|
|
|
' on E: Exception do' + LineEnding +
|
|
|
|
|
' WriteLn(E.Message);' + LineEnding +
|
|
|
|
|
' end;' + LineEnding +
|
|
|
|
|
' WriteLn(''OK'')' + LineEnding +
|
|
|
|
|
'end.',
|
|
|
|
|
Out_, EC) then
|
|
|
|
|
begin
|
|
|
|
|
Ignore('<toolchain-missing>');
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
AssertEquals('exit code', 0, EC);
|
|
|
|
|
AssertEquals('stdout', 'boom' + LineEnding + 'OK' + LineEnding, Out_);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TInternalLinkerE2ETests.TestRun_ClassAndVirtual;
|
|
|
|
|
var
|
|
|
|
|
Out_: string;
|
|
|
|
|
EC: Integer;
|
|
|
|
|
begin
|
|
|
|
|
if not CompileAndRun(
|
|
|
|
|
'program test_cls;' + LineEnding +
|
|
|
|
|
'type' + LineEnding +
|
|
|
|
|
' TBase = class' + LineEnding +
|
|
|
|
|
' function Name: string; virtual;' + LineEnding +
|
|
|
|
|
' end;' + LineEnding +
|
|
|
|
|
' TChild = class(TBase)' + LineEnding +
|
|
|
|
|
' function Name: string; override;' + LineEnding +
|
|
|
|
|
' end;' + LineEnding +
|
|
|
|
|
'function TBase.Name: string;' + LineEnding +
|
|
|
|
|
'begin' + LineEnding +
|
|
|
|
|
' Result := ''base'';' + LineEnding +
|
|
|
|
|
'end;' + LineEnding +
|
|
|
|
|
'function TChild.Name: string;' + LineEnding +
|
|
|
|
|
'begin' + LineEnding +
|
|
|
|
|
' Result := ''child'';' + LineEnding +
|
|
|
|
|
'end;' + LineEnding +
|
|
|
|
|
'var' + LineEnding +
|
|
|
|
|
' B: TBase;' + LineEnding +
|
|
|
|
|
'begin' + LineEnding +
|
|
|
|
|
' B := TChild.Create();' + LineEnding +
|
|
|
|
|
' try' + LineEnding +
|
|
|
|
|
' WriteLn(B.Name())' + LineEnding +
|
|
|
|
|
' finally' + LineEnding +
|
|
|
|
|
' B.Free()' + LineEnding +
|
|
|
|
|
' end' + LineEnding +
|
|
|
|
|
'end.',
|
|
|
|
|
Out_, EC) then
|
|
|
|
|
begin
|
|
|
|
|
Ignore('<toolchain-missing>');
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
AssertEquals('exit code', 0, EC);
|
|
|
|
|
AssertEquals('stdout', 'child' + LineEnding, Out_);
|
|
|
|
|
end;
|
|
|
|
|
|
2026-06-24 03:12:07 +03:00
|
|
|
{ Regression: when blaise_rtl.a cannot be found beside the compiler binary
|
|
|
|
|
(and BLAISE_RTL is unset), the compiler must REFUSE to link rather than
|
|
|
|
|
silently emit a binary with every RTL symbol left as an undefined dynamic
|
|
|
|
|
import — that broken-but-runnable binary dies at run time with
|
|
|
|
|
`undefined symbol: _SetArgs`. A mismatched carried-RTL name in
|
|
|
|
|
rolling-bootstrap surfaced exactly this. We reproduce by running a COPY of
|
|
|
|
|
the compiler from a scratch dir that has no blaise_rtl.a beside it. }
|
|
|
|
|
procedure TInternalLinkerE2ETests.TestLink_MissingRTL_FailsLoudly;
|
|
|
|
|
var
|
|
|
|
|
IsoDir, IsoCompiler, SrcFile, OutFile, CompOut: string;
|
|
|
|
|
Rc: Integer;
|
|
|
|
|
begin
|
|
|
|
|
if not Self.CompilerAvailable() then
|
|
|
|
|
begin
|
|
|
|
|
Ignore('<toolchain-missing>');
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
FCounter := FCounter + 1;
|
|
|
|
|
IsoDir := FScratch + 'no_rtl_' + IntToStr(FCounter) + '/';
|
|
|
|
|
ForceDirectories(IsoDir);
|
2026-07-04 21:44:54 +03:00
|
|
|
IsoCompiler := IsoDir + 'blaise';
|
|
|
|
|
{ Symlink the real compiler into the isolated dir — argv[0]'s dirname is the
|
|
|
|
|
isolated dir, which has NO blaise_rtl.a beside it, so FindRTLArchive
|
|
|
|
|
returns ''. (A symlink avoids copying the multi-MB binary and the
|
|
|
|
|
byte-exactness pitfalls of a string round-trip.) }
|
|
|
|
|
if FileExists(IsoCompiler) then DeleteFile(IsoCompiler);
|
|
|
|
|
if _test_symlink(PChar(FCompiler), PChar(IsoCompiler)) <> 0 then
|
|
|
|
|
begin
|
|
|
|
|
Ignore('<symlink-unavailable>');
|
|
|
|
|
Exit;
|
|
|
|
|
end;
|
2026-06-24 03:12:07 +03:00
|
|
|
|
|
|
|
|
SrcFile := IsoDir + 'prog.pas';
|
|
|
|
|
OutFile := IsoDir + 'prog';
|
|
|
|
|
WriteFile(SrcFile,
|
|
|
|
|
'program test_no_rtl;' + LineEnding +
|
|
|
|
|
'begin' + LineEnding +
|
|
|
|
|
' WriteLn(6 * 7)' + LineEnding +
|
|
|
|
|
'end.');
|
|
|
|
|
|
2026-07-04 21:44:54 +03:00
|
|
|
{ BLAISE_RTL is normally unset in the test environment; the isolated dir has
|
|
|
|
|
no blaise_rtl.a, so FindRTLArchive returns '' and the guard must fire. }
|
2026-06-24 03:12:07 +03:00
|
|
|
Rc := Self.RunProc(IsoCompiler, [
|
|
|
|
|
'--source', SrcFile,
|
2026-07-04 21:44:54 +03:00
|
|
|
'--unit-path', FRTLPath,
|
2026-06-24 03:12:07 +03:00
|
|
|
'--unit-path', FStdlibPath,
|
|
|
|
|
'--output', OutFile,
|
|
|
|
|
'--backend', 'native',
|
|
|
|
|
'--assembler', 'internal',
|
2026-07-04 21:44:54 +03:00
|
|
|
'--linker', 'internal'
|
2026-06-24 03:12:07 +03:00
|
|
|
], CompOut);
|
|
|
|
|
|
2026-07-04 21:44:54 +03:00
|
|
|
AssertTrue('compiler must FAIL when RTL is unreachable (got rc=0)', Rc <> 0);
|
|
|
|
|
AssertTrue('error must name the missing RTL archive: ' + CompOut,
|
|
|
|
|
Pos('blaise_rtl.a', CompOut) >= 0);
|
2026-06-24 03:12:07 +03:00
|
|
|
AssertFalse('no output binary should be produced on RTL-missing failure',
|
|
|
|
|
FileExists(OutFile));
|
|
|
|
|
end;
|
|
|
|
|
|
2026-06-12 12:10:21 +03:00
|
|
|
initialization
|
|
|
|
|
RegisterTest(TElfReaderTests);
|
2026-06-12 12:13:16 +03:00
|
|
|
RegisterTest(TSectionMergerTests);
|
feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 16:58:24 +03:00
|
|
|
RegisterTest(TLinkerTests);
|
|
|
|
|
RegisterTest(TLinkerE2ETests);
|
feat(linker): Phase C dynamic linking — GOT/PLT, CRT objects, PIE emission
Implement Phase C of the internal ELF linker: produce position-independent
ET_DYN executables linked against libc through dynamic GOT/PLT dispatch.
New capabilities:
- SetDynamic(True) switches the linker to PIE mode
- AddCrtObject() loads system CRT startup objects (Scrt1.o, crti.o,
crtn.o, crtbeginS.o, crtendS.o)
- CollectExternals scans undefined non-weak symbols and creates PLT entries
- CollectGotSlots creates non-PLT GOT slots for GOTPCREL references to
locally-defined symbols (e.g. main, __dso_handle)
- BuildDynamic generates .interp, .dynstr, .dynsym, SysV .hash, .got
(3 reserved + N PLT + M non-PLT slots), .plt (header + stubs),
.rela.plt (JUMP_SLOT entries)
- LayoutDynamic assigns PIE virtual addresses (base 0): RO metadata →
.text/.plt → RELRO (.init_array, .fini_array, .dynamic, .got) →
.data/.bss, with page-aligned PT_LOAD boundaries
- EmitDynExecutable writes the full PIE byte image with ELF header,
10 program headers (PHDR, INTERP, 4×LOAD, DYNAMIC, TLS, GNU_STACK,
GNU_RELRO), and section data at virtual offsets
Relocation handling extended:
- R_X86_64_64 → R_X86_64_RELATIVE in .rela.dyn
- R_X86_64_32/32S for absolute symbol references
- R_X86_64_GOTPCREL/GOTPCRELX/REX_GOTPCRELX → GOT slot lookup
- R_X86_64_TPOFF32 → TLS offset calculation
Tests: TDynLinkerTests (2 structural), TDynLinkerE2ETests (1 E2E that
links a main() calling write via PLT, runs the binary, asserts exit
code 42 + correct stdout). All 3528 tests pass.
2026-06-19 12:00:08 +03:00
|
|
|
RegisterTest(TDynLinkerTests);
|
|
|
|
|
RegisterTest(TDynLinkerE2ETests);
|
2026-06-19 12:48:04 +03:00
|
|
|
RegisterTest(TInternalLinkerE2ETests);
|
2026-06-12 12:10:21 +03:00
|
|
|
|
|
|
|
|
end.
|