Diagnosing GCC Link-Time Optimization (LTO) Symbol Visibility & Resolution Failures
A diagnostic guide for GCC 7–13 Link-Time Optimization failures: recognize undefined reference and multiple definition errors, trace root causes through symbol visibility, ODR flag mismatches, and mixed LTO/non-LTO objects, then apply targeted fixes with version-aware commands.
11 Aug 2025, 11:25 UTC

Recognizing the Problem
When you enable -flto in a GCC 7–13 build, you may see linker errors that never appear in a non–LTO build:
undefined reference to ‘foo’multiple definition of ‘bar’- plugin-related messages such as
lto: error: plugin failed
These errors usually surface during the final link step, after all object files have been compiled with -flto and the LTO plugin runs.
Common Causes
| Cause | Typical Symptom |
|---|---|
| Hidden or static inline functions referenced across units | Undefined reference |
ODR violations due to differing compiler flags (e.g., -fno-rtti, -fno-exceptions) | Multiple definition or weak symbol conflicts |
| Mixing LTO objects with non–LTO objects or libraries | Linker aborts or missing symbols |
Static libraries built without -flto linked into an LTO final link | Undefined references or duplicate definitions |
Incorrect symbol visibility (e.g., hidden in a public API) | Undefined reference to API symbol |
Diagnostic Checklist
- Verify GCC version consistency
gcc -vAll compilers in the toolchain should report the same major.minor version. Mismatched versions (e.g., GCC 11 compiler with GCC 10 libstdc++) cause plugin protocol mismatches.
- Check object file type
file a.oOutput should contain
LTO objectfor LTO–compiled units. Plain ELF objects indicate-fltowas not used at compile time. - Inspect symbol visibility
nm -C a.o | grep ' t 'Local text symbols are shown as
t. Global symbols useT. Hidden visibility is oftenteven if intended to be exported. Usenm -C a.o | grep ' [TtDdBb] 'to see all symbol types. - Examine COMDAT groups for ODR mismatches
readelf -S a.o | grep COMDATDifferent flags across translation units will create separate COMDAT sections, leading to duplicate definitions. Compare section names across objects.
- Trace LTO plugin activity
gcc -flto -v -Wl,--verbose 2>&1 | grep -E '(lto|plugin|resolution)'Look for messages about plugin failures or symbol resolution. The verbose linker output shows which plugin instance handles each object.
- Confirm all inputs are LTO objects
find . -name '*.o' -exec file {} \; | grep -v 'LTO object' | head -n 5Any non–LTO object indicates a mixed build. This includes objects from third-party static libraries.
- Check static libraries
ar -t libfoo.a file libfoo.aStatic libraries built without
-fltocannot be linked into an LTO final link; rebuild archives with-fltoor use-ffat-lto-objectsto embed both LTO and native code.
Step-by-Step Fixes
1. Align Flags Across Translation Units
Ensure every file is compiled with the same set of flags to prevent ODR violations, especially:
-fno-rtti-fno-exceptions-fvisibility=hidden-std=dialect (e.g.,-std=c++17vs-std=gnu++17)
Use a common build system variable or CMake target property to enforce consistency.
2. Expose Needed Symbols
If a symbol is hidden but needs to be accessed across units, use __attribute__((visibility("default"))) on the declaration, or verify with nm -C that the T entry exists. For C++ inline functions intended for cross-TU use, avoid static inline in headers; use inline without static and ensure the definition is visible.
3. Rebuild Static Libraries with LTO
If you must use pre-compiled static libraries in an LTO link, rebuild them with -flto. Alternatively, compile your objects with -ffat-lto-objects (default in GCC 7–10, off by default in GCC 11+) so each object contains both LTO IR and native code, allowing the linker to fall back to native code for non-LTO archives. Note: -ffat-lto-objects increases object size and compile time.
4. Resolve Duplicate Definitions from COMDAT Mismatches
When readelf -S shows multiple COMDAT groups for the same symbol (e.g., .group.__ZN3Foo4barEv with different signatures), identify the translation units with divergent flags. Common culprits: one TU compiled with -fno-rtti, another without. Recompile all affected TUs with identical flags.
5. Handle Thin LTO vs Full LTO Differences
GCC 11+ supports -flto=thin (ThinLTO) which partitions work differently. If full LTO works but ThinLTO fails (or vice versa), try the other mode to isolate whether the issue is partitioning-related. ThinLTO may expose ODR violations that full LTO's whole-program view silently resolves.
Escalation Criteria
Escalate to a GCC bug report or vendor support when:
- All diagnostic checks pass (consistent version, all LTO objects, aligned flags, correct visibility) but linker still reports undefined/multiple definitions.
- Plugin crash occurs:
lto: fatal error: bytecode stream in file '...' generated with LTO version X.Y instead of A.B— indicates version skew between compiler and linker plugin. - Internal compiler error (ICE) during LTO optimization phase with
-flto -vshowinglto1crash.
Before reporting, create a minimal reproducer: two or three source files, exact compiler invocations, and the full -v output from both compile and link steps.
Quick Verification After Fix
After applying a fix, rebuild and verify:
# Clean build
make clean && make
# Confirm binary size reduction (typical LTO benefit)
size myprogram
# Run basic smoke test
./myprogram --version
If link time is excessive, try -flto=thin -flto-partition=balanced (GCC 11+) and compare link memory usage with /usr/bin/time -v gcc ....
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.