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.
This commit is contained in:
parent
8f38fd1416
commit
078dc81e46
File diff suppressed because it is too large
Load diff
|
|
@ -76,6 +76,25 @@ type
|
||||||
procedure TestRun_SyscallHelloWorld;
|
procedure TestRun_SyscallHelloWorld;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
function TElfReaderTests.ProjectRoot: string;
|
function TElfReaderTests.ProjectRoot: string;
|
||||||
|
|
@ -876,10 +895,244 @@ begin
|
||||||
'Hello, linker!' + Chr(10), Output);
|
'Hello, linker!' + Chr(10), Output);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ ---- 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;
|
||||||
|
|
||||||
initialization
|
initialization
|
||||||
RegisterTest(TElfReaderTests);
|
RegisterTest(TElfReaderTests);
|
||||||
RegisterTest(TSectionMergerTests);
|
RegisterTest(TSectionMergerTests);
|
||||||
RegisterTest(TLinkerTests);
|
RegisterTest(TLinkerTests);
|
||||||
RegisterTest(TLinkerE2ETests);
|
RegisterTest(TLinkerE2ETests);
|
||||||
|
RegisterTest(TDynLinkerTests);
|
||||||
|
RegisterTest(TDynLinkerE2ETests);
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue