6. ELD (debugging guide)
This document describes the high-level flow of how ELD executes a link, with call-site pointers and practical tips for debugging failures.
Relocations: read -> scan -> apply -> (optional) emit reloc sections
Dynamic relocations (what creates
.rel[a].dyn/.rel[a].plt)Where failures typically come from (symptoms -> pipeline stage)
6.1. Big picture
At a high level, a link invocation looks like this:
eld main expands response files and selects a driver flavor/target.
Driver parses options and builds an ordered list of input actions.
Linker prepare initializes target/emulation, inputs, and plugins, then reads and normalizes input files (and may run LTO).
Linker link resolves symbols/relocations, lays out output sections, then emits the final ELF and optional map files.
Optional diagnostics: reproduce tarball/mapping file, plugin activity log, timing stats, summary, etc.
6.2. Entry point and driver selection
The executable entry point is tools/eld/eld.cpp:
Expands
@responsefiles viallvm::cl::ExpandResponseFiles(...).Creates a
Driverand callsDriver::setDriverFlavorAndInferredArchFromLinkCommand(...).Creates a GNU-ld-compatible driver (
GnuLdDriver) and callsGnuLdDriver::link(...).
Driver flavor selection is implemented in lib/LinkerWrapper/Driver.cpp:
First tries to infer a target from the program name (e.g.
arm-link,aarch64-link,hexagon-link).Otherwise inspects early arguments like
-m <emulation>or-marchto select a target-specific driver.
Environment hooks that affect arguments:
ELDFLAGS: appended to the link command by the driver (useful for always-on debug flags).
6.3. Argument parsing and preprocessing
The top-level flow of option parsing and dispatch is in
lib/LinkerWrapper/GnuLdDriver.cpp (GnuLdDriver::link(...)):
parseOptions(...)processLLVMOptions(...)(parses-mllvm ...arguments)processTargetOptions(...)(handles-mtriple,-march,-mabi,-m <emulation>, etc.)processOptions(...)(general linker options)checkOptions(...)andoverrideOptions(...)createInputActions(...)to build the ordered action listdoLink(...)to run the actual link pipeline
If you suspect argument/option issues, start with:
--verbose(or--verbose=<level>)--trace=command-line--trace=filesor-t(prints processed files)--error-style=GNUor--error-style=LLVM(if output formatting matters)
6.4. Input actions (what gets fed to the linker)
After parsing options, the driver builds a sequence of actions that are later “activated” to create inputs:
-T <script>/--default-script <script>->ScriptAction-R <file>->JustSymbolsAction--defsym <sym>=<expr>->DefSymAction-l <namespec>->NamespecAction(library search)Plain inputs ->
InputFileActionState toggles like
--whole-archive,--as-needed,--start-group/--end-group,--start-lib/--end-lib-> corresponding actions that affect how subsequent inputs are treated
This happens in GnuLdDriver::createInputActions(...) in
lib/LinkerWrapper/GnuLdDriver.cpp.
Debug tip: if you see failures about mismatched groups/libs, the error is detected here (before any ELF parsing starts).
6.5. Link pipeline overview (doLink -> Linker)
Once actions are created, GnuLdDriver::doLink(...) does the setup:
Looks up the LLVM target and the ELD target based on the chosen triple.
Creates a
Module(lib/Core/Module.cpp) and optionalLayoutInfofor map printing.Selects map printers based on
--MapStyle=...(or defaults) and prepares layout printers.Constructs an
eld::Linkerand runs: *linker.prepare(actions, target)*linker.link()(unless running “LTO-only” modes) *linker.printLayout()(map file emission)Runs plugin teardown hooks, unloads plugins, emits stats, and finalizes the diagnostic engine summary.
6.6. Prepare phase (Linker::prepare)
Linker::prepare(...) (lib/Core/Linker.cpp) is responsible for:
Target/emulation + backend
Initializes emulator and backend for the selected target.
Initialize inputs
Builds the input tree, creates internal linker-generated inputs, activates the action list, and reads linker scripts.
Universal plugins
Reads plugin configuration, loads universal plugins from the script, stores them, and runs plugin init hooks.
Read/normalize inputs
Reads all input files, sections, symbols, and (optionally) runs LTO-related preprocessing.
Common debug levers for this phase:
--trace=linker-scriptor--trace-linker-script(script parsing)--trace=threads(parallel input/section reading behavior)--trace=LTOor--trace-lto(LTO stage boundaries)--plugin-config=<config-file>and--no-default-plugins(plugin triage)
6.7. Normalize phase (Linker::normalize)
Linker::normalize() (lib/Core/Linker.cpp) performs:
Optional command-line header/summary printing when
--trace=command-lineis enabled (viaLinkerConfig::printOptions(...)inlib/Config/LinkerConfig.cpp).Reading all input files via
ObjLinker->normalize(): * Parses ELF objects/archives/shared libraries/bitcode inputs. * Populates symbol tables and initial symbol resolution.Loads non-universal plugins.
Computes code position (static/dynamic/PIE) and validates incompatible options (e.g. patch options with non-static output).
Parses external scripts:
Version scripts
Dynamic list (when building dynamic artifacts)
Adds linker-script-defined symbols.
LTO steps (when needed):
Creates an LTO object from bitcode inputs.
Re-runs normalization post-LTO after replacing bitcode with generated objects.
Debug tip: LTO-related failures often reproduce reliably with --reproduce
because the tarball will include bitcode inputs and any generated LTO objects
recorded by the linker.
6.8. Resolve + layout + emit (Linker::link)
Linker::link() in lib/Core/Linker.cpp is the main “work” phase:
Standard sections
Initializes default sections (and per-file synthetic dynamic sections when producing dynamic outputs).
Resolve (symbol/reloc processing)
Reads relocations.
Allocates common symbols.
Assigns output sections using default + linker script rules.
Runs plugin hooks around rule matching/layout.
Processes target-specific input handling.
Optionally performs garbage collection / stripping.
Scans relocations, finalizes scan results, and builds output/dynamic symbol tables.
Merges sections and creates section symbols.
Layout
Initializes stubs/trampolines, prelayout, merge-strings optimization.
Establishes final layout and postlayout output section table.
Finalizes symbol values, runs output-section iterator plugins, applies relocations, and finalizes output state.
Emit
Computes output file size and creates an
llvm::FileOutputBuffer.Writes section contents, performs post-processing, emits Build ID, commits the output, and optionally verifies the output size on disk.
If a failure happens late (layout/emit), map files are usually the fastest way to pinpoint the problematic section/segment/relocation.
6.9. Internal inputs and “internal sections”
ELD creates a number of linker-generated inputs up front so later stages can treat them uniformly as normal inputs/sections/symbols.
Creation happens in Module::createInternalInputs() (lib/Core/Module.cpp).
Each internal input corresponds to a named Input/InputFile (for example
Attributes, CommonSymbols, DynamicSections, Trampoline, and
others) and is used to host sections/fragments that are not sourced from a user
object file.
Two other “internal” concepts are easy to confuse:
Linker-internal input sections: sections owned by an internal input file (
Input::Internal). These typically haveLDFileFormat::Internalkind and may carry relocations to be applied later.Output-format sections: sections that come from the backend/output format (not from a user input file), for example dynamic tables/headers. These are treated as output sections and can be matched/discarded via linker-script rules; see
ObjectLinker::markDiscardFileFormatSections()inlib/Object/ObjectLinker.cpp.
Debug tips:
If you suspect an unexpected section exists (or is missing), prefer a text map:
-M --MapStyle=Text --Map=<file>.If a section is unexpectedly discarded, use
--trace-section <name>and check whether it matched a/DISCARD/rule.
6.10. Section merging (input sections -> output sections)
The “merge sections” name in ELD means: take the input section graph (from object files + internal inputs), and populate the output section layout according to default rules + linker-script rules + plugins.
There are three distinct sub-steps to keep straight:
Rule matching / output section assignment
ObjectLinker::assignOutputSections(...)(lib/Object/ObjectLinker.cpp) uses anObjectBuilderto match input sections against linker-script rules (including wildcards, sorting policies,EXCLUDE_FILE, etc).
Input section merging
ObjectLinker::mergeSections()callsmergeInputSections(...)which iterates all input sections and merges them into output sections, with special handling for some section kinds (.eh_frame,.sframe, target overrides, linkonce/reloc sections, etc).
Finalize output sections
createOutputSection(...)builds each output section’s fragment list, computes flags/alignment, assigns fragment offsets, and inserts the output sections into the module’s output section table.
6.10.1. Special-case section handling during merging
ObjectLinker::mergeInputSections(...) (lib/Object/ObjectLinker.cpp)
handles some input section kinds specially:
LDFileFormat::RelocationandLDFileFormat::LinkOnce: if the “link” section is discarded/ignored, the relocation/linkonce section is ignored too.LDFileFormat::Target: backends may override merging viaGNULDBackend::DoesOverrideMerge(...)andGNULDBackend::mergeSection(...).LDFileFormat::EhFrame:Splits and re-chunks
.eh_frameinto CIE/FDE fragments.If enabled, registers content for
.eh_frame_hdrand creates filler/hdr fragments in the backend.
LDFileFormat::SFrame:Parses the section and may create an
SFrameheader fragment when configured.
Everything else typically flows through ObjectBuilder::mergeSection(...) and
ends up contributing fragments to an output section.
6.10.2. Output section construction and offsets
Once merging decides which fragments belong to a particular output section,
ObjectLinker::createOutputSection(...) and ObjectLinker::assignOffset(...)
lay them out:
Output section
ALIGN/ input sectionSUBALIGN(from linker script) is enforced when present.“Dirty” rules (modified by plugins) trigger a recomputation of input section flags/type/align based on the fragments that ended up in the rule.
Fragment offsets are assigned linearly; per-fragment padding/alignment is applied by
Fragment::paddingSize()(lib/Fragment/Fragment.cpp).
Debug tips:
If you see “offset not assigned” diagnostics, the fragment/section likely never got placed into an output section (or got discarded). The diagnostic plumbing is in
Fragment::getOffset(...)(lib/Fragment/Fragment.cpp).If rule sorting changes layout unexpectedly, check whether the linker script wildcard includes a sort policy, or whether
--sort-section=...is enabled.
6.11. String merging (MergeString fragments)
String merging is a dedicated optimization pass that runs during layout, before final output layout is established:
ObjectLinker::doMergeStrings()calls:mergeIdenticalStrings(): mergesMergeStringFragmentcontent (can be threaded across output sections; global non-alloc merge is done single-threaded).fixMergeStringRelocations(): updates relocations that refer into merged strings viaRelocator::doMergeStrings(...).
The output offset calculation for merged strings is special-cased in
FragmentRef::getOutputOffset(...) (lib/Fragment/FragmentRef.cpp), because
multiple input strings may map to a shared output string (including suffix
merging).
Debug tips:
Use
--trace=merge-strings/--trace-merge-strings=<option>to see why strings were merged and how offsets were computed.
6.12. Relocations: read -> scan -> apply -> (optional) emit reloc sections
There are multiple relocation passes, and confusing them is a common source of “where did this relocation come from?” debugging pain.
6.12.1. Read relocations
ObjectLinker::readRelocations() reads relocations from input objects
(lib/Object/ObjectLinker.cpp):
Skips non-object inputs, and skips inputs marked “just symbols”.
For patch-base inputs, runs patch-base parsing via the executable-object parser.
6.12.2. Scan relocations (reservation / planning)
ObjectLinker::scanRelocations(...) (lib/Object/ObjectLinker.cpp) is the
“planning” pass. It typically:
Invokes
Relocator::scanRelocation(...)per relocation, which is where the backend decides whether it needs to reserve GOT/PLT entries, create or reserve dynamic relocations, create stubs/trampolines, etc. (target-specific logic is inlib/Target/*/*Relocator.cpp).Collects copy-relocation candidates per input, then creates copy relocations once per symbol (see
createCopyRelocation(...)/addCopyReloc(...)inlib/Object/ObjectLinker.cpp).Merges per-file dynamic relocation vectors into a single “reloc input” (
getDynamicSectionHeadersInputFile()) so later code can treat them consistently.Runs
ObjectLinker::finalizeScanRelocations()which callsGNULDBackend::finalizeScanRelocations()for backend-specific finalization.
In relocatable/partial links, ELD uses partialScanRelocation(...) instead.
6.12.3. Create output relocation sections (--emit-relocs)
If --emit-relocs is enabled, ELD creates output relocation sections during
prelayout:
ObjectLinker::prelayout()callscreateRelocationSections().createRelocationSections()counts relocations per output section and creates the corresponding output relocation sections (.rel.<sec>/.rela.<sec>style, based on target) sized to hold all entries.
6.12.4. Apply relocations (writes relocation results)
Relocation application happens in ObjectLinker::relocation(...):
Applies internal/linker-created relocations.
Applies input relocations, skipping relocations that are known to be relaxed or that target discarded/ignored sections/symbols.
Applies branch-island (relaxation) relocations after input relocations are applied.
If
--emit-relocsis enabled, emits external-form relocation records into the output relocation sections (viaEmitOneReloc).
Finally, syncRelocations(...) writes relocation results into the output
buffer, including extra ordering/barriers to avoid races when multi-threaded.
Debug tips:
--trace=reloc=<pattern>pinpoints a single relocation kind.--trace=symbol=<name>helps tie relocations back to symbol resolution.If you see overflows/unencodable immediates, diagnostics originate from
Relocation::issueSignedOverflow(...)/issueUnencodableImmediate(...)inlib/Readers/Relocation.cpp.
6.13. Dynamic relocations (what creates .rel[a].dyn / .rel[a].plt)
Dynamic relocation entries are typically created/reserved during the relocation scan phase inside the target relocator and backend:
Target relocators decide whether a given relocation needs: * a static relocation only, * a dynamic relocation (REL/RELA), * a PLT/GOT entry (and an associated relocation), * a copy relocation (for executable data symbol imports).
Backends provide the actual sections for dynamic relocations (for example
.rela.dyn/.rela.plt) and may sort/finalize them. A common set of helper logic lives inlib/Target/GNULDBackend.cpp.
You can generally think of the relocation scan as “reserving and populating dynamic relocation vectors”, and layout/emission as “placing and writing those sections”.
6.14. Garbage collection (--gc-sections)
Garbage collection in ELD is graph reachability over sections, built from relocations and a chosen root set (“entry sections”).
6.14.1. Where it runs
The default GC pass is triggered during the resolve phase:
ObjectLinker::dataStrippingOpt()checksIRBuilder::shouldRunGarbageCollection()and callsObjectLinker::runGarbageCollection(\"GC\").The implementation is
GarbageCollectioninlib/GarbageCollection/GarbageCollection.cpp.
6.14.2. How the graph is built
GarbageCollection::setUpReachedSectionsAndSymbols():
Traverses input relocations and records, per “apply section”, the set of reachable target sections and reachable symbols.
Handles special cases:
Script-defined symbols: walks the assignment expression’s symbol references.
Magic
__start_*/__stop_*symbols: forces sections with matching names into the reachable set.Bitcode: defers some reachability until it can map referenced symbols back to bitcode “input sections”.
Allows the backend to add extra reachability via
GNULDBackend::setUpReachedSectionsForGC(...).
6.14.3. How entry sections are chosen
GarbageCollection::getEntrySections() considers multiple root sources:
The configured entry symbol (if it resolves to a fragment).
Sections matched by
KEEP(...)in the linker script.When producing dynamic outputs, exported/visible symbols (subject to version script scoping) contribute entry sections.
Sections marked with
SHF_GNU_RETAINare treated as entry-like.
6.14.4. Mark-and-sweep
GarbageCollection::findReferencedSectionsAndSymbols(...) performs a BFS from
entry sections, following the reachability map built earlier, producing a live
set. stripSections(...) then marks sections not in the live set as ignored
(and can optionally print what got collected).
Debug tips:
--print-gc-sectionsshows what got collected.--trace=garbage-collectionand--trace=live-edgesare useful when a section is unexpectedly dead/alive.If GC keeps/drops a zero-sized section unexpectedly, check whether it is the target of a relocation or contains a symbol (see the FAQ discussion of zero-sized sections).
6.15. Fragment model (Fragment / FragmentRef)
ELD uses a fragment model internally where fragments are the minimum linking unit, not sections.
6.15.1. Fragments
Fragment (include/eld/Fragment/Fragment.h) represents a typed chunk of
content that will appear in the output. Examples include:
raw data regions (region fragments),
stubs / trampolines / branch island content,
GOT / PLT entries,
mergeable strings,
.eh_frame-related pieces (CIE/FDE fragments),build-id fragments, timing fragments, and others.
Each fragment belongs to an owning (input) section and has:
an alignment requirement,
an assigned (unaligned) offset, and derived padding size,
an
emit(...)implementation that writes bytes during output generation.
6.15.2. Offsets, padding, and “why is this input marked used?”
During output section construction, ELD assigns fragment offsets linearly. The
final effective offset includes per-fragment padding computed by
Fragment::paddingSize() (lib/Fragment/Fragment.cpp). When a fragment
offset is assigned, Fragment::setOffset(...) also marks the owning input as
“used” when it contributes allocatable content (this feeds into GC and
diagnostics).
6.15.3. FragmentRef (symbol/relocation addressing)
FragmentRef (include/eld/Fragment/FragmentRef.h) is a pointer to:
a fragment, plus
an additional byte offset within that fragment.
This is the core indirection used by:
output symbols (symbols carry a
FragmentRefto their definition),relocations (relocation “place” and/or “target” is a
FragmentRef).
Output offset computation is not always “fragment offset + ref offset”:
FragmentRef::getOutputOffset(...)special-cases.eh_frameto map offsets through the split/piece layout.It also special-cases merged strings so references land on the deduplicated output string (including suffix merging).
Debug tips:
If a relocation points somewhere surprising, inspect: * the relocation’s
targetRef(place), and * the symbol’sfragRef(definition), and remember both can have special-cased output offset behavior.
6.16. Map files (layout printers)
Map emission is handled after the link attempt in Linker::printLayout() and
also from the crash signal handler.
Key options:
-M/--print-map: enable map generation--Map=<filename>: choose map output file--MapStyle=<YAML|Text|Binary>: choose format(s)--MapDetail=<option>: more detail in maps--color-map: colorize map output--trampoline-map <filename>: trampoline information (YAML)
6.17. Reproducing failures (tarball + mapping file)
ELD can capture a self-contained reproducer for link issues:
--reproduce <tarfilename>: always produce a tarball--reproduce-compressed <tarfilename>: compressed tarball--reproduce-on-fail <tarfilename>: only on failureELD_REPRODUCE_CREATE_TAR: environment variable that forces reproducer creation (uses a temporary tar file if no filename is provided)
Additional reproduce helpers:
--mapping-file <INI-file>: reproduce link using a mapping file--dump-mapping-file <outputfilename>: dump mapping info--dump-response-file <outputfilename>: dump rewritten response file
The reproduce tarball logic is wired through:
GnuLdDriver::handleReproduce(...)andwriteReproduceTar(...)inlib/LinkerWrapper/GnuLdDriver.cppModule::createOutputTarWriter()creation decision viaLinkerConfig::shouldCreateReproduceTar()(lib/Config/LinkerConfig.cpp)
6.18. Crash/signal behavior (what gets written on a crash)
ELD installs a default signal handler in GnuLdDriver::doLink(...):
Flushes a text map file (if configured).
Detects likely plugin crashes and reports them.
Writes a temporary
.shscript that appends--reproduce build.tarto the command line and instructs the user to rerun.
This is implemented in GnuLdDriver::defaultSignalHandler(...) in
lib/LinkerWrapper/GnuLdDriver.cpp.
6.19. Where failures typically come from (symptoms -> pipeline stage)
This section is meant as a quick index: if you see a symptom, these are the
stages/files to inspect first. Many of these topics are also discussed in more
detail in docs/userguide/documentation/linker_faq.rst.
6.19.1. Driver/target selection failures
Symptoms:
“unsupported emulation” / “cannot find target”
wrong backend chosen when using
-m/-march
Start here:
Driver::getDriverFlavorFromLinkCommand(...)inlib/LinkerWrapper/Driver.cppGnuLdDriver::processTargetOptions(...)and target lookups inGnuLdDriver::doLink(...)(lib/LinkerWrapper/GnuLdDriver.cpp)
6.19.2. Input specification / archive/group issues
Symptoms:
“mismatched group” / “mismatched lib”
unexpected missing objects from an archive
Start here:
GnuLdDriver::createInputActions(...)for--start-group/--end-groupand--start-lib/--end-libbalancing and orderingUse
-t/--trace=filesto confirm what ELD actually processed
6.19.3. Linker script rule-matching errors
Symptoms:
“no linker script rule for “.bss”” / “.data.bar” style errors
sections landing in unexpected output sections
Start here:
ObjectLinker::assignOutputSections(...)and friends inlib/Object/ObjectLinker.cppEmit a map file and confirm whether the input section matched any rule; the FAQ has a guide for diagnosing these errors and for finding used/unused rules.
If this is a script parsing/syntax problem (rather than rule matching), enable
--trace=linker-script/--trace-linker-scriptand use--reproduce[-on-fail]to capture the exact script(s) and rewritten command line that ELD used.
Practical tips:
Reduce the script: comment out most rules and add back until the behavior flips. For rule-matching bugs, keep only the relevant
SECTIONSrules and a minimalMEMORYmap.Prefer map files while iterating: they show which input section went to which output section and why that changed across experiments.
6.19.4. Linker script parsing and evaluation errors
Symptoms:
parse errors (unexpected token, unexpected
), etc)script expression issues (undefined symbol in an expression, unexpected value)
placement issues that are script-driven (for example: region overflow, PHDR mismatch, or a section being forced into an incompatible segment)
Start here:
--trace=linker-script/--trace-linker-scriptto see script parsing, includes, and key evaluation decisions.A text map file to confirm what the script actually did: memory regions, output section addresses, and segment layout are usually visible there.
--reproduce[-on-fail]so the exact script(s) used by the link are captured alongside the rewritten response file (this is critical when scripts are generated by the build system).
6.19.5. Undefined references and symbol resolution surprises
Symptoms:
“undefined reference” failures
symbol unexpectedly resolved from a different archive/object
Start here:
Resolve phase in
Linker::resolve()(lib/Core/Linker.cpp) and the diagnostic engine outputUse
--trace=symbol=<name>(or--trace=all-symbols) to see the resolution path
When the failure is a runtime crash (not a link error), symbol resolution can still be the root cause:
Wrong interposition/visibility (a symbol resolves but to an unexpected definition at runtime).
Lazy binding via PLT (a crash happens on the first call into a function that is resolved late by the dynamic loader).
Quick inspections:
# Symbol tables (static and dynamic), with type/binding/visibility:
llvm-readelf -s --demangle --extra-sym-info ./app | less
llvm-readelf -s --demangle --extra-sym-info ./libfoo.so | less
# Dynamic symbols only (what the runtime loader sees):
llvm-readelf --dyn-syms --demangle --extra-sym-info ./libfoo.so | less
# Undefined dynamic symbols (what must be provided by dependencies):
llvm-readelf --dyn-syms --demangle --extra-sym-info ./libfoo.so | awk '$7==\"UND\" {print}'
# Imported/Exported symbols (alternate views):
nm -D --defined-only ./libfoo.so | less
nm -D --undefined-only ./libfoo.so | less
6.19.6. Garbage collection removed something needed
Symptoms:
function/data present in inputs but missing from output
a section disappears only with
--gc-sections
Start here:
GarbageCollectionimplementation inlib/GarbageCollection/GarbageCollection.cpp--print-gc-sectionsplus--trace=garbage-collection/--trace=live-edgesEnsure linker-script
KEEP(...)is used for sections that must never be GC’d
6.19.7. Relocation overflows / unencodable immediates / target-specific relocation bugs
Symptoms:
overflow/unencodable relocation diagnostics
crashes during relocation scan/apply
output runs but has wrong addresses at runtime
Start here:
Scan phase:
ObjectLinker::scanRelocations(...)and target relocators (lib/Target/*/*Relocator.cpp)Apply phase:
ObjectLinker::relocation(...)and sync/writeback (lib/Object/ObjectLinker.cpp)Diagnostics:
lib/Readers/Relocation.cpp(location printing, overflow, etc)
6.19.8. Trampolines / stubs / relaxation issues
Symptoms:
failures mentioning trampolines, far calls, or branch islands
layout changes causing new trampolines or changing trampoline reuse
Start here:
ObjectLinker::initStubs()and target stub factories/backendsMap/trampoline map options (
--trampoline-map) plus FAQ sections on trampoline naming and reuse controls
6.19.9. LTO failures
Symptoms:
failures only with
-flto/ ThinLTO / Full LTO“LTO merge error” / codegen diagnostics
Start here:
ObjectLinker::createLTOObject()and LTO diagnostics handler inlib/Object/ObjectLinker.cpp--trace=LTO/--trace-ltoand--reproduce[-on-fail]to capture inputs and generated objects--save-temps(or--save-temps=<dir>) to preserve intermediate LTO artifacts for inspection (files use the prefix<output>.llvm-lto.*)If you need stable, non-temporary LTO-generated objects:
--lto-obj-path=<prefix>(also keeps the objects from being deleted after LTO)
6.19.10. Plugin-caused failures
Symptoms:
crashes only when a plugin is configured
non-deterministic behavior across runs with the same inputs
Start here:
--plugin-activity-file=<file>to capture plugin activity--no-default-pluginsto isolateCrash handler output from
GnuLdDriver::defaultSignalHandler(...)can explicitly call out a plugin as the likely crash source
6.19.11. Output emission failures
Symptoms:
“unwritable output” / commit errors
output size verification failures
Start here:
Linker::emit()inlib/Core/Linker.cpp(llvm::FileOutputBuffercreation/commit)
6.20. Practical debugging checklist
When a link fails and you need actionable data quickly, try (in order):
Add
--reproduce-on-fail repro.tar(or--reproduce repro.tar).Add
--verbose --trace=command-line --trace=files.Enable map output:
-M --Map=layout.map --MapStyle=Text(or YAML).If plugins are involved:
--plugin-activity-file=plugins.jsonand try--no-default-pluginsto isolate.If time-sensitive or flaky:
--print-timing-statsand consider--emit-timing-stats=<file>to capture timing consistently.
6.21. Debugging runtime crashes in ELD-built images
This section is for cases where the link succeeds but the produced ELF image (executable / shared library / firmware image) fails at runtime (crash, abort, unexpected exception, bad unwind/backtrace, etc.).
6.21.1. Preserve the right artifacts
For runtime debugging, the most common blocker is having only a stripped image with no symbols or line tables.
Keep (or be able to re-create) at least:
The exact linked ELF that ran (same build-id if you use build-ids).
An unstripped ELF (or a separate
.debugfile) that matches the runtime image.The ELD map file (
--Map=... --MapStyle=Textor YAML) for fast address-to-section/symbol correlation.The crash report: PC/LR/SP, full backtrace if available, and a core dump when possible.
If your production image must be stripped, keep debug info out-of-band using
llvm-objcopy (or GNU objcopy):
llvm-objcopy --only-keep-debug app app.debug
llvm-objcopy --strip-debug --strip-unneeded app app.stripped
llvm-objcopy --add-gnu-debuglink=app.debug app.stripped
6.21.2. Use linker map files for layout correlation
When debugging runtime failures that are layout-sensitive (for example: wrong PLT/GOT access, unexpected text/rodata placement, thunk/trampoline differences, RELRO placement), generate and keep a linker map file for the exact link:
eld ... -M --Map=layout.map --MapStyle=Text
The map file is often the fastest way to answer: “which output section/segment contains this address?” and “why did this archive member/section get pulled in?”.
6.21.3. Symbolize a crash address (PC) quickly
If you have an address from a crash report (for example PC=0x...) you can
usually get file:line without opening a debugger:
# Pick one:
llvm-addr2line -f -C -e ./app 0xADDR
addr2line -f -C -e ./app 0xADDR
For PIE executables and shared libraries under ASLR, 0xADDR is typically a
runtime virtual address. Convert it to an ELF-relative address first:
Find the module load base (
/proc/<pid>/mapsfor a running process, orimage list -o -fin lldb for a core).Compute
REL = ADDR - BASE.Run
addr2lineonRELusing the corresponding module file.
If you are using sanitizers, make sure symbolization is enabled and points at a working symbolizer:
LLVM_SYMBOLIZER_PATH=/path/to/llvm-symbolizerASAN_OPTIONS=symbolize=1:abort_on_error=1(plus your project defaults)UBSAN_OPTIONS=print_stacktrace=1
6.21.4. Debug with lldb (core dumps and live debugging)
For a core dump:
ulimit -c unlimited
./app # reproduce crash
lldb -c core ./app
On systems using systemd-coredump, coredumpctl is often the easiest:
coredumpctl list ./app
coredumpctl dump <PID> --output=core
lldb -c core ./app
In lldb, start with:
bt/thread backtrace allregister readanddisassemble -m -p --start-address $pcimage list -o -f(verify loaded modules + load addresses)
If you cannot run the image locally (cross/embedded), use lldb-server on the
target and attach from the host toolchain debugger.
6.21.5. Run musl builds under qemu (quick cross-runtime triage)
If you need a fast, reproducible runtime environment (especially for cross-target issues), a practical workflow is: build a small musl-based binary and run it under qemu (user-mode or system-mode).
6.21.5.1. User-mode qemu (recommended for simple tests)
For Linux user-mode emulation, run the target executable directly:
# Examples (pick your target qemu):
qemu-aarch64 ./app
qemu-arm ./app
qemu-riscv64 ./app
qemu-riscv32 ./app
qemu-x86_64 ./app
If the binary is dynamically linked, point qemu at a sysroot that contains the target loader + shared libraries (musl or glibc as appropriate):
qemu-aarch64 -L /path/to/<triplet>/sysroot ./app
Common qemu-user tips:
-straceprints syscalls (useful when a crash is actually anENOENT/ loader issue).-E VAR=...sets environment variables for the emulated program (for example-E LD_LIBRARY_PATH=...).-d in_asm,cpu,exec -D qemu.loglogs executed instructions; it is noisy but can pinpoint the last instruction before a crash.-g <port>enables a gdbstub; you can attach with lldb using gdb-remote:qemu-aarch64 -g 1234 ./app lldb ./app (lldb) gdb-remote 1234
6.21.5.2. System-mode qemu (when you need a full OS image)
Use system-mode (qemu-system-*) when user-mode is insufficient (for example:
kernel/driver interactions, missing syscalls, or you need a full rootfs).
In system-mode, typical debugging flags include:
-s -S(open gdbstub and stop at reset)-d in_asm,cpu,exec -D qemu.log(instruction logging; very verbose)
6.21.6. Inspect exception handling and unwinding
Runtime failures that look like “crash while unwinding”, “terminate called after throwing”, or incorrect backtraces typically reduce to missing/mismatched unwind or exception tables.
At link time, ELD may merge/synthesize unwind-related sections (for example
.eh_frame and .eh_frame_hdr) and can also process SFrame
(.sframe with --sframe-hdr). For ARM EHABI you may also see
.ARM.exidx / .ARM.extab.
Quick checks:
llvm-readelf -S ./app | grep -E \"eh_frame|eh_frame_hdr|gcc_except_table|ARM\\.exidx|ARM\\.extab|sframe\"
llvm-readelf --unwind ./app # unwind info (includes .eh_frame when present)
Common causes of missing/insufficient unwind info:
Built without unwind tables (toolchain flags such as
-fno-asynchronous-unwind-tables,-fno-unwind-tables).Over-aggressive stripping (for example link-time
--strip-debug) combined with not keeping a matching.debugfile.Inconsistent binaries/libraries at runtime (debugging with one ELF but running a different one; mismatched build-ids).
For C++ exceptions: missing runtime pieces (for example
__gxx_personality_v0not resolved, or the wrong unwinder/libgcc_s on the target).
If the problem is “backtrace is garbage” rather than exceptions specifically,
also consider building with frame pointers (for example -fno-omit-frame-pointer)
and validating that your unwinder matches the format your toolchain emits
(.eh_frame vs SFrame vs target-specific unwind tables).
6.21.8. Target ABI / relocations / GOT-PLT debugging notes
When debugging runtime crashes that involve dynamic linking, relocation application, or address materialization sequences, it helps to look at:
Relocations:
llvm-readelf -r ./appandllvm-readelf --dyn-relocations ./app.Dynamic table:
llvm-readelf -d ./app(NEEDED,RPATH/RUNPATH, flags).Program headers:
llvm-readelf -l ./app(PT_LOADflags/alignment,PT_GNU_RELRO).GOT/PLT-related sections and their contents:
llvm-readelf -S ./app | grep -E \"\\.plt|\\.got|\\.rela(\\.plt|\\.dyn)|\\.rel(\\.plt|\\.dyn)\" llvm-readelf -x .got ./app 2>/dev/null | less llvm-readelf -x .got.plt ./app 2>/dev/null | less llvm-readelf -x .plt ./app 2>/dev/null | less
Correlate entries with disassembly + relocations:
llvm-objdump -dr --no-show-raw-insn ./app | less llvm-readelf -r ./app | less
6.21.8.1. External ABI / ELF references (authoritative relocation tables)
When you need a definitive answer for relocation semantics, PLT/GOT conventions, TLS models, or calling convention/stack rules, prefer the architecture psABI/ABI documents (rather than reverse-engineering from a tool implementation).
Useful starting points:
Generic ELF / System V ABI:
Linux Foundation reference index: ELF and ABI Standards
System V gABI Edition 4.1: gabi41.pdf
ELF Object File Format (modern publication of the ELF chapters): Xinuos ELF spec (HTML)
x86_64 System V psABI:
psABI PDF: x86_64-SysV-psABI.pdf
Sources/repo: x86-64 psABI (GitLab)
Arm AArch32 / AArch64:
Official Arm ABI repository (AAELF32/AAELF64, PCS, EHABI, etc): ARM-software/abi-aa
AAELF (Arm ELF, AArch32): IHI0044E_aaelf.pdf
AAELF64 (AArch64 ELF): IHI0056G_2020Q2_aaelf64.pdf
RISC-V psABI:
Spec site: riscv-elf-psabi-doc
Sources/releases: riscv-elf-psabi-doc (GitHub)
Qualcomm Hexagon:
ABI user guide (includes Hexagon-specific ELF/relocations): Qualcomm Hexagon ABI User Guide (PDF)
Architecture-specific relocation names are target-defined; a quick way to see what you are dealing with is the relocation inventory command in this guide. The following are common relocation families you may see during runtime triage:
x86_64 ABI: look for
R_X86_64_*(for example:*_RELATIVE,*_JUMP_SLOT,*_GLOB_DAT,*_PC32,*_PLT32,*_GOTPCREL*).AArch64 ABI: look for
R_AARCH64_*(for example:*_CALL26,*_JUMP26,*_ADR_PREL_PG_HI21,*_ADD_ABS_LO12_NC,*_RELATIVE,*_JUMP_SLOT,*_GLOB_DAT).ARM ABI (arm/thumb): look for
R_ARM_*(for example:*_CALL,*_JUMP24,*_THM_CALL,*_THM_JUMP24) and, on EHABI platforms, unwind tables like.ARM.exidx/.ARM.extab.RISC-V 32/64: look for
R_RISCV_*(for example:*_PCREL_HI20,*_PCREL_LO12_*,*_CALL*,*_JAL, plus dynamic relocations like*_RELATIVE,*_JUMP_SLOT,*_GLOB_DAT).Hexagon ABI: look for
R_HEX_*relocations; usellvm-readelf -rand disassembly to connect the relocation type to the instruction sequence.
6.21.8.2. ARM-specific: verifying veneers/thunks
On ARM/Thumb, the linker may need to create veneers/thunks (branch islands) when branches cannot reach their targets or when interworking is required.
When a crash looks like a bad branch target or a call landing in the wrong mode:
Enable and inspect trampoline/thunk diagnostics and maps:
--trampoline-map(plus the usual text map file).Disassemble around the call site and look for a veneer sequence and its relocation(s):
llvm-objdump -dr --start-address=... --stop-address=....Confirm the callee symbol type and interworking expectations in the symbol table (
llvm-readelf -s --extra-sym-info).
6.21.9. Minimize runtime failures with A/B experiments
When a runtime crash is hard to reason about, treat it like a minimization problem: change one knob at a time until the failure becomes deterministic, then pinpoint which library/object/relocation pattern is responsible.
Dynamic-linking knobs that often turn “mystery crashes” into actionable data:
Force eager binding (converts lazy-PLT crashes into startup failures):
LD_BIND_NOW=1 ./app
Or make it a link-time property of the binary:
-Wl,-z,now.Strengthen shared-library resolution rules (catch unresolved imports sooner):
-Wl,-z,defs(or-Wl,--no-undefineddepending on toolchain).Toggle dependency pruning and interposition-related behavior:
-Wl,--as-needed/-Wl,--no-as-neededand (when applicable)-Wl,-Bsymbolic/-Wl,-Bsymbolic-functions.Toggle RELRO (affects which GOT/relocation targets become read-only at runtime):
-Wl,-z,relroand-Wl,-z,norelro.
Use these knobs together with map files and relocation inspection (see below) to connect “crash address” -> “instruction” -> “relocation” -> “symbol” -> “DSO that provides it”.
6.21.11. Switching toolchains/compilers to bisect regressions
If the failure appeared after a toolchain update, a fast way to narrow root cause is to bisect one dimension at a time:
Swap compiler only (keep assembler/linker constant) to see if codegen changes are responsible.
Swap linker only (keep compiler constant) to see if layout/relocation handling is responsible.
Swap runtime libraries (libc, libstdc++/libc++, libgcc_s/libunwind) to catch ABI or unwinder differences.
Use build-ids, map files, and the relocation inventory command to keep these experiments grounded in “what changed” rather than “what you think changed”.
6.21.12. Write small regression tests (symbols/relocs/dynamic flags)
When you can reproduce a runtime failure, try to turn it into a small link-time observable property so it can be tested without running on a target.
In this repo, most linker tests are lit tests (*.test) that:
compile tiny C/asm inputs,
run
%link(eld) with specific flags, andverify ELF properties using
%readelf(llvm-readelf),llvm-readobj, or%objdump+FileCheck.
Examples of properties that correlate strongly with runtime behavior:
Symbol kind/binding/visibility (from
llvm-readelf -s --extra-sym-info/nm).Relocation types and placement (from
llvm-readelf -r/llvm-readobj -r).Dynamic linking flags and segments (
llvm-readelf -dforBIND_NOW/FLAGS_1, andllvm-readelf -lforPT_GNU_RELROandp_align).
If you need a starting point, see test/Templates/ExampleOfMyLitTest.test and
existing tests under test/lld/ELF that check relocations and flags like
-z now via %readelf/%objdump.
Illustrative lit pattern (dynamic flags + RELRO):
# RUN: %link %linkopts -shared -z now -z relro %t.o -o %t.so
# RUN: %readelf -d %t.so | FileCheck %s --check-prefix=DYN
# RUN: %readelf -l %t.so | FileCheck %s --check-prefix=PHDR
# DYN: BIND_NOW
# PHDR: GNU_RELRO
6.21.13. Other useful inspection tools
Address/section correlation:
llvm-nm -n,nm -n,llvm-objdump -d,objdump -d, and the ELD map file.ELF metadata:
llvm-readelf -h -l -S -s -n(build-id inllvm-readelf -n).Relocation inventory across a build tree (quickly see which relocation types are present in objects/archives):
find . -name \"*.o\" -o -name \"*.a\" | xargs llvm-readelf -r | awk '{print $3}' | sort -u | grep R_
More robust (handles spaces in paths and avoids
findprecedence traps):find . \\( -name \"*.o\" -o -name \"*.a\" \\) -print0 \\ | xargs -0 llvm-readelf -r \\ | awk '{print $3}' \\ | sort -u \\ | grep R_
Inspect relocations around a crash site (when investigating wrong codegen, bad PLT/GOT usage, or runtime loader fixups):
# Disassemble with relocations shown (pick one): llvm-objdump -dr --no-show-raw-insn ./app | less objdump -dr --no-show-raw-insn ./app | less # Narrow to a region around an address (adjust for PIE/ASLR first): llvm-objdump -dr --start-address=0xSTART --stop-address=0xSTOP ./app
Segment properties / page alignment / RELRO (loader-relevant):
llvm-readelf -l ./app # program headers: p_flags, p_align, PT_LOAD, PT_GNU_RELRO, PT_INTERP llvm-readelf -d ./app # dynamic tags: DT_BIND_NOW, (FLAGS / FLAGS_1), RPATH/RUNPATH
System-call tracing:
strace -f(andltracewhen applicable).Memory corruption: ASan/UBSan/TSan builds;
valgrindwhen supported.