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).
This commit is contained in:
parent
7a663c561f
commit
fe4e5602cf
|
|
@ -8,31 +8,81 @@
|
|||
|
||||
unit blaise.linker.elf;
|
||||
|
||||
{ Internal ELF linker — section merging (Phase A of
|
||||
docs/internal-linker-design.adoc).
|
||||
{ Internal ELF linker — section merging (Phase A) plus symbol
|
||||
resolution, static relocations, and non-PIE ET_EXEC emission
|
||||
(Phase B of docs/internal-linker-design.adoc).
|
||||
|
||||
TSectionMerger concatenates like-named allocatable sections from a
|
||||
set of parsed input objects, padding each contribution to its
|
||||
section's alignment, and records a placement (merged section +
|
||||
offset) for every input section. Placements are the basis for
|
||||
symbol and relocation rebasing in Phase B: a symbol's final offset
|
||||
is its object-local value plus its section's placement offset.
|
||||
Phase A — TSectionMerger concatenates like-named allocatable
|
||||
sections from a set of parsed input objects, padding each
|
||||
contribution to its section's alignment, and records a placement
|
||||
(merged section + offset) for every input section. Placements are
|
||||
the basis for symbol and relocation rebasing: a symbol's final
|
||||
offset is its object-local value plus its section's placement
|
||||
offset.
|
||||
|
||||
SHT_NOBITS contributions advance the merged size without adding
|
||||
bytes; mixing NOBITS and PROGBITS under one name is rejected.
|
||||
Non-allocatable bookkeeping sections (symtab, strtab, rela,
|
||||
.note.GNU-stack, .comment) are skipped — the linker rebuilds those
|
||||
itself. Non-alloc .opdf.* debug sections ARE kept: they must ride
|
||||
through into the final executable for the OPDF debugger. }
|
||||
through into the final executable for the OPDF debugger.
|
||||
|
||||
Phase B — TLinker takes a set of parsed objects, merges their
|
||||
sections, assigns virtual addresses at a fixed base (non-PIE
|
||||
ET_EXEC, no GOT/PLT, no dynamic linking), builds a global symbol
|
||||
table, resolves intra-program PC-relative relocations
|
||||
(R_X86_64_PC32, R_X86_64_PLT32), and writes a runnable executable.
|
||||
This is the standalone-program path of the design: every real
|
||||
Blaise program reaches libc through the RTL and needs Phase C's
|
||||
dynamic linker, but a hand-written object that talks to the kernel
|
||||
through raw syscalls links and runs with Phase B alone.
|
||||
|
||||
Relocation types that require dynamic linking (R_X86_64_64 in a
|
||||
PIE, GOT/PLT/TLS forms) and absolute 32-bit symbol references
|
||||
(R_X86_64_32 / R_X86_64_32S, which a static ET_EXEC could in
|
||||
principle take but the native backend does not emit for symbol
|
||||
references) are rejected with a diagnostic — they belong to later
|
||||
phases or are codegen bugs. }
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
SysUtils, Generics.Collections, blaise.elfreader;
|
||||
SysUtils, Generics.Collections, streams, blaise.elfreader;
|
||||
|
||||
type
|
||||
ELinker = class(Exception);
|
||||
|
||||
{ Platform/architecture parameters for one link target.
|
||||
|
||||
Phase B fills this for Linux x86-64 ELF only, but every value that
|
||||
differs across the roadmap targets (i386/x86-64, Linux/FreeBSD,
|
||||
later Windows) lives here rather than hard-coded in the emitter,
|
||||
so adding a target is a new record value plus, where the container
|
||||
differs (PE/Mach-O), a sibling writer behind the same TLinker
|
||||
symbol/relocation core. See the "Platform Parameterisation"
|
||||
section of docs/internal-linker-design.adoc.
|
||||
|
||||
The pointer width (Is64) drives ELF class, header sizes, address
|
||||
arithmetic and the relocation set; OSABI/BaseAddr/PageSize are the
|
||||
per-OS knobs. Container format (ELF vs PE/Mach-O) is implied by
|
||||
which writer is invoked; only ELF targets are modelled here. }
|
||||
TLinkArch = (laX86_64, laI386);
|
||||
|
||||
TLinkTarget = class
|
||||
public
|
||||
Arch: TLinkArch;
|
||||
Is64: Boolean; { 64-bit pointers/addresses }
|
||||
OSABI: Integer; { EI_OSABI: 0 = SysV/Linux, 9 = FreeBSD }
|
||||
EMachine: Integer; { e_machine: EM_X86_64 / EM_386 }
|
||||
BaseAddr: Int64; { fixed load base for non-PIE ET_EXEC }
|
||||
PageSize: Int64; { segment alignment }
|
||||
constructor Create;
|
||||
end;
|
||||
|
||||
{ Linux x86-64 ELF, non-PIE ET_EXEC. Caller frees. }
|
||||
function LinuxX86_64Target: TLinkTarget;
|
||||
|
||||
type
|
||||
{ One output section accumulating contributions from input objects. }
|
||||
TMergedSection = class
|
||||
public
|
||||
|
|
@ -76,6 +126,85 @@ type
|
|||
property Placements: TList<TSectionPlacement> read FPlacements;
|
||||
end;
|
||||
|
||||
{ A resolved global symbol: name plus final virtual address. Built
|
||||
after layout, so Addr is absolute for the chosen load base. }
|
||||
TLinkSymbol = class
|
||||
public
|
||||
Name: string;
|
||||
Addr: Int64; { final virtual address (0 for weak-undef) }
|
||||
Defined: Boolean; { False = weak undefined resolved to 0 }
|
||||
IsFunc: Boolean;
|
||||
IsWeakSlot: Boolean; { defined only by a STB_WEAK symbol so far }
|
||||
end;
|
||||
|
||||
{ Phase B linker: merge → layout → resolve symbols → relocate →
|
||||
emit a non-PIE ET_EXEC. One instance links one executable.
|
||||
|
||||
Lifecycle: AddObject* for each input, then Link(entry, output).
|
||||
The merger, layout addresses, symbol table and patched section
|
||||
bytes are all owned by the linker and freed with it. }
|
||||
TLinker = class
|
||||
private
|
||||
FTarget: TLinkTarget;
|
||||
FOwnTarget: Boolean;
|
||||
FObjects: TList<TElfObjectFile>;
|
||||
FOwned: TList<TElfObjectFile>; { objects we must free }
|
||||
FMerger: TSectionMerger;
|
||||
FSymbols: TList<TLinkSymbol>;
|
||||
FSecAddr: TList<TMergedSection>; { merged sections, in layout order }
|
||||
FAddrOf: TList<Int64>; { virtual base addr per FSecAddr entry }
|
||||
FEntry: Int64;
|
||||
|
||||
function MergedAddr(AMerged: TMergedSection): Int64;
|
||||
function SectionOfPlacement(AObjIdx, ASecIdx: Integer): TMergedSection;
|
||||
function PlacementBaseAddr(AObjIdx, ASecIdx: Integer): Int64;
|
||||
function FileOffset(AAddr: Int64): Integer;
|
||||
procedure PlaceSection(AM: TMergedSection; var AAddr: Int64);
|
||||
procedure LayoutSections;
|
||||
procedure BuildSymbols;
|
||||
function FindSymbol(const AName: string): TLinkSymbol;
|
||||
procedure AddSynthSymbol(const AName: string; AAddr: Int64);
|
||||
procedure DefineSynthSymbols;
|
||||
procedure ApplyRelocations;
|
||||
function ResolveSymbolAddr(AObj: TElfObjectFile; ASymIdx: Integer;
|
||||
const AContext: string): Int64;
|
||||
function EmitExecutable(AEntry: Int64): string;
|
||||
public
|
||||
constructor Create; overload;
|
||||
constructor Create(ATarget: TLinkTarget); overload; { borrows target }
|
||||
destructor Destroy; override;
|
||||
|
||||
{ Add a parsed object the caller owns (not freed by the linker). }
|
||||
procedure AddObject(AObj: TElfObjectFile);
|
||||
{ Add an object the linker takes ownership of and frees. }
|
||||
procedure AddOwnedObject(AObj: TElfObjectFile);
|
||||
|
||||
{ Merge, lay out, resolve, relocate and write an ET_EXEC whose
|
||||
entry point is the symbol AEntryName. Raises ELinker on any
|
||||
unresolved symbol, duplicate definition, or unsupported
|
||||
relocation. }
|
||||
procedure Link(const AEntryName, AOutputPath: string);
|
||||
|
||||
{ Same pipeline, returning the executable bytes instead of writing
|
||||
a file (used by tests for structural assertions). }
|
||||
function LinkToBytes(const AEntryName: string): string;
|
||||
|
||||
{ Address a global symbol resolved to (valid only after Link/
|
||||
LinkToBytes). -1 if absent. }
|
||||
function AddrOfSymbol(const AName: string): Int64;
|
||||
|
||||
{ Merged section by name (e.g. '.text'), or nil — exposes the
|
||||
relocated bytes for tests/inspection. Valid after Link. }
|
||||
function FindMerged(const AName: string): TMergedSection;
|
||||
function FindMergedText: TMergedSection;
|
||||
|
||||
property Target: TLinkTarget read FTarget;
|
||||
end;
|
||||
|
||||
{ Mark a file user+group+other readable/executable (0755). Used to
|
||||
make the linked output runnable. }
|
||||
procedure MakeFileExecutable(const APath: string);
|
||||
|
||||
implementation
|
||||
|
||||
function LkAlignUp(AVal: Int64; AAlign: Int64): Int64;
|
||||
|
|
@ -244,4 +373,739 @@ begin
|
|||
Result := nil;
|
||||
end;
|
||||
|
||||
{ ---- ELF executable constants (Phase B) ------------------------------- }
|
||||
|
||||
const
|
||||
ET_EXEC = 2;
|
||||
EM_386 = 3;
|
||||
|
||||
ELFOSABI_SYSV = 0;
|
||||
ELFOSABI_FREEBSD = 9;
|
||||
|
||||
PT_LOAD = 1;
|
||||
PF_X = 1;
|
||||
PF_W = 2;
|
||||
PF_R = 4;
|
||||
|
||||
EI_NIDENT = 16;
|
||||
|
||||
{ ---- Little-endian byte writers --------------------------------------- }
|
||||
|
||||
{ Blaise codegen does not support assigning to a string element through
|
||||
a var-string parameter (`ABuf[i] := c` on a var param), so every
|
||||
encoder here RETURNS the bytes and callers append them; fixed-offset
|
||||
patching is done with memcpy (a pointer write, which is fine). }
|
||||
|
||||
{ N-byte little-endian encoding of AVal. }
|
||||
function LkLE(AVal: Int64; ANBytes: Integer): string;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
for I := 0 to ANBytes - 1 do
|
||||
Result := Result + Chr(Integer((AVal shr (I * 8)) and $FF));
|
||||
end;
|
||||
|
||||
{ Overwrite ABuf[AOff..] in place with ASrc's bytes (ABuf already large
|
||||
enough). Blaise rejects `ABuf[i] := c` and `@ABuf[i]`, so writes go
|
||||
through a local PChar — the one idiom the native backend accepts for
|
||||
in-place string mutation (see ZeroBuf in uElfObject.pas). }
|
||||
procedure LkCopyInto(var ABuf: string; AOff: Integer; const ASrc: string);
|
||||
var
|
||||
P: PChar;
|
||||
I: Integer;
|
||||
begin
|
||||
P := PChar(ABuf);
|
||||
for I := 0 to Length(ASrc) - 1 do
|
||||
P[AOff + I] := ASrc[I];
|
||||
end;
|
||||
|
||||
{ Patch a 32-bit LE value at AOff. }
|
||||
procedure LkPatch32(var ABuf: string; AOff: Integer; AVal: Int64);
|
||||
begin
|
||||
LkCopyInto(ABuf, AOff, LkLE(AVal, 4));
|
||||
end;
|
||||
|
||||
{ ---- chmod binding ----------------------------------------------------- }
|
||||
|
||||
function _lk_chmod(APath: PChar; AMode: Integer): Integer;
|
||||
external name 'chmod';
|
||||
|
||||
procedure MakeFileExecutable(const APath: string);
|
||||
begin
|
||||
{ 0o755 = rwxr-xr-x }
|
||||
_lk_chmod(PChar(APath), 493);
|
||||
end;
|
||||
|
||||
{ ---- TLinkTarget ------------------------------------------------------- }
|
||||
|
||||
constructor TLinkTarget.Create;
|
||||
begin
|
||||
inherited Create();
|
||||
Arch := laX86_64;
|
||||
Is64 := True;
|
||||
OSABI := ELFOSABI_SYSV;
|
||||
EMachine := EM_X86_64;
|
||||
BaseAddr := $400000;
|
||||
PageSize := $1000;
|
||||
end;
|
||||
|
||||
function LinuxX86_64Target: TLinkTarget;
|
||||
begin
|
||||
Result := TLinkTarget.Create();
|
||||
{ defaults already describe Linux x86-64 }
|
||||
end;
|
||||
|
||||
{ ---- TLinker ----------------------------------------------------------- }
|
||||
|
||||
constructor TLinker.Create;
|
||||
begin
|
||||
Self.Create(LinuxX86_64Target());
|
||||
FOwnTarget := True;
|
||||
end;
|
||||
|
||||
constructor TLinker.Create(ATarget: TLinkTarget);
|
||||
begin
|
||||
inherited Create();
|
||||
FTarget := ATarget;
|
||||
FOwnTarget := False;
|
||||
FObjects := TList<TElfObjectFile>.Create();
|
||||
FOwned := TList<TElfObjectFile>.Create();
|
||||
FMerger := TSectionMerger.Create();
|
||||
FSymbols := TList<TLinkSymbol>.Create();
|
||||
FSecAddr := TList<TMergedSection>.Create();
|
||||
FAddrOf := TList<Int64>.Create();
|
||||
FEntry := 0;
|
||||
end;
|
||||
|
||||
destructor TLinker.Destroy;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := 0 to FSymbols.Count - 1 do
|
||||
FSymbols.Get(I).Free();
|
||||
FSymbols.Free();
|
||||
FAddrOf.Free();
|
||||
FSecAddr.Free(); { sections owned by FMerger }
|
||||
FMerger.Free();
|
||||
for I := 0 to FOwned.Count - 1 do
|
||||
FOwned.Get(I).Free();
|
||||
FOwned.Free();
|
||||
FObjects.Free();
|
||||
if FOwnTarget then
|
||||
FTarget.Free();
|
||||
inherited Destroy();
|
||||
end;
|
||||
|
||||
procedure TLinker.AddObject(AObj: TElfObjectFile);
|
||||
begin
|
||||
FMerger.AddObject(FObjects.Count, AObj);
|
||||
FObjects.Add(AObj);
|
||||
end;
|
||||
|
||||
procedure TLinker.AddOwnedObject(AObj: TElfObjectFile);
|
||||
begin
|
||||
FOwned.Add(AObj);
|
||||
Self.AddObject(AObj);
|
||||
end;
|
||||
|
||||
{ Virtual base address assigned to a merged section, or -1 if it was
|
||||
not laid out (e.g. a non-alloc debug section). }
|
||||
function TLinker.MergedAddr(AMerged: TMergedSection): Int64;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := 0 to FSecAddr.Count - 1 do
|
||||
if FSecAddr.Get(I) = AMerged then
|
||||
begin
|
||||
Result := FAddrOf.Get(I);
|
||||
Exit;
|
||||
end;
|
||||
Result := -1;
|
||||
end;
|
||||
|
||||
{ File offset for a virtual address in a laid-out section: the file
|
||||
image mirrors the virtual layout shifted down by the load base. }
|
||||
function TLinker.FileOffset(AAddr: Int64): Integer;
|
||||
begin
|
||||
Result := Integer(AAddr - FTarget.BaseAddr);
|
||||
end;
|
||||
|
||||
function TLinker.SectionOfPlacement(AObjIdx,
|
||||
ASecIdx: Integer): TMergedSection;
|
||||
var
|
||||
P: TSectionPlacement;
|
||||
begin
|
||||
P := FMerger.PlacementOf(AObjIdx, ASecIdx);
|
||||
if P = nil then
|
||||
Result := nil
|
||||
else
|
||||
Result := P.Merged;
|
||||
end;
|
||||
|
||||
function TLinker.PlacementBaseAddr(AObjIdx, ASecIdx: Integer): Int64;
|
||||
var
|
||||
P: TSectionPlacement;
|
||||
Base: Int64;
|
||||
begin
|
||||
P := FMerger.PlacementOf(AObjIdx, ASecIdx);
|
||||
if P = nil then
|
||||
begin
|
||||
Result := -1;
|
||||
Exit;
|
||||
end;
|
||||
Base := Self.MergedAddr(P.Merged);
|
||||
if Base < 0 then
|
||||
begin
|
||||
Result := -1;
|
||||
Exit;
|
||||
end;
|
||||
Result := Base + P.Offset;
|
||||
end;
|
||||
|
||||
{ Assign virtual addresses. Allocatable PROGBITS/NOBITS sections are
|
||||
grouped by permission into two loadable runs — executable (text +
|
||||
rodata) then writable (data + bss) — each starting on a fresh page.
|
||||
The first run begins after the ELF header + program headers, with
|
||||
p_vaddr congruent to p_offset modulo PageSize, as the loader
|
||||
requires. Non-allocatable sections (.opdf.*) are not assigned an
|
||||
address; they ride through unmapped. }
|
||||
procedure TLinker.PlaceSection(AM: TMergedSection; var AAddr: Int64);
|
||||
var
|
||||
Al: Int64;
|
||||
begin
|
||||
Al := AM.Align;
|
||||
if Al < 1 then Al := 1;
|
||||
AAddr := LkAlignUp(AAddr, Al);
|
||||
FSecAddr.Add(AM);
|
||||
FAddrOf.Add(AAddr);
|
||||
AAddr := AAddr + AM.Size;
|
||||
end;
|
||||
|
||||
procedure TLinker.LayoutSections;
|
||||
var
|
||||
I: Integer;
|
||||
M: TMergedSection;
|
||||
Addr: Int64;
|
||||
HdrBytes: Int64;
|
||||
IsAlloc, IsExec, IsWrite: Boolean;
|
||||
begin
|
||||
{ Program-header count is known: PT_LOAD x2 (exec run, write run).
|
||||
Reserve header space so the first section's file offset — which
|
||||
equals (addr - base) — clears the headers. Elf64_Phdr = 56. }
|
||||
HdrBytes := ELF64_EHDR_SIZE + 2 * 56;
|
||||
|
||||
{ Executable run: header bytes share its first page. }
|
||||
Addr := FTarget.BaseAddr + HdrBytes;
|
||||
for I := 0 to FMerger.Merged.Count - 1 do
|
||||
begin
|
||||
M := FMerger.Merged.Get(I);
|
||||
IsAlloc := (M.Flags and SHF_ALLOC) <> 0;
|
||||
IsExec := (M.Flags and SHF_EXECINSTR) <> 0;
|
||||
if IsAlloc and IsExec then Self.PlaceSection(M, Addr);
|
||||
end;
|
||||
{ Read-only non-exec (rodata) joins the executable run. }
|
||||
for I := 0 to FMerger.Merged.Count - 1 do
|
||||
begin
|
||||
M := FMerger.Merged.Get(I);
|
||||
IsAlloc := (M.Flags and SHF_ALLOC) <> 0;
|
||||
IsExec := (M.Flags and SHF_EXECINSTR) <> 0;
|
||||
IsWrite := (M.Flags and SHF_WRITE) <> 0;
|
||||
if IsAlloc and (not IsExec) and (not IsWrite) then
|
||||
Self.PlaceSection(M, Addr);
|
||||
end;
|
||||
|
||||
{ Writable run starts on a fresh page. }
|
||||
Addr := LkAlignUp(Addr, FTarget.PageSize);
|
||||
for I := 0 to FMerger.Merged.Count - 1 do
|
||||
begin
|
||||
M := FMerger.Merged.Get(I);
|
||||
IsAlloc := (M.Flags and SHF_ALLOC) <> 0;
|
||||
IsWrite := (M.Flags and SHF_WRITE) <> 0;
|
||||
if IsAlloc and IsWrite and (M.ShType <> SHT_NOBITS) then
|
||||
Self.PlaceSection(M, Addr);
|
||||
end;
|
||||
for I := 0 to FMerger.Merged.Count - 1 do
|
||||
begin
|
||||
M := FMerger.Merged.Get(I);
|
||||
IsAlloc := (M.Flags and SHF_ALLOC) <> 0;
|
||||
IsWrite := (M.Flags and SHF_WRITE) <> 0;
|
||||
if IsAlloc and IsWrite and (M.ShType = SHT_NOBITS) then
|
||||
Self.PlaceSection(M, Addr);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TLinker.FindSymbol(const AName: string): TLinkSymbol;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := 0 to FSymbols.Count - 1 do
|
||||
if FSymbols.Get(I).Name = AName then
|
||||
begin
|
||||
Result := FSymbols.Get(I);
|
||||
Exit;
|
||||
end;
|
||||
Result := nil;
|
||||
end;
|
||||
|
||||
procedure TLinker.AddSynthSymbol(const AName: string; AAddr: Int64);
|
||||
var
|
||||
S: TLinkSymbol;
|
||||
begin
|
||||
S := Self.FindSymbol(AName);
|
||||
if S <> nil then Exit; { a real definition wins over the synth one }
|
||||
S := TLinkSymbol.Create();
|
||||
S.Name := AName;
|
||||
S.Addr := AAddr;
|
||||
S.Defined := True;
|
||||
S.IsFunc := False;
|
||||
FSymbols.Add(S);
|
||||
end;
|
||||
|
||||
{ Build the global symbol table from every input object. Only
|
||||
STB_GLOBAL / STB_WEAK symbols with a real definition (section index
|
||||
not SHN_UNDEF, not ABS/COMMON) are entered; a second STB_GLOBAL
|
||||
definition of the same name is a duplicate-symbol error, while a
|
||||
STB_GLOBAL overrides a previously seen STB_WEAK. LayoutSections
|
||||
must have run so addresses are known. }
|
||||
procedure TLinker.BuildSymbols;
|
||||
var
|
||||
Oi, Si: Integer;
|
||||
Obj: TElfObjectFile;
|
||||
Sym: TRdSymbol;
|
||||
Existing: TLinkSymbol;
|
||||
NewSym: TLinkSymbol;
|
||||
Base: Int64;
|
||||
begin
|
||||
for Oi := 0 to FObjects.Count - 1 do
|
||||
begin
|
||||
Obj := FObjects.Get(Oi);
|
||||
for Si := 0 to Obj.Symbols.Count - 1 do
|
||||
begin
|
||||
Sym := Obj.Symbols.Get(Si);
|
||||
if (Sym.Bind <> STB_GLOBAL) and (Sym.Bind <> STB_WEAK) then Continue;
|
||||
if Sym.Name = '' then Continue;
|
||||
if Sym.Shndx = SHN_UNDEF then Continue;
|
||||
if (Sym.Shndx = SHN_ABS) or (Sym.Shndx = SHN_COMMON) then Continue;
|
||||
|
||||
Base := Self.PlacementBaseAddr(Oi, Sym.Shndx);
|
||||
if Base < 0 then Continue; { defined in a section we did not lay out }
|
||||
|
||||
Existing := Self.FindSymbol(Sym.Name);
|
||||
if Existing <> nil then
|
||||
begin
|
||||
{ Both strong → duplicate. Strong over weak → replace. }
|
||||
if (Sym.Bind = STB_GLOBAL) and Existing.Defined
|
||||
and (not Existing.IsWeakSlot) then
|
||||
raise ELinker.Create('duplicate symbol: ' + Sym.Name);
|
||||
if Sym.Bind = STB_GLOBAL then
|
||||
begin
|
||||
Existing.Addr := Base + Sym.Value;
|
||||
Existing.Defined := True;
|
||||
Existing.IsFunc := Sym.SymType = STT_FUNC;
|
||||
Existing.IsWeakSlot := False;
|
||||
end;
|
||||
Continue;
|
||||
end;
|
||||
|
||||
NewSym := TLinkSymbol.Create();
|
||||
NewSym.Name := Sym.Name;
|
||||
NewSym.Addr := Base + Sym.Value;
|
||||
NewSym.Defined := True;
|
||||
NewSym.IsFunc := Sym.SymType = STT_FUNC;
|
||||
NewSym.IsWeakSlot := Sym.Bind = STB_WEAK;
|
||||
FSymbols.Add(NewSym);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Linker-synthesised symbols. Phase B has no GOT, so
|
||||
_GLOBAL_OFFSET_TABLE_ resolves to the writable run base (harmless
|
||||
for the standalone path that does not touch it); __bss_start/_edata/
|
||||
_end mark the data/bss boundaries; __TMC_END__ resolves to _end.
|
||||
Only defined if not already provided by an input object. }
|
||||
procedure TLinker.DefineSynthSymbols;
|
||||
var
|
||||
I: Integer;
|
||||
M: TMergedSection;
|
||||
DataEnd, BssStart, BssEnd, WritableBase: Int64;
|
||||
A: Int64;
|
||||
begin
|
||||
DataEnd := FTarget.BaseAddr;
|
||||
BssStart := -1;
|
||||
BssEnd := FTarget.BaseAddr;
|
||||
WritableBase := -1;
|
||||
|
||||
for I := 0 to FSecAddr.Count - 1 do
|
||||
begin
|
||||
M := FSecAddr.Get(I);
|
||||
A := FAddrOf.Get(I);
|
||||
if (M.Flags and SHF_WRITE) <> 0 then
|
||||
begin
|
||||
if WritableBase < 0 then WritableBase := A;
|
||||
if M.ShType = SHT_NOBITS then
|
||||
begin
|
||||
if BssStart < 0 then BssStart := A;
|
||||
if A + M.Size > BssEnd then BssEnd := A + M.Size;
|
||||
end
|
||||
else
|
||||
begin
|
||||
if A + M.Size > DataEnd then DataEnd := A + M.Size;
|
||||
if A + M.Size > BssEnd then BssEnd := A + M.Size;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
if BssStart < 0 then BssStart := DataEnd;
|
||||
if WritableBase < 0 then WritableBase := DataEnd;
|
||||
|
||||
Self.AddSynthSymbol('_GLOBAL_OFFSET_TABLE_', WritableBase);
|
||||
Self.AddSynthSymbol('__bss_start', BssStart);
|
||||
Self.AddSynthSymbol('_edata', DataEnd);
|
||||
Self.AddSynthSymbol('_end', BssEnd);
|
||||
Self.AddSynthSymbol('__TMC_END__', BssEnd);
|
||||
end;
|
||||
|
||||
{ Resolve a relocation's symbol to its final virtual address. A
|
||||
reference to a STB_LOCAL section/symbol resolves through that
|
||||
object's own section placement; a global reference goes through the
|
||||
resolved symbol table. A strong undefined symbol with no definition
|
||||
is a link error; a weak undefined resolves to 0. }
|
||||
function TLinker.ResolveSymbolAddr(AObj: TElfObjectFile; ASymIdx: Integer;
|
||||
const AContext: string): Int64;
|
||||
var
|
||||
Sym: TRdSymbol;
|
||||
Oi: Integer;
|
||||
Base: Int64;
|
||||
G: TLinkSymbol;
|
||||
begin
|
||||
if (ASymIdx < 0) or (ASymIdx >= AObj.Symbols.Count) then
|
||||
raise ELinker.Create(AContext + ': relocation symbol index out of range');
|
||||
Sym := AObj.Symbols.Get(ASymIdx);
|
||||
|
||||
{ Locally-defined (any binding) symbol: resolve via its section. }
|
||||
if (Sym.Shndx <> SHN_UNDEF) and (Sym.Shndx <> SHN_ABS)
|
||||
and (Sym.Shndx <> SHN_COMMON) then
|
||||
begin
|
||||
Oi := FObjects.IndexOf(AObj);
|
||||
Base := Self.PlacementBaseAddr(Oi, Sym.Shndx);
|
||||
if Base < 0 then
|
||||
raise ELinker.Create(AContext + ': symbol ' + Sym.Name
|
||||
+ ' defined in an unlaid-out section');
|
||||
Result := Base + Sym.Value;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if Sym.Shndx = SHN_ABS then
|
||||
begin
|
||||
Result := Sym.Value;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
{ Undefined here — look up the global table. }
|
||||
G := Self.FindSymbol(Sym.Name);
|
||||
if (G <> nil) and G.Defined and (not G.IsWeakSlot) then
|
||||
begin
|
||||
Result := G.Addr;
|
||||
Exit;
|
||||
end;
|
||||
if (G <> nil) and G.Defined then { resolved weak slot }
|
||||
begin
|
||||
Result := G.Addr;
|
||||
Exit;
|
||||
end;
|
||||
{ Weak undefined resolves to 0; strong undefined is an error. }
|
||||
if Sym.Bind = STB_WEAK then
|
||||
begin
|
||||
Result := 0;
|
||||
Exit;
|
||||
end;
|
||||
raise ELinker.Create('undefined reference to `' + Sym.Name + '''');
|
||||
end;
|
||||
|
||||
{ Patch the merged section bytes for every relocation. Phase B
|
||||
supports the intra-program PC-relative forms only. }
|
||||
procedure TLinker.ApplyRelocations;
|
||||
var
|
||||
Oi, Ri: Integer;
|
||||
Obj: TElfObjectFile;
|
||||
Rel: TRdReloc;
|
||||
M: TMergedSection;
|
||||
P: TSectionPlacement;
|
||||
PAddr: Int64; { virtual address of the patched bytes (P) }
|
||||
PFileOff: Integer; { offset of the patched bytes within M.Data }
|
||||
S, Val: Int64;
|
||||
Ctx: string;
|
||||
begin
|
||||
for Oi := 0 to FObjects.Count - 1 do
|
||||
begin
|
||||
Obj := FObjects.Get(Oi);
|
||||
for Ri := 0 to Obj.Relocs.Count - 1 do
|
||||
begin
|
||||
Rel := Obj.Relocs.Get(Ri);
|
||||
P := FMerger.PlacementOf(Oi, Rel.TargetSection);
|
||||
if P = nil then Continue; { reloc in a dropped section }
|
||||
M := P.Merged;
|
||||
if Self.MergedAddr(M) < 0 then Continue;
|
||||
|
||||
Ctx := Obj.SourceName;
|
||||
PFileOff := Integer(P.Offset + Rel.Offset);
|
||||
PAddr := Self.MergedAddr(M) + P.Offset + Rel.Offset;
|
||||
S := Self.ResolveSymbolAddr(Obj, Rel.SymIndex, Ctx);
|
||||
|
||||
case Rel.RelocType of
|
||||
R_X86_64_NONE: ;
|
||||
R_X86_64_PC32, R_X86_64_PLT32:
|
||||
begin
|
||||
{ Intra-program PC-relative: S + A - P. A PLT32 against an
|
||||
internally-defined symbol relaxes to the same direct
|
||||
computation (no PLT in Phase B). }
|
||||
Val := S + Rel.Addend - PAddr;
|
||||
if M.ShType = SHT_NOBITS then
|
||||
raise ELinker.Create(Ctx
|
||||
+ ': relocation into a NOBITS section');
|
||||
LkPatch32(M.Data, PFileOff, Val and $FFFFFFFF);
|
||||
end;
|
||||
R_X86_64_64:
|
||||
raise ELinker.Create(Ctx + ': R_X86_64_64 relocation against `'
|
||||
+ Obj.Symbols.Get(Rel.SymIndex).Name
|
||||
+ ''' needs dynamic linking (Phase C)');
|
||||
R_X86_64_32, R_X86_64_32S:
|
||||
raise ELinker.Create(Ctx
|
||||
+ ': absolute 32-bit relocation is unsupported (Phase B is '
|
||||
+ 'PC-relative only)');
|
||||
R_X86_64_GOTPCREL, R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX,
|
||||
R_X86_64_TPOFF32:
|
||||
raise ELinker.Create(Ctx
|
||||
+ ': GOT/TLS relocation needs dynamic linking (Phase C)');
|
||||
else
|
||||
raise ELinker.Create(Ctx + ': unsupported relocation type '
|
||||
+ IntToStr(Rel.RelocType));
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Build the ET_EXEC byte image: ELF header, two PT_LOAD program
|
||||
headers (exec run, write run), section payloads at file offsets that
|
||||
match (vaddr - base), then a minimal section-header table so
|
||||
readelf/objdump can inspect the result. }
|
||||
function TLinker.EmitExecutable(AEntry: Int64): string;
|
||||
var
|
||||
Buf: string;
|
||||
I: Integer;
|
||||
M: TMergedSection;
|
||||
A: Int64;
|
||||
Base, PageSz: Int64;
|
||||
ExecLo, ExecHi, WriteLo, WriteMemHi, WriteFileHi: Int64;
|
||||
PhOff, FirstSecFileEnd: Integer;
|
||||
ShStr: string;
|
||||
ShStrOff: TList<Integer>;
|
||||
SecCount, ShTabOff: Integer;
|
||||
NamePos: Integer;
|
||||
begin
|
||||
Base := FTarget.BaseAddr;
|
||||
PageSz := FTarget.PageSize;
|
||||
|
||||
{ Compute the address extents of each run. }
|
||||
ExecLo := -1; ExecHi := Base;
|
||||
WriteLo := -1; WriteMemHi := Base; WriteFileHi := Base;
|
||||
for I := 0 to FSecAddr.Count - 1 do
|
||||
begin
|
||||
M := FSecAddr.Get(I);
|
||||
A := FAddrOf.Get(I);
|
||||
if (M.Flags and SHF_WRITE) <> 0 then
|
||||
begin
|
||||
if WriteLo < 0 then WriteLo := A;
|
||||
if A + M.Size > WriteMemHi then WriteMemHi := A + M.Size;
|
||||
if (M.ShType <> SHT_NOBITS) and (A + M.Size > WriteFileHi) then
|
||||
WriteFileHi := A + M.Size;
|
||||
end
|
||||
else
|
||||
begin
|
||||
if ExecLo < 0 then ExecLo := A;
|
||||
if A + M.Size > ExecHi then ExecHi := A + M.Size;
|
||||
end;
|
||||
end;
|
||||
if ExecLo < 0 then ExecLo := Base + ELF64_EHDR_SIZE;
|
||||
if WriteLo < 0 then begin WriteLo := ExecHi; WriteFileHi := ExecHi;
|
||||
WriteMemHi := ExecHi; end;
|
||||
|
||||
{ ---- ELF header + program headers (assembled front-to-back) ---- }
|
||||
PhOff := ELF64_EHDR_SIZE;
|
||||
Buf := '';
|
||||
Buf := Buf + Chr($7F) + 'ELF'; { e_ident magic }
|
||||
Buf := Buf + Chr(ELFCLASS64) + Chr(ELFDATA2LSB) + Chr(EV_CURRENT)
|
||||
+ Chr(FTarget.OSABI);
|
||||
Buf := Buf + LkZeros(8); { e_ident[8..15] }
|
||||
Buf := Buf + LkLE(ET_EXEC, 2); { e_type }
|
||||
Buf := Buf + LkLE(FTarget.EMachine, 2); { e_machine }
|
||||
Buf := Buf + LkLE(EV_CURRENT, 4); { e_version }
|
||||
Buf := Buf + LkLE(AEntry, 8); { e_entry }
|
||||
Buf := Buf + LkLE(PhOff, 8); { e_phoff }
|
||||
Buf := Buf + LkLE(0, 8); { e_shoff (patched later) }
|
||||
Buf := Buf + LkLE(0, 4); { e_flags }
|
||||
Buf := Buf + LkLE(ELF64_EHDR_SIZE, 2); { e_ehsize }
|
||||
Buf := Buf + LkLE(56, 2); { e_phentsize }
|
||||
Buf := Buf + LkLE(2, 2); { e_phnum (2 PT_LOAD) }
|
||||
Buf := Buf + LkLE(ELF64_SHDR_SIZE, 2); { e_shentsize }
|
||||
Buf := Buf + LkLE(0, 2); { e_shnum (patched later) }
|
||||
Buf := Buf + LkLE(0, 2); { e_shstrndx (patched later) }
|
||||
|
||||
{ PT_LOAD #0 — executable run; covers the headers (file offset 0). }
|
||||
Buf := Buf + LkLE(PT_LOAD, 4) + LkLE(PF_R or PF_X, 4);
|
||||
Buf := Buf + LkLE(0, 8); { p_offset }
|
||||
Buf := Buf + LkLE(Base, 8); { p_vaddr }
|
||||
Buf := Buf + LkLE(Base, 8); { p_paddr }
|
||||
Buf := Buf + LkLE(Self.FileOffset(ExecHi), 8); { p_filesz }
|
||||
Buf := Buf + LkLE(ExecHi - Base, 8); { p_memsz }
|
||||
Buf := Buf + LkLE(PageSz, 8); { p_align }
|
||||
|
||||
{ PT_LOAD #1 — writable run (data + bss). }
|
||||
Buf := Buf + LkLE(PT_LOAD, 4) + LkLE(PF_R or PF_W, 4);
|
||||
Buf := Buf + LkLE(Self.FileOffset(WriteLo), 8); { p_offset }
|
||||
Buf := Buf + LkLE(WriteLo, 8); { p_vaddr }
|
||||
Buf := Buf + LkLE(WriteLo, 8); { p_paddr }
|
||||
Buf := Buf + LkLE(WriteFileHi - WriteLo, 8);{ p_filesz }
|
||||
Buf := Buf + LkLE(WriteMemHi - WriteLo, 8); { p_memsz }
|
||||
Buf := Buf + LkLE(PageSz, 8); { p_align }
|
||||
|
||||
{ Pad headers out to the first section's file offset (the exec run's
|
||||
first byte sits at Self.FileOffset(ExecLo)). }
|
||||
if Length(Buf) < Self.FileOffset(ExecLo) then
|
||||
Buf := Buf + LkZeros(Self.FileOffset(ExecLo) - Length(Buf));
|
||||
|
||||
{ ---- section payloads ---- }
|
||||
{ Grow the image to the end of writable file data, then splat every
|
||||
PROGBITS section at its file offset (= vaddr - Base). }
|
||||
FirstSecFileEnd := Self.FileOffset(WriteFileHi);
|
||||
if Length(Buf) < FirstSecFileEnd then
|
||||
Buf := Buf + LkZeros(FirstSecFileEnd - Length(Buf));
|
||||
for I := 0 to FSecAddr.Count - 1 do
|
||||
begin
|
||||
M := FSecAddr.Get(I);
|
||||
A := FAddrOf.Get(I);
|
||||
if M.ShType = SHT_NOBITS then Continue;
|
||||
if Length(M.Data) > 0 then
|
||||
LkCopyInto(Buf, Self.FileOffset(A), M.Data);
|
||||
end;
|
||||
|
||||
{ ---- section header table (for tooling) ---- }
|
||||
{ .shstrtab: NULL, one name per laid-out section, then .shstrtab. }
|
||||
ShStr := Chr(0);
|
||||
ShStrOff := TList<Integer>.Create();
|
||||
try
|
||||
for I := 0 to FSecAddr.Count - 1 do
|
||||
begin
|
||||
ShStrOff.Add(Length(ShStr));
|
||||
ShStr := ShStr + FSecAddr.Get(I).Name + Chr(0);
|
||||
end;
|
||||
NamePos := Length(ShStr);
|
||||
ShStr := ShStr + '.shstrtab' + Chr(0);
|
||||
|
||||
ShTabOff := Length(Buf);
|
||||
Buf := Buf + ShStr;
|
||||
|
||||
SecCount := FSecAddr.Count + 2; { NULL + sections + .shstrtab }
|
||||
while (Length(Buf) and 7) <> 0 do Buf := Buf + Chr(0);
|
||||
|
||||
{ Patch e_shoff / e_shnum / e_shstrndx now that the table offset is
|
||||
known. e_shoff @40, e_shnum @60, e_shstrndx @62. }
|
||||
LkCopyInto(Buf, 40, LkLE(Length(Buf), 8));
|
||||
LkCopyInto(Buf, 60, LkLE(SecCount, 2));
|
||||
LkCopyInto(Buf, 62, LkLE(SecCount - 1, 2));
|
||||
|
||||
{ SHT_NULL header. }
|
||||
Buf := Buf + LkZeros(ELF64_SHDR_SIZE);
|
||||
|
||||
for I := 0 to FSecAddr.Count - 1 do
|
||||
begin
|
||||
M := FSecAddr.Get(I);
|
||||
A := FAddrOf.Get(I);
|
||||
Buf := Buf + LkLE(ShStrOff.Get(I), 4); { sh_name }
|
||||
Buf := Buf + LkLE(M.ShType, 4); { sh_type }
|
||||
Buf := Buf + LkLE(M.Flags, 8); { sh_flags }
|
||||
Buf := Buf + LkLE(A, 8); { sh_addr }
|
||||
if M.ShType = SHT_NOBITS then
|
||||
Buf := Buf + LkLE(Self.FileOffset(WriteFileHi), 8) { sh_offset }
|
||||
else
|
||||
Buf := Buf + LkLE(Self.FileOffset(A), 8);
|
||||
Buf := Buf + LkLE(M.Size, 8); { sh_size }
|
||||
Buf := Buf + LkLE(0, 4); { sh_link }
|
||||
Buf := Buf + LkLE(0, 4); { sh_info }
|
||||
Buf := Buf + LkLE(M.Align, 8); { sh_addralign }
|
||||
Buf := Buf + LkLE(0, 8); { sh_entsize }
|
||||
end;
|
||||
|
||||
{ .shstrtab section header. }
|
||||
Buf := Buf + LkLE(NamePos, 4);
|
||||
Buf := Buf + LkLE(SHT_STRTAB, 4);
|
||||
Buf := Buf + LkLE(0, 8);
|
||||
Buf := Buf + LkLE(0, 8);
|
||||
Buf := Buf + LkLE(ShTabOff, 8);
|
||||
Buf := Buf + LkLE(Length(ShStr), 8);
|
||||
Buf := Buf + LkLE(0, 4);
|
||||
Buf := Buf + LkLE(0, 4);
|
||||
Buf := Buf + LkLE(1, 8);
|
||||
Buf := Buf + LkLE(0, 8);
|
||||
finally
|
||||
ShStrOff.Free();
|
||||
end;
|
||||
|
||||
Result := Buf;
|
||||
end;
|
||||
|
||||
function TLinker.LinkToBytes(const AEntryName: string): string;
|
||||
var
|
||||
Sym: TLinkSymbol;
|
||||
begin
|
||||
Self.LayoutSections();
|
||||
Self.BuildSymbols();
|
||||
Self.DefineSynthSymbols();
|
||||
Self.ApplyRelocations();
|
||||
|
||||
Sym := Self.FindSymbol(AEntryName);
|
||||
if (Sym = nil) or (not Sym.Defined) or Sym.IsWeakSlot then
|
||||
raise ELinker.Create('entry symbol not found: ' + AEntryName);
|
||||
FEntry := Sym.Addr;
|
||||
Result := Self.EmitExecutable(FEntry);
|
||||
end;
|
||||
|
||||
procedure TLinker.Link(const AEntryName, AOutputPath: string);
|
||||
var
|
||||
Bytes: string;
|
||||
FOut: TFileOutputStream;
|
||||
begin
|
||||
Bytes := Self.LinkToBytes(AEntryName);
|
||||
FOut := TFileOutputStream.Create(AOutputPath);
|
||||
try
|
||||
FOut.Write(PChar(Bytes), Length(Bytes));
|
||||
FOut.Flush();
|
||||
finally
|
||||
FOut.Close();
|
||||
FOut.Free();
|
||||
end;
|
||||
MakeFileExecutable(AOutputPath);
|
||||
end;
|
||||
|
||||
function TLinker.AddrOfSymbol(const AName: string): Int64;
|
||||
var
|
||||
S: TLinkSymbol;
|
||||
begin
|
||||
S := Self.FindSymbol(AName);
|
||||
if S = nil then
|
||||
Result := -1
|
||||
else
|
||||
Result := S.Addr;
|
||||
end;
|
||||
|
||||
function TLinker.FindMerged(const AName: string): TMergedSection;
|
||||
begin
|
||||
Result := FMerger.FindMerged(AName);
|
||||
end;
|
||||
|
||||
function TLinker.FindMergedText: TMergedSection;
|
||||
begin
|
||||
Result := FMerger.FindMerged('.text');
|
||||
end;
|
||||
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -8,14 +8,17 @@
|
|||
|
||||
unit cp.test.linker;
|
||||
|
||||
{ Tests for the internal linker's input layer (Phase A):
|
||||
blaise.elfreader — ELF relocatable-object parsing and ar-archive
|
||||
parsing with GNU long-name support. }
|
||||
{ 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. }
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
SysUtils, blaise.testing, Generics.Collections,
|
||||
SysUtils, process, blaise.testing, Generics.Collections,
|
||||
blaise.elfreader, blaise.linker.elf, blaise.assembler.x86_64;
|
||||
|
||||
type
|
||||
|
|
@ -42,6 +45,37 @@ type
|
|||
procedure TestMerge_SkipsBookkeepingSections;
|
||||
end;
|
||||
|
||||
TLinkerTests = class(TTestCase)
|
||||
private
|
||||
function LinkObjs(AObjAsm: array of string;
|
||||
const AEntry: string; out ABytes: string): TLinker;
|
||||
published
|
||||
{ Symbol resolution }
|
||||
procedure TestSym_GlobalResolvesToVaddr;
|
||||
procedure TestSym_TwoGlobalsDuplicate_Raises;
|
||||
procedure TestSym_StrongUndefined_Raises;
|
||||
procedure TestSym_WeakUndefinedResolvesToZero;
|
||||
procedure TestSym_SynthesisedSymbolsDefined;
|
||||
{ Static relocations }
|
||||
procedure TestReloc_PC32CrossObjectCall;
|
||||
procedure TestReloc_Quad64_Raises;
|
||||
{ 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;
|
||||
|
||||
implementation
|
||||
|
||||
function TElfReaderTests.ProjectRoot: string;
|
||||
|
|
@ -403,8 +437,449 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ ---- 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;
|
||||
|
||||
procedure TLinkerTests.TestSym_TwoGlobalsDuplicate_Raises;
|
||||
var
|
||||
Lk: TLinker;
|
||||
Bytes: string;
|
||||
Raised: Boolean;
|
||||
begin
|
||||
Raised := False;
|
||||
Lk := nil;
|
||||
try
|
||||
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);
|
||||
except
|
||||
on E: ELinker do
|
||||
Raised := True;
|
||||
end;
|
||||
Lk.Free();
|
||||
AssertTrue('duplicate global symbol must raise ELinker', Raised);
|
||||
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;
|
||||
|
||||
procedure TLinkerTests.TestReloc_Quad64_Raises;
|
||||
var
|
||||
Lk: TLinker;
|
||||
Bytes: string;
|
||||
Raised: Boolean;
|
||||
begin
|
||||
{ An absolute 64-bit pointer to a symbol needs dynamic linking under
|
||||
a real PIE; Phase B rejects R_X86_64_64 explicitly. }
|
||||
Raised := False;
|
||||
Lk := nil;
|
||||
try
|
||||
Lk := LinkObjs(
|
||||
['.data' + LineEnding + 'ptr:' + LineEnding + '.quad target' + LineEnding +
|
||||
'.text' + LineEnding + '.globl target' + LineEnding + 'target:' +
|
||||
LineEnding + 'ret' + LineEnding +
|
||||
'.globl _start' + LineEnding + '_start:' + LineEnding + 'ret' + LineEnding],
|
||||
'_start', Bytes);
|
||||
except
|
||||
on E: ELinker do
|
||||
Raised := True;
|
||||
end;
|
||||
Lk.Free();
|
||||
AssertTrue('R_X86_64_64 must raise ELinker in Phase B', Raised);
|
||||
end;
|
||||
|
||||
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;
|
||||
|
||||
initialization
|
||||
RegisterTest(TElfReaderTests);
|
||||
RegisterTest(TSectionMergerTests);
|
||||
RegisterTest(TLinkerTests);
|
||||
RegisterTest(TLinkerE2ETests);
|
||||
|
||||
end.
|
||||
|
|
|
|||
Loading…
Reference in a new issue