1010 lines
39 KiB
Plaintext
1010 lines
39 KiB
Plaintext
= Internal Linker Design
|
||
:author: Graeme Geldenhuys
|
||
:revdate: 2026-06-12
|
||
:toc:
|
||
:sectnums:
|
||
|
||
== Motivation
|
||
|
||
The Blaise compiler currently depends on an external C compiler driver (`cc` /
|
||
`gcc` / `clang`) to link executables. This is the last remaining external
|
||
toolchain dependency — the internal assembler and ELF object writer
|
||
(`--assembler internal`) already eliminated the dependency on an external
|
||
assembler. Replacing the linker completes the toolchain-independence goal:
|
||
a single `blaise` binary that compiles Pascal source to a runnable executable
|
||
with no external tools.
|
||
|
||
Benefits:
|
||
|
||
* **Simpler bootstrapping.** A self-contained compiler binary avoids the
|
||
chicken-and-egg problem of needing an external linker to build the linker.
|
||
Cold bootstrap reduces to: previous release binary → new binary. Done.
|
||
* **Portable builds.** Users on minimal systems (containers, embedded, CI
|
||
images) do not need to install a C toolchain.
|
||
* **Deterministic output.** The compiler controls every byte of the output
|
||
binary. No variation from different `cc` versions, CRT object versions,
|
||
or default flag differences across distributions.
|
||
* **Faster builds.** Eliminating the fork+exec of `cc` (which itself forks
|
||
`collect2` → `ld`) removes ~50ms of process overhead per link.
|
||
|
||
== Scope and Non-Goals
|
||
|
||
=== In scope
|
||
|
||
* Link `.o` files (from the internal assembler) and `.a` archives (the RTL)
|
||
into a dynamically-linked ELF executable on Linux x86-64.
|
||
* Resolve relocations: `R_X86_64_PC32`, `R_X86_64_PLT32`, `R_X86_64_64`,
|
||
`R_X86_64_32S`, `R_X86_64_GOTPCREL`, `R_X86_64_TPOFF32`, and their
|
||
relaxable variants (`R_X86_64_GOTPCRELX`, `R_X86_64_REX_GOTPCRELX`).
|
||
* Produce PIE executables (ET_DYN with PT_INTERP) — the same format `cc`
|
||
produces by default on modern Linux.
|
||
* Emit a GOT for external data symbols and a PLT with eager binding
|
||
(`DT_FLAGS BIND_NOW`) for external function calls.
|
||
* Emit `.rela.dyn` with `R_X86_64_RELATIVE` entries for absolute 64-bit
|
||
relocations in writable sections (vtable pointers, typeinfo, etc.) —
|
||
required for PIE because the load address is not known at link time.
|
||
* Emit a `.dynamic` section with `DT_NEEDED` entries for `libc.so.6` (and
|
||
conditionally `libm.so.6` / `libpthread.so.0` for glibc < 2.34).
|
||
* Handle TLS variables (`.tbss` and `.tdata` sections,
|
||
`R_X86_64_TPOFF32`, `PT_TLS` segment).
|
||
* Merge `.init_array` / `.fini_array` sections and emit corresponding
|
||
`DT_INIT_ARRAY` / `DT_FINI_ARRAY` dynamic tags.
|
||
* Merge CRT startup objects (`Scrt1.o`, `crti.o`, `crtn.o`) — these are
|
||
system-provided and supply `_start`, `.init`, and `.fini`.
|
||
* Define linker-synthesised symbols: `_GLOBAL_OFFSET_TABLE_`,
|
||
`__bss_start`, `_edata`, `_end`, `__TMC_END__`.
|
||
* Resolve weak undefined symbols (e.g. `__gmon_start__`,
|
||
`_ITM_registerTMCloneTable` from CRT objects) to address 0 without error.
|
||
* Pass through OPDF debug sections (`.opdf.*`) from input objects into the
|
||
final executable, preserving the debug workflow for pdr.
|
||
|
||
=== Not in scope (future work)
|
||
|
||
* **Static linking** — linking libc statically (`-static`) requires pulling
|
||
in hundreds of libc `.o` files and resolving IFUNC dispatchers. The
|
||
internal linker links dynamically against `libc.so.6` (same as today).
|
||
* **macOS / Mach-O linking** — macOS uses Mach-O, not ELF. The linker will
|
||
be structured to allow a future Mach-O backend, but this design covers
|
||
ELF only.
|
||
* **Windows / PE-COFF linking** — Windows uses PE format with import
|
||
libraries. Deferred.
|
||
* **Shared library output** — producing `.so` files. The compiler only
|
||
produces executables.
|
||
* **Link-time optimisation (LTO)** — no IR-level linking.
|
||
* **Linker scripts** — not needed for simple executables.
|
||
* **`.eh_frame` merging** — CRT objects carry `.eh_frame` sections. The
|
||
internal linker will pass them through unmerged (or drop them) rather than
|
||
implementing the `.eh_frame_hdr` binary-search table. The compiler's own
|
||
exception handling uses `setjmp`/`longjmp`, not DWARF unwinding.
|
||
|
||
== PIE Across Target Platforms
|
||
|
||
The design targets PIE (Position-Independent Executable) output because it
|
||
is the modern default and required for ASLR security. This section records
|
||
the PIE landscape for all target platforms in the Blaise roadmap.
|
||
|
||
=== Linux
|
||
|
||
PIE is the default output format of `gcc` and `clang` on all modern
|
||
distributions (Ubuntu 17.10+, Fedora 23+, Debian 10+, Arch). GCC is
|
||
configured with `--enable-default-pie`. The ELF type is `ET_DYN` with a
|
||
`PT_INTERP` segment pointing to `/lib64/ld-linux-x86-64.so.2`.
|
||
|
||
Our internal linker produces this format. Non-PIE (`ET_EXEC`) would be
|
||
simpler but is increasingly treated as a second-class citizen by toolchains
|
||
and some hardened kernels refuse to run non-PIE executables.
|
||
|
||
=== FreeBSD
|
||
|
||
FreeBSD 13+ uses LLVM/Clang as the system compiler. PIE executables are
|
||
fully supported. FreeBSD's `cc` (clang) defaults to PIE on amd64.
|
||
The ELF structure is the same as Linux with the following differences:
|
||
|
||
* **Dynamic linker path:** `/libexec/ld-elf.so.1` (not
|
||
`/lib64/ld-linux-x86-64.so.2`).
|
||
* **ELF OS/ABI brand:** FreeBSD's image activator checks
|
||
`EI_OSABI = ELFOSABI_FREEBSD` (9) or an `NT_FREEBSD_ABI_TAG` note. The
|
||
existing ELF writer hardcodes `ELFOSABI_NONE` — the executable writer
|
||
must parameterise this per target.
|
||
* **CRT objects:** `Scrt1.o`, `crti.o`, `crtbeginS.o`, `crtendS.o`,
|
||
`crtn.o` from `/usr/lib/`.
|
||
* **No libgcc:** FreeBSD uses compiler-rt instead of libgcc.
|
||
* **libc version:** `libc.so.7` instead of `libc.so.6`.
|
||
* **Symbol versioning:** FreeBSD libc uses `FBSD_1.x` symbol versions.
|
||
Unversioned symbol references work (the dynamic linker binds to the
|
||
default version), so the internal linker does not need to emit
|
||
`.gnu.version` / `.gnu.version_r` sections initially.
|
||
|
||
The relocation types, GOT/PLT layout, and section merging are identical to
|
||
Linux.
|
||
|
||
=== macOS
|
||
|
||
macOS uses Mach-O, not ELF. PIE is the default and effectively mandatory
|
||
since macOS 10.7 (Lion). The Mach-O header includes an `MH_PIE` flag.
|
||
Apple's linker (`ld64` / `ld-prime`) produces PIE by default.
|
||
|
||
The internal linker's ELF code does not apply to macOS. A future Mach-O
|
||
linker backend would be a separate implementation behind the `ILinker`
|
||
interface. The segment/section model (segments containing sections, with
|
||
`__TEXT` and `__DATA` segments) differs from ELF's flat section model.
|
||
|
||
=== Windows
|
||
|
||
Windows uses PE (Portable Executable) format. The equivalent of PIE is
|
||
ASLR, enabled by the `/DYNAMICBASE` linker flag (default since MSVC 2012).
|
||
PE executables include a `.reloc` section with base relocations. The OS
|
||
kernel relocates the entire image at load time.
|
||
|
||
Like macOS, a Windows PE linker would be a separate implementation. The PE
|
||
format has import tables (`.idata`), export tables, and a fundamentally
|
||
different relocation model from ELF.
|
||
|
||
== Architecture
|
||
|
||
=== Interface Design
|
||
|
||
The linker integrates into the compiler via a strategy interface, mirroring
|
||
the existing `--assembler internal|external` pattern:
|
||
|
||
[source,pascal]
|
||
----
|
||
type
|
||
ILinker = interface
|
||
procedure Link(const AInputFiles: array of string;
|
||
const AOutputFile: string;
|
||
const ATarget: TTargetDesc);
|
||
end;
|
||
|
||
TInternalLinker = class(TObject, ILinker)
|
||
procedure Link(const AInputFiles: array of string;
|
||
const AOutputFile: string;
|
||
const ATarget: TTargetDesc);
|
||
end;
|
||
|
||
TExternalLinker = class(TObject, ILinker)
|
||
procedure Link(const AInputFiles: array of string;
|
||
const AOutputFile: string;
|
||
const ATarget: TTargetDesc);
|
||
end;
|
||
----
|
||
|
||
`TExternalLinker` wraps the current `cc` invocation (extracting the existing
|
||
code from `LinkObjectFile` / `CompileToNativeDirect` in `Blaise.pas`).
|
||
|
||
`TInternalLinker` implements the ELF linking pipeline described below.
|
||
|
||
The compiler selects the implementation via a `--linker internal|external`
|
||
CLI flag, defaulting to `external` initially (switched to `internal` once
|
||
validated).
|
||
|
||
=== Source Layout
|
||
|
||
[cols="1,3"]
|
||
|===
|
||
| Unit | Purpose
|
||
|
||
| `blaise.linker.pas`
|
||
| `ILinker` interface, `TExternalLinker`, factory function
|
||
|
||
| `blaise.linker.elf.pas`
|
||
| `TInternalLinker` — ELF executable writer, relocation resolver, GOT/PLT
|
||
builder, section merger
|
||
|
||
| `blaise.elfreader.pas`
|
||
| ELF `.o` and `.a` reader — parses ELF headers, section headers, symbol
|
||
tables, relocation entries from input files
|
||
|
||
| `blaise.elfwriter.pas`
|
||
| _Existing_ — ELF relocatable object writer (used by the assembler).
|
||
Shared constants and low-level helpers reused by the linker.
|
||
|===
|
||
|
||
=== Data Flow
|
||
|
||
[source]
|
||
----
|
||
Input .o files ──┐
|
||
├──→ ELF Reader ──→ Section merge ──→ Symbol resolution
|
||
RTL .a archive ──┘ │
|
||
▼
|
||
Relocation resolution
|
||
│
|
||
┌──────────────────────────────┘
|
||
▼
|
||
GOT/PLT generation (for external symbols)
|
||
│
|
||
▼
|
||
Layout: assign virtual addresses to sections
|
||
│
|
||
▼
|
||
Emit ELF executable: ELF header, program headers,
|
||
sections (.text, .rodata, .data, .bss, .tdata, .tbss,
|
||
.got, .plt, .dynamic, .interp, .dynsym, .dynstr,
|
||
.hash, .rela.dyn, .rela.plt, .init_array, .fini_array,
|
||
.opdf.*), section headers
|
||
----
|
||
|
||
== ELF Linking Pipeline
|
||
|
||
=== Phase 1: Read Input Objects
|
||
|
||
Parse each input `.o` file and each member of the `.a` archive:
|
||
|
||
* Read ELF header — validate `ELFCLASS64`, `ELFDATA2LSB`, `ET_REL`,
|
||
`EM_X86_64`.
|
||
* Read section headers — collect `.text`, `.data`, `.rodata`, `.bss`,
|
||
`.tdata`, `.tbss`, `.init_array`, `.fini_array`, `.init`, `.fini`,
|
||
`.symtab`, `.strtab`, `.rela.*`, and `.opdf.*` sections.
|
||
* Read symbol table — collect all `STB_GLOBAL` and `STB_LOCAL` symbols
|
||
with their section index, value (offset within section), size, and type.
|
||
* Read relocation sections — collect all `RELA` entries (type, symbol
|
||
index, offset, addend).
|
||
|
||
Archive processing: read the `!<arch>\n` magic, parse the GNU extended
|
||
name table (`//` member) for long filenames (required — `blaise_rtl.a`
|
||
contains `blaise_setjmp_x86_64.o` which exceeds the 15-character ar name
|
||
limit), then iterate members and parse each as an ELF object.
|
||
|
||
For the initial implementation, all archive members are included
|
||
unconditionally. The RTL archive contains ~11 curated object files; full
|
||
inclusion is acceptable and avoids the complexity of iterative symbol-
|
||
driven member selection. Selective inclusion (maintain an unresolved
|
||
symbol set, pull members that define needed symbols, iterate until no new
|
||
members are pulled) should be added in Phase D — the infrastructure is
|
||
nearly free since the unresolved-symbol set is already needed for
|
||
"undefined reference" diagnostics.
|
||
|
||
=== Phase 2: Merge Sections
|
||
|
||
Concatenate like-named sections from all input objects:
|
||
|
||
* All `.text` sections → merged `.text`
|
||
* All `.rodata` sections → merged `.rodata`
|
||
* All `.data` sections → merged `.data`
|
||
* All `.bss` sections → merged `.bss` (sizes summed, no data)
|
||
* All `.tdata` sections → merged `.tdata` (initialised TLS data;
|
||
`SHF_TLS | SHF_WRITE`, `SHT_PROGBITS`)
|
||
* All `.tbss` sections → merged `.tbss` (zero-initialised TLS;
|
||
`SHF_TLS | SHF_WRITE`, `SHT_NOBITS`)
|
||
* All `.init_array` sections → merged `.init_array` (constructor pointers)
|
||
* All `.fini_array` sections → merged `.fini_array` (destructor pointers)
|
||
* All `.opdf.*` sections → passed through into the output (OPDF debug
|
||
info for the pdr debugger)
|
||
|
||
Sections from CRT objects (`.init`, `.fini`, `.eh_frame`) are merged or
|
||
dropped as appropriate — `.init`/`.fini` are merged (the CRT prologue/
|
||
epilogue objects bracket them), `.eh_frame` is dropped (the Blaise runtime
|
||
uses `setjmp`/`longjmp`, not DWARF unwinding).
|
||
|
||
For each input section, record its start offset within the merged section.
|
||
This offset is added to all symbol values and relocation offsets from that
|
||
input section.
|
||
|
||
Section alignment: each input section has an `sh_addralign` value. The
|
||
merged section's alignment is the maximum of all input sections'
|
||
alignments. When concatenating, insert padding to satisfy alignment.
|
||
|
||
=== Phase 3: Resolve Symbols
|
||
|
||
Build a global symbol table from all input objects:
|
||
|
||
* For each `STB_GLOBAL` or `STB_WEAK` symbol with a definition
|
||
(`SHN_UNDEF` excluded): if the name is already in the table, a new
|
||
`STB_GLOBAL` wins over `STB_WEAK`; two `STB_GLOBAL` definitions is a
|
||
"duplicate symbol" error.
|
||
* `STB_LOCAL` symbols are not merged — they remain scoped to their input
|
||
object.
|
||
* Undefined symbols (`SHN_UNDEF`): add to the unresolved set.
|
||
|
||
**Linker-defined symbols.** The linker itself must define the following
|
||
symbols, which are referenced by CRT objects and the RTL:
|
||
|
||
[cols="1,3"]
|
||
|===
|
||
| Symbol | Value
|
||
|
||
| `_GLOBAL_OFFSET_TABLE_`
|
||
| Address of the `.got` section (referenced by RTL objects `blaise_exc.o`,
|
||
`blaise_mem.o`)
|
||
|
||
| `__bss_start`
|
||
| Start of `.bss`
|
||
|
||
| `_edata`
|
||
| End of initialised data (end of `.data`)
|
||
|
||
| `_end`
|
||
| End of `.bss` (program break)
|
||
|
||
| `__TMC_END__`
|
||
| Referenced by `crtbeginS.o`; set to end of `.tm_clone_table` or 0
|
||
|===
|
||
|
||
**Weak undefined symbols.** CRT objects reference symbols like
|
||
`__gmon_start__`, `_ITM_registerTMCloneTable`,
|
||
`_ITM_deregisterTMCloneTable`, and `__cxa_finalize` as weak undefined.
|
||
These must resolve to address 0 without error when no definition exists.
|
||
|
||
After processing all inputs, classify remaining unresolved symbols:
|
||
|
||
* **Weak undefined** → resolve to 0 (no error).
|
||
* **Strong undefined, name matches a libc function** → generate GOT/PLT
|
||
entries and `.dynsym` records.
|
||
* **Strong undefined, not found anywhere** → link error ("undefined
|
||
reference to ...").
|
||
|
||
=== Phase 4: Build GOT, PLT, and Dynamic Relocations
|
||
|
||
External symbols (those resolved to shared library functions) need
|
||
indirection. The design uses **eager binding** (`DT_FLAGS BIND_NOW`,
|
||
`DT_FLAGS_1 NOW`) rather than lazy PLT resolution. This is simpler to
|
||
implement (no PLT[0] resolver stub, no `_dl_runtime_resolve` protocol) and
|
||
matches what `cc` produces by default (`-z now`). Eager binding also
|
||
enables full RELRO (`PT_GNU_RELRO` covering the GOT), which hardened
|
||
distributions require.
|
||
|
||
**GOT (Global Offset Table):**
|
||
|
||
* Three reserved entries at the start: GOT[0] = address of `.dynamic`,
|
||
GOT[1] = 0, GOT[2] = 0 (no lazy resolver).
|
||
* One 8-byte entry per external symbol.
|
||
* Located in a writable data segment (becomes read-only after RELRO).
|
||
* At load time, the dynamic linker fills each entry with the symbol's
|
||
actual address (eager, not lazy).
|
||
|
||
**PLT (Procedure Linkage Table):**
|
||
|
||
With eager binding, PLT stubs are simplified — each is a single indirect
|
||
jump through the pre-resolved GOT entry:
|
||
|
||
[source,asm]
|
||
----
|
||
jmpq *GOT_ENTRY(%rip) ; indirect jump through GOT (6 bytes)
|
||
nop ; padding to 16-byte alignment (10 bytes)
|
||
----
|
||
|
||
No PLT[0] resolver stub is needed. The dynamic linker resolves all
|
||
`R_X86_64_JUMP_SLOT` entries at load time before `_start` runs.
|
||
|
||
**`.rela.plt` — PLT relocations:**
|
||
|
||
* One `R_X86_64_JUMP_SLOT` relocation per PLT entry, pointing at the
|
||
corresponding GOT slot.
|
||
* Processed eagerly at load time due to `DT_FLAGS BIND_NOW`.
|
||
|
||
**`.rela.dyn` — data relocations (critical for PIE):**
|
||
|
||
In a PIE executable (`ET_DYN`), absolute 64-bit addresses in writable
|
||
sections cannot be resolved at link time because the load base is unknown.
|
||
Every `R_X86_64_64` relocation against an internally-defined symbol in
|
||
`.data` (vtable pointers, typeinfo records, class-name `.quad` entries)
|
||
must be converted to an `R_X86_64_RELATIVE` dynamic relocation:
|
||
|
||
* The linker patches the data section with the link-time value `S + A`.
|
||
* An `R_X86_64_RELATIVE` entry is emitted in `.rela.dyn` with the offset
|
||
and addend = `S + A`.
|
||
* At load time, the dynamic linker adds the load base to the addend and
|
||
writes the result.
|
||
|
||
The `.dynamic` section must include `DT_RELA`, `DT_RELASZ`,
|
||
`DT_RELAENT`, and `DT_RELACOUNT` tags for `.rela.dyn`.
|
||
|
||
NOTE: The RTL archive contains ~131 `R_X86_64_64` relocations. Every
|
||
vtable/typeinfo `.quad` the native backend emits is one. Without
|
||
`.rela.dyn`, no program using classes can link under PIE.
|
||
|
||
NOTE: `R_X86_64_32S` against a symbol address is a **link error** under
|
||
PIE (it would be a text relocation, which the loader refuses). The
|
||
linker must reject this case with a diagnostic rather than silently
|
||
emitting a wrong value.
|
||
|
||
**GOTPCREL relaxation:**
|
||
|
||
* `R_X86_64_GOTPCRELX` and `R_X86_64_REX_GOTPCRELX` can be relaxed to
|
||
direct `leaq` when the symbol is defined locally. The internal linker
|
||
will handle this for internally-defined symbols.
|
||
|
||
=== Phase 5: Layout
|
||
|
||
Assign virtual addresses to sections. The layout follows the standard
|
||
Linux convention:
|
||
|
||
[source]
|
||
----
|
||
Address space layout:
|
||
|
||
0x0000 ELF header + program headers
|
||
.interp
|
||
.dynsym, .dynstr, .hash
|
||
.rela.dyn, .rela.plt
|
||
─── R segment (read-only, non-executable) ───
|
||
|
||
0x0000 + page .text (merged)
|
||
.init, .fini
|
||
.plt
|
||
─── R+X segment (read + execute) ───
|
||
|
||
next page .rodata (merged)
|
||
─── R segment (read-only) ───
|
||
|
||
next page .init_array, .fini_array
|
||
.dynamic
|
||
.got
|
||
─── RELRO boundary (PT_GNU_RELRO covers above) ───
|
||
.data (merged)
|
||
─── R+W segment (read + write) ───
|
||
|
||
after .data .bss (merged, no file content)
|
||
|
||
TLS block: .tdata (initialised TLS data, file content)
|
||
.tbss (zero-initialised TLS, no file content)
|
||
─── PT_TLS segment ───
|
||
|
||
.opdf.* (debug sections, non-loadable)
|
||
----
|
||
|
||
Each `PT_LOAD` segment starts on a page boundary (4096 bytes).
|
||
|
||
**RELRO.** The `.got`, `.dynamic`, `.init_array`, and `.fini_array`
|
||
sections are placed in the RELRO region — a `PT_GNU_RELRO` program header
|
||
marks them as read-only after relocation. With eager binding
|
||
(`DT_FLAGS BIND_NOW`), the dynamic linker mprotects the GOT to read-only
|
||
after resolving all symbols, preventing GOT-overwrite attacks.
|
||
|
||
**TLS layout.** The `.tdata` and `.tbss` sections form the TLS block,
|
||
described by a `PT_TLS` program header. The TLS offset formula for x86-64
|
||
variant II (used by Linux and FreeBSD) is:
|
||
`TP_offset = -(align_up(tls_size, tls_align))`, where `tls_size` =
|
||
`sizeof(.tdata) + sizeof(.tbss)` and `tls_align` = max alignment of the
|
||
TLS sections. `R_X86_64_TPOFF32` relocations use this negative offset.
|
||
|
||
The entry point is set to the symbol `_start` (provided by `Scrt1.o` from
|
||
the system CRT). `_start` calls `__libc_start_main`, which calls `main`.
|
||
|
||
=== Phase 6: Apply Relocations
|
||
|
||
Walk all relocation entries from the input objects. For each relocation:
|
||
|
||
* Look up the symbol — now has a resolved virtual address.
|
||
* Compute the final value based on relocation type:
|
||
|
||
[cols="2,4"]
|
||
|===
|
||
| Type | Computation
|
||
|
||
| `R_X86_64_PC32`
|
||
| `S + A - P` (32-bit, PC-relative)
|
||
|
||
| `R_X86_64_PLT32`
|
||
| `L + A - P` where L = PLT entry address (32-bit)
|
||
|
||
| `R_X86_64_64`
|
||
| In writable section: patch `S + A`, emit `R_X86_64_RELATIVE` in
|
||
`.rela.dyn`. In read-only section: link error (PIE cannot have text
|
||
relocations).
|
||
|
||
| `R_X86_64_32S`
|
||
| Link error under PIE — absolute 32-bit address is a text relocation
|
||
that the loader refuses. The native backend should not emit this for
|
||
symbol references (only for stack-frame offsets, which are PC-relative).
|
||
|
||
| `R_X86_64_GOTPCREL`
|
||
| `G + GOT + A - P` (32-bit, GOT-relative)
|
||
|
||
| `R_X86_64_TPOFF32`
|
||
| `S + A - align_up(TLS_SIZE, TLS_ALIGN)` (negative offset from TP,
|
||
x86-64 variant II)
|
||
|
||
| `R_X86_64_GOTPCRELX`
|
||
| Relax to `leaq` if local, else `G + GOT + A - P`
|
||
|
||
| `R_X86_64_REX_GOTPCRELX`
|
||
| Same as above with REX prefix
|
||
|===
|
||
|
||
Where: S = symbol value, A = addend, P = relocation offset (virtual
|
||
address of the byte being patched), L = PLT entry address, G = GOT entry
|
||
offset, GOT = GOT base address, TLS_SIZE = total TLS block size.
|
||
|
||
* Patch the bytes at the relocation offset with the computed value.
|
||
|
||
=== Phase 7: Emit Executable
|
||
|
||
Write the final ELF executable file:
|
||
|
||
* **ELF header** — `ET_DYN` (PIE), `EM_X86_64`, entry point = `_start`.
|
||
* **Program headers** — `PT_PHDR`, `PT_INTERP`, `PT_LOAD` × N,
|
||
`PT_DYNAMIC`, `PT_TLS`, `PT_GNU_STACK` (NX stack).
|
||
* **Section contents** — merged and relocated `.text`, `.rodata`, `.data`,
|
||
`.tdata`, `.init_array`, `.fini_array`, `.init`, `.fini`, plus
|
||
generated `.interp`, `.plt`, `.got`, `.dynamic`, `.dynsym`, `.dynstr`,
|
||
`.hash`, `.rela.dyn`, `.rela.plt`, and pass-through `.opdf.*`.
|
||
* **Section headers** — for tooling (objdump, readelf, strip).
|
||
|
||
The `.dynamic` section contains:
|
||
|
||
[cols="1,2"]
|
||
|===
|
||
| Tag | Value
|
||
|
||
| `DT_NEEDED`
|
||
| `libc.so.6` (and conditionally `libpthread.so.0`, `libm.so.6` for
|
||
glibc < 2.34 — see Platform Parameterisation)
|
||
|
||
| `DT_SYMTAB`
|
||
| Address of `.dynsym`
|
||
|
||
| `DT_STRTAB`
|
||
| Address of `.dynstr`
|
||
|
||
| `DT_STRSZ`
|
||
| Size of `.dynstr`
|
||
|
||
| `DT_SYMENT`
|
||
| Size of one `.dynsym` entry (24)
|
||
|
||
| `DT_PLTGOT`
|
||
| Address of `.got`
|
||
|
||
| `DT_PLTRELSZ`
|
||
| Size of `.rela.plt`
|
||
|
||
| `DT_PLTREL`
|
||
| `DT_RELA` (7)
|
||
|
||
| `DT_JMPREL`
|
||
| Address of `.rela.plt`
|
||
|
||
| `DT_RELA`
|
||
| Address of `.rela.dyn`
|
||
|
||
| `DT_RELASZ`
|
||
| Size of `.rela.dyn`
|
||
|
||
| `DT_RELAENT`
|
||
| Size of one RELA entry (24)
|
||
|
||
| `DT_RELACOUNT`
|
||
| Number of `R_X86_64_RELATIVE` entries in `.rela.dyn`
|
||
|
||
| `DT_INIT_ARRAY`
|
||
| Address of `.init_array`
|
||
|
||
| `DT_INIT_ARRAYSZ`
|
||
| Size of `.init_array`
|
||
|
||
| `DT_FINI_ARRAY`
|
||
| Address of `.fini_array`
|
||
|
||
| `DT_FINI_ARRAYSZ`
|
||
| Size of `.fini_array`
|
||
|
||
| `DT_INIT`
|
||
| Address of `.init` (from CRT)
|
||
|
||
| `DT_FINI`
|
||
| Address of `.fini` (from CRT)
|
||
|
||
| `DT_FLAGS`
|
||
| `DF_BIND_NOW` (eager binding)
|
||
|
||
| `DT_FLAGS_1`
|
||
| `DF_1_NOW \| DF_1_PIE`
|
||
|
||
| `DT_HASH`
|
||
| Address of SysV symbol hash table (start with SysV `.hash`; add
|
||
`.gnu.hash` as polish)
|
||
|
||
| `DT_DEBUG`
|
||
| 0 (filled by the dynamic linker; enables `r_debug` rendezvous for
|
||
GDB/pdr shared-library awareness)
|
||
|
||
| `DT_NULL`
|
||
| Terminator
|
||
|===
|
||
|
||
== CRT Objects
|
||
|
||
The C runtime startup objects provide the `_start` entry point and
|
||
initialisation/finalisation scaffolding. On Linux x86-64 with PIE:
|
||
|
||
[cols="1,3"]
|
||
|===
|
||
| Object | Purpose
|
||
|
||
| `Scrt1.o`
|
||
| Defines `_start`, calls `__libc_start_main(main, argc, argv, ...)`
|
||
|
||
| `crti.o`
|
||
| `.init` and `.fini` section prologue
|
||
|
||
| `crtn.o`
|
||
| `.init` and `.fini` section epilogue
|
||
|
||
| `crtbeginS.o`
|
||
| GCC internal — `.ctors`/`.dtors` registration (PIE variant)
|
||
|
||
| `crtendS.o`
|
||
| GCC internal — `.ctors`/`.dtors` termination (PIE variant)
|
||
|===
|
||
|
||
These are small relocatable objects (< 1 KB each) installed by the C
|
||
toolchain. The internal linker must locate them at link time. Resolution
|
||
strategy:
|
||
|
||
1. Check `$BLAISE_CRT_DIR` environment variable.
|
||
2. Probe well-known paths:
|
||
- `/usr/lib/x86_64-linux-gnu/` (Debian/Ubuntu multiarch)
|
||
- `/usr/lib64/` (Fedora/RHEL)
|
||
- `/usr/lib/` (general)
|
||
3. For `crtbeginS.o` / `crtendS.o`, probe the GCC install directory:
|
||
- Run `cc -print-file-name=crtbeginS.o` at configure time, or
|
||
- Search `/usr/lib/gcc/x86_64-linux-gnu/*/` (Debian)
|
||
- Search `/usr/lib/gcc/x86_64-linux-gnu-*/` (Debian trixie)
|
||
|
||
If no CRT objects are found, fall back to `TExternalLinker` with a
|
||
diagnostic message. This avoids a hard failure on systems where the CRT
|
||
layout is non-standard.
|
||
|
||
**IMPORTANT:** CRT discovery succeeding does not guarantee correctness.
|
||
On musl-based systems (Alpine Linux), CRT objects exist under `/usr/lib/`
|
||
but require a different dynamic linker path (`/lib/ld-musl-x86_64.so.1`)
|
||
and libc name (`libc.musl-x86_64.so.1`). The linker must detect the
|
||
libc flavour — e.g. by checking for the existence of
|
||
`/lib/ld-musl-x86_64.so.1` — and parameterise accordingly, or fall back
|
||
to `TExternalLinker`. A future `--libc glibc|musl` flag can provide
|
||
explicit control.
|
||
|
||
=== Eliminating CRT Dependency (Future)
|
||
|
||
Long-term, the compiler can provide its own `_start` implementation in
|
||
assembly, eliminating the need for system CRT objects entirely. The minimal
|
||
`_start` for Linux x86-64 is:
|
||
|
||
[source,asm]
|
||
----
|
||
.text
|
||
.globl _start
|
||
_start:
|
||
xorl %ebp, %ebp ; mark deepest stack frame
|
||
movq %rsp, %rdi ; argc+argv on stack
|
||
andq $-16, %rsp ; align stack to 16 bytes
|
||
callq _blaise_entry ; Blaise program entry point
|
||
movl %eax, %edi ; exit code
|
||
movl $60, %eax ; SYS_exit
|
||
syscall
|
||
----
|
||
|
||
This removes the libc dependency for `__libc_start_main` but also loses
|
||
`atexit` handling, stdio buffer flushing, TLS setup, and
|
||
`.init`/`.fini`/`.ctors`/`.dtors` processing. Worth pursuing after the
|
||
runtime's libc dependency list is minimised (many RTL functions currently
|
||
call libc — `mmap`, `write`, `memcpy`, etc.).
|
||
|
||
== Platform Parameterisation
|
||
|
||
The linker's platform-dependent values are collected in a record, keyed by
|
||
the target triple:
|
||
|
||
[cols="2,2,2,2"]
|
||
|===
|
||
| Parameter | Linux x86-64 | FreeBSD x86-64 | Notes
|
||
|
||
| Dynamic linker
|
||
| `/lib64/ld-linux-x86-64.so.2`
|
||
| `/libexec/ld-elf.so.1`
|
||
| Written into `PT_INTERP`
|
||
|
||
| `EI_OSABI`
|
||
| `ELFOSABI_NONE` (0)
|
||
| `ELFOSABI_FREEBSD` (9)
|
||
| FreeBSD image activator checks this
|
||
|
||
| libc
|
||
| `libc.so.6`
|
||
| `libc.so.7`
|
||
| `DT_NEEDED` entry
|
||
|
||
| Extra `DT_NEEDED`
|
||
| `libpthread.so.0`, `libm.so.6` (glibc < 2.34 only)
|
||
| none (FreeBSD libc bundles these)
|
||
| See note below
|
||
|
||
| Page size
|
||
| 4096
|
||
| 4096
|
||
| Segment alignment
|
||
|
||
| CRT objects
|
||
| `Scrt1.o`, `crti.o`, `crtn.o`
|
||
| `Scrt1.o`, `crti.o`, `crtn.o`
|
||
| Same names, different paths
|
||
|
||
| Default base
|
||
| 0 (PIE)
|
||
| 0 (PIE)
|
||
| ET_DYN, loaded by kernel
|
||
|===
|
||
|
||
**glibc version note.** glibc 2.34+ merged libpthread and libm into
|
||
`libc.so.6`, making separate `DT_NEEDED` entries unnecessary. However,
|
||
older distributions (Debian 10 with glibc 2.28, Ubuntu 18.04 with
|
||
glibc 2.27) still ship separate `libpthread.so.0` and `libm.so.6`. The
|
||
internal linker should detect the glibc version at link time (check
|
||
whether `libpthread.so.0` exists in the library search path) and emit
|
||
conditional `DT_NEEDED` entries. Alternatively, always emit all three
|
||
`DT_NEEDED` entries — redundant on glibc 2.34+ but harmless (the loader
|
||
ignores `DT_NEEDED` for already-loaded libraries).
|
||
|
||
**musl / Alpine Linux.** On musl-based systems, the dynamic linker is
|
||
`/lib/ld-musl-x86_64.so.1` and libc is `libc.musl-x86_64.so.1`. CRT
|
||
objects exist under `/usr/lib/` but require the correct interp path.
|
||
The CRT discovery strategy must detect musl (e.g. by checking for
|
||
`/lib/ld-musl-x86_64.so.1`) and parameterise accordingly, or fall back
|
||
to `TExternalLinker`. A `--libc` flag (e.g. `--libc musl`) is a future
|
||
option for explicit selection.
|
||
|
||
== Implementation Phases
|
||
|
||
=== Phase A: ELF Reader + Archive Reader + Section Merge [IMPLEMENTED]
|
||
|
||
* Implement `blaise.elfreader.pas` — parse ELF headers, section headers,
|
||
symbol tables, relocation entries from `.o` files.
|
||
* Implement `.a` archive reader — parse the `!<arch>\n` header, GNU
|
||
long-name table (`//`), iterate members.
|
||
* Merge like-named sections with alignment padding.
|
||
* Test: read the internal assembler's `.o` output and verify section
|
||
contents match. Read `blaise_rtl.a` and verify all 11 members parse
|
||
correctly (validates the long-name table handling).
|
||
|
||
=== Phase B: Symbol Resolution + Static Relocations [IMPLEMENTED]
|
||
|
||
* Build global symbol table from merged sections.
|
||
* Define linker-synthesised symbols (`_GLOBAL_OFFSET_TABLE_`,
|
||
`__bss_start`, `_edata`, `_end`, `__TMC_END__`).
|
||
* Resolve weak undefined symbols to 0.
|
||
* Resolve `R_X86_64_PC32` and `R_X86_64_PLT32` relocations between
|
||
internally-defined symbols (intra-program calls/references).
|
||
* Produce a non-PIE `ET_EXEC` executable with a fixed base address and
|
||
no GOT/PLT — skipping dynamic linking entirely.
|
||
* Validate by structural comparison: link a hand-crafted `.o` fixture,
|
||
then compare section contents and relocated bytes against `ld -r`
|
||
output or the `cc`-linked binary. (Every Blaise program reaches libc
|
||
through the RTL, so a truly standalone test needs a hand-written
|
||
fixture that uses `SYS_exit` directly.)
|
||
|
||
Implemented in `TLinker` (`blaise.linker.elf.pas`). Layout assigns
|
||
virtual addresses at a fixed base (`0x400000`) in two permission runs
|
||
— an executable run (`.text` + `.rodata`, sharing the first page with
|
||
the ELF header and program headers) and a writable run (`.data` +
|
||
`.bss`) starting on a fresh page. `BuildSymbols` enters every
|
||
`STB_GLOBAL`/`STB_WEAK` definition (global-over-weak, two-globals =
|
||
duplicate error); `DefineSynthSymbols` adds the five linker symbols;
|
||
`ResolveSymbolAddr` handles local section-relative, absolute, resolved
|
||
global, weak-undefined→0, and strong-undefined→error. `ApplyRelocations`
|
||
patches `R_X86_64_PC32`/`PLT32` as `S + A − P` and rejects the
|
||
dynamic-only forms (`R_X86_64_64`, GOT/TLS) and absolute 32-bit symbol
|
||
references with a diagnostic, deferring them to Phase C. `EmitExecutable`
|
||
writes the `ET_EXEC` image (ELF header, two `PT_LOAD` program headers,
|
||
relocated section payloads, and a section-header table for tooling) and
|
||
marks the file executable.
|
||
|
||
Platform/architecture values that differ across the roadmap targets
|
||
(`Is64`, `OSABI`, `EMachine`, `BaseAddr`, `PageSize`) are collected in
|
||
`TLinkTarget` rather than hard-coded, so FreeBSD x86-64 (different
|
||
`EI_OSABI`) 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.
|
||
|
||
Tests: `TLinkerTests` (symbol resolution — global/weak/duplicate/strong-
|
||
undefined, synthesised symbols, `PC32` cross-object displacement, `R_X86_64_64`
|
||
rejection, ELF-header/entry-point structure) and `TLinkerE2ETests`
|
||
(`TestRun_SyscallHelloWorld` — a hand-written `write`+`exit` syscall
|
||
object linked internally to an `ET_EXEC`, executed, and asserted on
|
||
stdout and exit code). `readelf -a` reports the output as a clean
|
||
statically-linked x86-64 executable.
|
||
|
||
=== Phase C: Dynamic Linking + RTL + TLS [IMPLEMENTED]
|
||
|
||
This phase produces the first real runnable Blaise program. It must
|
||
include the RTL archive and TLS support because the RTL is required for
|
||
any program that calls `WriteLn` (or any runtime function), and the RTL
|
||
itself uses TLS (`blaise_mem.o` contains `.tdata` and `.tbss` with 26
|
||
`R_X86_64_TPOFF32` relocations).
|
||
|
||
* Read and link CRT startup objects (`Scrt1.o`, `crti.o`, `crtn.o`,
|
||
`crtbeginS.o`, `crtendS.o`).
|
||
* Link the RTL archive (`blaise_rtl.a` — all members).
|
||
* Classify unresolved symbols as external (libc) or weak-undefined.
|
||
* Generate GOT entries with eager binding, PLT stubs, `.rela.plt`.
|
||
* Generate `.rela.dyn` with `R_X86_64_RELATIVE` entries for all
|
||
`R_X86_64_64` relocations in writable sections.
|
||
* Generate `.dynamic`, `.dynsym`, `.dynstr`, `.hash` sections (start
|
||
with SysV `.hash`; `.gnu.hash` is polish for Phase E).
|
||
* Handle TLS: merge `.tdata` and `.tbss`, compute TLS block size with
|
||
variant-II alignment, emit `PT_TLS`, resolve `R_X86_64_TPOFF32`.
|
||
* Merge `.init_array`/`.fini_array` from CRT objects; emit
|
||
`DT_INIT_ARRAY` / `DT_FINI_ARRAY` tags.
|
||
* Merge `.init`/`.fini` sections from CRT; emit `DT_INIT`/`DT_FINI`.
|
||
* Add `PT_INTERP`, `PT_DYNAMIC`, `PT_GNU_RELRO`, `PT_GNU_STACK`
|
||
program headers.
|
||
* Switch from `ET_EXEC` to `ET_DYN` (PIE).
|
||
* Test: link a "Hello, world" program that calls `WriteLn`. Link a
|
||
program using classes (exercises `.rela.dyn` for vtable pointers).
|
||
Link a program using threadvars (exercises TLS).
|
||
|
||
**Implementation notes:**
|
||
|
||
* `SHF_TLS` was initially defined as `$200` (incorrect); fixed to the
|
||
correct ELF spec value `$400` in both `blaise.elfreader.pas` and
|
||
`blaise.elfwriter.pas`. The wrong value caused `.tbss` sections to be
|
||
placed twice in the layout (once in the normal NOBITS loop that did not
|
||
recognise them as TLS, once in the explicit TLS placement), producing
|
||
doubled negative TPOFF32 offsets and segfaults at runtime.
|
||
* `FTlsAddr` must be read from the post-alignment address
|
||
(`MergedAddr(M)` after `PlaceSection`), not from the pre-alignment
|
||
cursor, to avoid off-by-padding TPOFF values.
|
||
* Duplicate global symbols use first-wins semantics (matching ld
|
||
behaviour for archive members), since the main program object and RTL
|
||
archive members may both define system runtime functions.
|
||
* CRT object discovery (`FindCrtObjects` in `uToolchain.pas`) probes
|
||
Debian and RedHat layout paths with GCC versions 11–14.
|
||
* `PhdrCount` is 11 in both `LayoutDynamic` and `EmitDynExecutable`:
|
||
PT_PHDR, PT_INTERP, PT_LOAD x4, PT_DYNAMIC, PT_TLS, PT_GNU_STACK,
|
||
PT_GNU_RELRO. PT_TLS is always emitted (zero-sized when no TLS
|
||
sections) to keep header count stable.
|
||
|
||
=== Phase D: Full Integration [IMPLEMENTED]
|
||
|
||
* Wire `TLinker` into the compiler driver behind `--linker
|
||
internal` (`TNativeBackendDriver.LinkViaInternalLinker`).
|
||
* `--linker internal` works with both `--assembler internal` and
|
||
`--assembler external`.
|
||
* CRT startup objects are discovered automatically via
|
||
`FindCrtObjects`.
|
||
* OPDF debug sections (`.opdf.*`) pass through from input objects into
|
||
the final executable.
|
||
* Duplicate global symbols handled with first-wins semantics (matching
|
||
ld).
|
||
* Full test suite (3536 tests) passes with `--linker internal`.
|
||
* Self-compilation: the compiler can compile itself and produce a
|
||
working binary using `--backend native --assembler internal --linker
|
||
internal` — no external tools in the pipeline.
|
||
* E2E tests in `TInternalLinkerE2ETests`: HelloWorld, StringOps,
|
||
ExceptionHandling, ClassAndVirtual.
|
||
|
||
**Remaining Phase D items (not yet implemented):**
|
||
|
||
* Selective archive member inclusion (pull only members that define
|
||
symbols in the unresolved set, iterate until stable).
|
||
* Fixpoint verification with `--linker internal` (the binary is
|
||
structurally correct but the fixpoint scripts do not yet exercise
|
||
this path).
|
||
* Differential validation: compile the existing e2e test corpus with
|
||
both `--linker external` and `--linker internal`, compare stdout and
|
||
exit codes.
|
||
|
||
=== Phase E: Default and Polish
|
||
|
||
* Switch default from `external` to `internal`.
|
||
* Add FreeBSD x86-64 support (different `PT_INTERP`, `EI_OSABI`,
|
||
CRT locations, `libc.so.7`).
|
||
* Detect musl-based systems and parameterise interp/libc accordingly
|
||
(or fall back to `TExternalLinker`).
|
||
* Replace SysV `.hash` with `.gnu.hash` (bloom filter, sorted
|
||
`.dynsym`).
|
||
* Emit conditional `DT_NEEDED` for `libpthread.so.0` / `libm.so.6`
|
||
on glibc < 2.34 systems.
|
||
* Add `--linker external` escape hatch documentation.
|
||
|
||
== Testing Strategy
|
||
|
||
[cols="1,2,3"]
|
||
|===
|
||
| Layer | Tests | What It Catches
|
||
|
||
| Unit tests
|
||
| `TElfReaderTests` — parse known `.o` files, verify headers, symbols,
|
||
relocations. Parse `blaise_rtl.a` archive members.
|
||
| ELF/archive parsing bugs
|
||
|
||
| Unit tests
|
||
| `TLinkerTests` — merge sections, resolve symbols, apply relocations on
|
||
synthetic inputs. Must cover every relocation type with
|
||
manually-verified expected bytes. Include relocation serialisation
|
||
(`.rela.*` `r_info` packing, symbol remapping after local/global sort).
|
||
| Relocation arithmetic, symbol resolution logic
|
||
|
||
| E2E tests
|
||
| `TInternalLinkerE2ETests` — compile small programs with `--linker
|
||
internal`, run, assert stdout. Must include: class with virtual
|
||
method, `Double` arithmetic, threadvar, global nil-assignment,
|
||
exception handling, dynamic array, string operations.
|
||
| Full pipeline integration
|
||
|
||
| Differential
|
||
| Compile the existing e2e test corpus with both `--linker external` and
|
||
`--linker internal`; assert identical stdout and exit codes. Structural
|
||
comparison of `readelf -l` / `readelf -d` output between the two
|
||
binaries.
|
||
| Subtle divergences in section layout, relocation, or symbol resolution
|
||
|
||
| Negative tests
|
||
| Undefined reference (strong), duplicate symbol (two globals), wrong-class
|
||
object file (32-bit ELF, non-x86_64)
|
||
| Error reporting quality and robustness
|
||
|
||
| Native TestRunner
|
||
| Run the full native TestRunner under `--linker internal`
|
||
| Catches issues the fixpoint misses (documented in project memory)
|
||
|
||
| Fixpoint
|
||
| Run `fixpoint.sh` and `fixpoint-native.sh` with `--linker internal`
|
||
| Self-hosting correctness
|
||
|
||
| Container matrix
|
||
| Run the e2e suite on Alpine (musl), Debian oldstable (glibc 2.31),
|
||
Ubuntu 24.04 (glibc 2.39) — at minimum as a manual validation step
|
||
| CRT discovery and glibc version edge cases
|
||
|===
|
||
|
||
== Risk Assessment
|
||
|
||
**Highest risk:** Dynamic relocations for PIE. The `.rela.dyn` /
|
||
`R_X86_64_RELATIVE` mechanism is conceptually simple but touches every
|
||
vtable and typeinfo pointer in `.data`. A single missing entry produces
|
||
a binary that loads but segfaults on the first virtual method call —
|
||
the symptom is indistinguishable from a codegen bug, making diagnosis
|
||
difficult. The differential test harness (compare against `cc`-linked
|
||
binary) is the primary mitigation.
|
||
|
||
**High risk:** TLS layout. The x86-64 variant-II TLS offset formula
|
||
requires `align_up(tls_size, tls_align)` — a classic off-by-alignment
|
||
bug. The RTL's `blaise_mem.o` contains both `.tdata` (initialised) and
|
||
`.tbss` (zero-initialised) TLS sections; a wrong TLS offset corrupts
|
||
the memory allocator's thread-local state, causing non-deterministic
|
||
crashes anywhere in the program.
|
||
|
||
**High risk:** Relocation arithmetic. A single off-by-one in
|
||
`S + A - P` produces a binary that crashes at runtime with no obvious
|
||
diagnostic. The unit tests for Phase B must cover every relocation type
|
||
with manually-verified expected values.
|
||
|
||
**Medium risk:** CRT object location. Systems have varying layouts
|
||
(multiarch, cross-compilation sysroots, musl). The fallback to
|
||
`TExternalLinker` mitigates this — if CRT objects cannot be found, the
|
||
compiler degrades gracefully. The musl case is particularly tricky:
|
||
CRT objects exist but require a different interp path.
|
||
|
||
**Medium risk:** `.init_array` / `.fini_array` ordering. CRT objects
|
||
bracket these sections (`crtbeginS.o` before, `crtendS.o` after).
|
||
If the linker merges them in the wrong order, constructors run in the
|
||
wrong sequence or not at all — a load-time crash with no useful
|
||
backtrace.
|
||
|
||
**Low risk:** GOT/PLT generation. With eager binding, PLT stubs are
|
||
trivial (one indirect jump each). No lazy resolver protocol to
|
||
implement.
|
||
|
||
== Current Linker Dependency Analysis
|
||
|
||
For reference, the current `cc` link line for a simple Blaise program
|
||
pulls in:
|
||
|
||
* **CRT objects:** `Scrt1.o`, `crti.o`, `crtbeginS.o`, `crtendS.o`,
|
||
`crtn.o`
|
||
* **Dynamic libraries:** `libc.so.6` (provides all ~47 undefined symbols
|
||
in a typical program: `write`, `mmap`, `memcpy`, `exit`, `fork`,
|
||
`pthread_*`, `clock_gettime`, etc.)
|
||
* **Implicit flags:** `-pie`, `-z now`, `-z relro`, `--build-id`,
|
||
`--eh-frame-hdr`, `--hash-style=gnu`
|
||
|
||
The RTL archive (`blaise_rtl.a`) contains 11 object files and ~131
|
||
`R_X86_64_64` relocations (vtable/typeinfo pointers). All undefined
|
||
symbols in the RTL are libc functions — there are no dependencies on
|
||
libgcc or libstdc++. The `-lm` and `-lpthread` flags on the current
|
||
link line are unnecessary on glibc 2.34+ (which merged libm and
|
||
libpthread into `libc.so.6`), but are required on older glibc versions
|
||
still in use on Debian 10, Ubuntu 18.04, etc.
|