diff --git a/compiler/src/main/pascal/blaise.linker.elf.pas b/compiler/src/main/pascal/blaise.linker.elf.pas index 6b6ba55..d8804b6 100644 --- a/compiler/src/main/pascal/blaise.linker.elf.pas +++ b/compiler/src/main/pascal/blaise.linker.elf.pas @@ -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 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; + FOwned: TList; { objects we must free } + FMerger: TSectionMerger; + FSymbols: TList; + FSecAddr: TList; { merged sections, in layout order } + FAddrOf: TList; { 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.Create(); + FOwned := TList.Create(); + FMerger := TSectionMerger.Create(); + FSymbols := TList.Create(); + FSecAddr := TList.Create(); + FAddrOf := TList.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; + 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.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. diff --git a/compiler/src/test/pascal/cp.test.linker.pas b/compiler/src/test/pascal/cp.test.linker.pas index e652ab7..1fd21d4 100644 --- a/compiler/src/test/pascal/cp.test.linker.pas +++ b/compiler/src/test/pascal/cp.test.linker.pas @@ -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.