Link ELF Objects Directly with ld.lld-20

ld.lld-20 links object files, archives and libraries directly, with none of the hand-holding your compiler driver normally does. This guide builds a relocatable ELF object and a shared library directly with ld.lld-20, plus a repeatable way to inspect what the linker is actually doing. It uses Ubuntu LLD 20.1.8 from package lld-20. Allow about 15 minutes. You need a shell, GCC or another compiler to produce an object file, and enough write access to a temporary working directory.

Checkpoint: this is the ELF linker, not the compiler driver. It combines object files, archives and libraries, resolves symbols and writes an executable, shared library or another object. For a normal application, let gcc or clang invoke the linker: the driver supplies startup objects, library paths and the dynamic loader for you. Call ld.lld-20 directly only when you need that level of control or you are building a linker-oriented workflow.

1. Confirm the installed linker

Check the exact binary before relying on any option it offers:

$ ld.lld-20 --version
Ubuntu LLD 20.1.8 (compatible with GNU linkers)

The manpage describes the ELF mode of LLD and is dated 25 July 2023, while the installed executable reports 20.1.8. Keep both facts in mind before copying advice written for a different LLVM release. Use --help for the option set on this machine:

$ ld.lld-20 --help | sed -n '1,8p'
OVERVIEW: lld

USAGE: ld.lld-20 [options] file...

No elevated privileges are needed for these checks. Do not write output into a system library directory while experimenting.

2. Produce a small object file

Compile source to an object, stopping before the compiler driver performs a final link. This example uses a harmless function so the resulting file is easy to inspect:

$ work=$(mktemp -d)
$ gcc -x c -c -o "$work/math.o" - <<'EOF'
int answer(void) { return 42; }
EOF
$ file "$work/math.o"
/tmp/.../math.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

The temporary directory is disposable, but keep its path in $work for the rest of the session. The input architecture has to match the target you are linking: a cross compiler can produce a different architecture, but then you need the matching target and runtime inputs too.

Checkpoint: if file does not report an ELF relocatable object, stop here. The linker cannot repair a source file, a text file, or an object built for the wrong architecture.

3. Combine objects without making an executable

Use -r, also written --relocatable, to make another relocatable object. Relocations and symbols stay available for a later link:

$ ld.lld-20 -r -o "$work/combined.o" "$work/math.o"
$ file "$work/combined.o"
/tmp/.../combined.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Useful whenever a build stage needs to merge several objects before a final executable or shared-library link. -o names the output; leave it out and the manpage says the default is a.out, an easy distraction and a poor choice for scripted builds.

For a reproducible diagnostic, add --print-map to print the link map to standard output, or --Map="$work/link.map" to save it. A map records how input sections and symbols were placed. Treat it as build output and review it before ever committing it to a source tree.

4. Build a shared object

Use --shared for a shared object. The same object from step 2 is enough for a minimal demonstration:

$ ld.lld-20 --shared -o "$work/libanswer.so" "$work/math.o"
$ file "$work/libanswer.so"
/tmp/.../libanswer.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not stripped

For a real library, give the object code position-independent input, set an appropriate soname with --soname=libanswer.so.1, and link its required libraries deliberately. The manpage says --allow-shlib-undefined is enabled by default for shared-library links. Use --no-undefined when you want unresolved references to be an outright error:

$ ld.lld-20 --shared --no-undefined -o "$work/libanswer.so" "$work/math.o"
$ readelf -h "$work/libanswer.so" | grep 'Type:'
  Type:                              DYN (Shared object file)

Safety boundary: do not use --no-undefined blindly on a plugin that is intentionally resolved by its host. Conversely, do not accept an accidental unresolved symbol just because the default happens to permit it for shared objects.

5. Link libraries in a deliberate order

Use -L to add a library search directory and -lNAME to search for libNAME.so or libNAME.a. Put an object or library that needs symbols before the library that supplies them: static archives are processed as the linker encounters them, so order changes the result.

Mutually dependent archives are a common failure point. LLD can use --start-group and --end-group to rescan a group, and its implementation notes describe --warn-backrefs for spotting invocations that may behave differently from a traditional Unix linker:

$ ld.lld-20 --warn-backrefs -o "$work/app" "$work/main.o" \
    -L"$work/lib" -lfirst -lsecond
ld.lld: error: cannot open ...

That final line is deliberately an example of a failure shape, not a claim that those placeholder files exist. Replace every placeholder with real objects and libraries. If a symbol goes missing, rerun with --trace to print input filenames, --trace-symbol=NAME to follow one symbol, and --verbose for more linker detail. Do not silence the problem with --allow-multiple-definition unless picking the first definition is explicitly safe to do.

6. Preserve a failing link for reproduction

When a link only fails in a build environment, use --reproduce=PATH. LLD writes a tar archive containing the needed inputs, the command-line options in response.txt, and the linker version in version.txt:

$ ld.lld-20 --reproduce="$work/link-reproduction.tar" -r \
    -o "$work/reproduced.o" "$work/math.o"
$ tar -tf "$work/link-reproduction.tar"
response.txt
version.txt

Keep that archive private if the input objects contain proprietary code, embedded paths or secrets: it is a copy of build inputs, not a harmless log. Delete it from shared storage after the investigation, per your retention policy. The environment variable LLD_REPRODUCE can request the same kind of archive, but an explicit --reproduce option takes precedence over it.

7. Clean up and verify the result

Inspect the output before it goes anywhere near a build or deployment directory:

$ readelf -h "$work/combined.o" | grep 'Type:'
  Type:                              REL (Relocatable file)
$ readelf -h "$work/libanswer.so" | grep 'Type:'
  Type:                              DYN (Shared object file)
$ rm -rf "$work"

Warning: that final command removes only the temporary directory created in step 2. If you used a different path, substitute that exact path; never paste a broad directory or an unset variable into a recursive removal command. There is no undo for rm -rf, so run printf '%s\n' "$work" first if you are at all unsure. Linking itself needs no root. Elevated privileges belong only in a separate installation or deployment step, after you have reviewed the resulting files.

Done means