LTO Archives in Cross Builds: Keep Compiler Plugins and Binutils Compatible
A static archive containing GCC LTO objects is more than a collection of target machine instructions. Its consumer may need to read GCC’s intermediate representation through a compatible compiler and linker plugin. When an archive is visible but linking fails, establish that tool relationship before changing library order.
This guide pins GCC 14.2 and GNU BFD Binutils 2.44. The executed fixture used one matching x86-64 host toolchain to contrast native, slim-LTO and fat-LTO archives. Cross-version compatibility and an embedded target build remain untested.
Identify what the archive actually contains
List archive members and inspect representative objects with readelf -SW. GCC’s LTO representation uses special ELF sections; the presence of an ELF header does not mean the member contains a complete native-code fallback. Keep the member’s producer identity beside the section listing.
The GCC 14.2 optimization manual distinguishes fat objects, which carry intermediate representation and native object code, from non-fat objects that require an LTO-aware toolchain. It also describes strict bytecode version checks. Do not promise that an arbitrary newer compiler can consume an older LTO archive merely because both compilers target the same CPU.
An archive can mix native and LTO members, and only selected members may participate in a particular link. Inspect the member implicated by the failure rather than declaring the entire archive compatible because one symbol was listed successfully. Preserve the archive hash and member names before rebuilding anything.

Build a producer-to-consumer ledger
Record the producer compiler’s full version, configuration, target tuple and relevant compile options. Include the LTO mode, optimization settings, ABI-affecting flags and whether fat objects were requested. Record the consumer compiler driver, actual linker, archive tools, plugin path and plugin hash separately.
The linker plugin is a host-executed tool, while the resulting machine code belongs to the target architecture. A valid target tuple cannot compensate for a plugin that the build host cannot load. Conversely, a plugin that loads successfully does not prove that its intermediate-representation reader matches the archive producer.
Use the delivered compiler’s read-only queries, such as -dumpmachine, -dumpfullversion and -print-file-name=liblto_plugin.so, as evidence. Check whether the last query returned an existing path; an unchanged filename alone does not establish that a plugin was found. Resolve wrappers and symlinks so the ledger identifies actual tools.
Keep archive tools inside that contract
For GCC’s non-fat LTO workflow, use the matching gcc-ar, gcc-ranlib and gcc-nm wrappers unless the delivered toolchain explicitly supplies equivalent plugin-aware behavior. The wrappers are part of the producer/consumer contract, not interchangeable names for whichever ar happens to be first in PATH.
Modern Binutils can also discover installed plugins automatically. That explains why a plain ar or nm invocation may work on one workstation and fail in a clean SDK consumer environment. Record how plugin discovery occurred instead of depending on an undocumented workstation installation.
The Binutils 2.44 archive and symbol-tool documentation supplies the inspection controls. Listing an archive’s members is weaker evidence than reading their symbols through the required plugin. Preserve both results so a missing archive index is not confused with unreadable intermediate representation.
Use a clean consumer matrix
The fixture defined a tiny integer function and built three fresh archives from the same source. One contained ordinary native code. One used -flto with -fno-fat-lto-objects. The third used -flto with -ffat-lto-objects. Matching gcc-ar and gcc-ranlib created and indexed the archives.
| Archive and consumer mode | Observed host outcome |
|---|---|
| Native archive, ordinary compiler-driver link | Linked and returned the expected result. |
| Slim LTO archive, matching LTO driver | Linked and returned the expected result. |
| Fat LTO archive, LTO and plugin explicitly disabled | Native fallback linked and ran. |
| Slim LTO archive, LTO and plugin explicitly disabled | Link failed. |
The same GCC 14.2.0 and BFD 2.44 installation produced and consumed these objects. No alternate compiler version was installed or tested. The negative case demonstrates that this slim archive depends on the LTO path; it does not reproduce every possible plugin-version diagnostic.
Read failure evidence before choosing a repair
A plugin-load error points first toward the host tool or discovery environment. An intermediate-representation version rejection points toward producer/consumer compatibility. A normal undefined symbol after successful LTO processing still needs an ordinary symbol and archive-selection investigation. Keep those failure stages separate in the incident record.
Perform the final LTO link through the appropriate GCC driver, retaining the full command and response-file contents. A hand-written direct ld invocation can omit the compiler’s plugin and runtime setup. Replacing the driver should be a controlled experiment, not an undocumented production workaround.
Do not patch version markers in the object or copy an unrelated plugin into a global tool directory. Rebuild the archive with the consumer’s supported toolchain, deliver its matching SDK, or deliberately supply a qualified native-code distribution. Fat objects can offer a non-LTO path, but that path still has target ISA, ABI and runtime requirements.
Qualify the delivered SDK independently
Extract the SDK into a fresh location and build a small consumer using only documented inputs. Confirm that the selected plugin and archive tools come from the intended delivery, not the producer’s original build tree. A relocated path and an isolated dependency environment are separate tests.
For the product, repeat the consumer’s relevant behavior on its authorized target and retain the map, artifact hashes and tool ledger. Compare a native baseline with the intended LTO configuration before attributing a failure to optimization. The handover should state the supported compiler/plugin pairing and whether a native fallback exists, rather than calling the archive universally portable.