What Is Symbol Resolution?
main.cpp calls add(3, 4), but the implementation lives in math.cpp. Each file compiles on its own. Symbol resolution is how the linker connects that call to its definition.
Select a reference below, then omit its definition or add a duplicate. The lookup must succeed before the linker can relocate the reference to its final destination.
Symbol Resolution
How does an undefined reference in main.o find its definition? Select a reference, then change the linked inputs.
References in main.o
Resolution process
1. Read the reference
main.o needs _Z3addii
2. Search matching definitions
- math.o · strong definition_Z3addii
3. Apply the binding rules
One strong definition satisfies the reference. The linker can apply its relocation.
Reference resolved to math.o
Source files: main.cpp, math.cpp and utils.cpp
int add(int, int);
void print_result(int);
extern int global_count;
extern int uninitialized_count;
int main() {
print_result(add(3, 4));
print_result(global_count);
print_result(uninitialized_count);
return 0;
}int global_count = 42;
int uninitialized_count; // Zero-initialized BSS; a strong definition
const int MAX_RETRIES = 3; // Internal linkage; local read-only data
extern const int PUBLIC_LIMIT = 100; // External read-only definition
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }#include <cstdio>
__attribute__((weak)) int debug_mode = 0;
__attribute__((weak)) void default_hook() {}
void print_result(int value) {
std::printf("Result: %d\n", value);
}A typical C++ build preprocesses, compiles, and assembles each translation unit into an object file (.o), then links those files and libraries. Object files record both definitions and unresolved references. A required reference with no definition produces an "undefined reference" error; conflicting strong definitions produce a "multiple definition" error.
The examples here use GCC/Clang on ELF systems and GNU nm conventions. The opening trace and symbol inspector share the same three source files. Their symbol sizes and scenario changes are authored illustrations; they do not run a compiler in your browser. Expand Source files in the opening figure to inspect the complete example.
Symbol Types
Two properties matter: where a symbol lives (code, initialized data, BSS, or read-only data) and how it binds (local, global, or weak). nm compresses this information into a type letter; readelf -s exposes binding and section separately.
Strong Symbols
Ordinary externally linked function and variable definitions are strong, including uninitialized C++ globals. Two strong definitions of the same external symbol conflict in a normal link.
// math.cpp
int global_count = 42; // Strong: initialized data (D)
int uninitialized_count; // Strong: zero-initialized BSS (B)
const int MAX_RETRIES = 3; // Internal linkage: local read-only data (r)
extern const int PUBLIC_LIMIT = 100; // External read-only definition (R)
int add(int a, int b) { // Strong: function definition (type T)
return a + b;
}
T, D, B, and R identify different storage sections, not different strengths. A namespace-scope non-volatile const variable such as MAX_RETRIES has internal linkage by default in this example, so it appears as local r and cannot satisfy another file’s external reference. extern const with an initializer defines an externally linked object. Optimizations may remove unused local constants.
Weak Symbols
GNU attributes can explicitly mark a function or object as weak. Among the inputs actually included in a link, a strong definition takes precedence over weak definitions. With only weak definitions, the linker can select one.
// utils.cpp
__attribute__((weak)) int debug_mode = 0; // Weak object (V)
__attribute__((weak)) void default_hook() {} // Weak function (W)
Weak symbols let libraries provide default implementations that application code can override. Archive extraction still matters: a definition inside an unextracted archive member does not participate. An undefined weak reference alone normally does not extract an archive member.
Inline functions and template instantiations are often emitted in mergeable groups, sometimes with weak binding. They may also be inlined or omitted. Their C++ definitions must still obey ODR; weak binding is not permission to supply different bodies.
Undefined Symbols
A use that needs an external definition can produce an undefined symbol in the object file. A declaration alone does not necessarily produce an entry: the compiled code must reference it. These calls and variable uses create the four external references in main.o:
// main.cpp
extern int global_count; // Undefined: declared but not defined here
extern int uninitialized_count;
int add(int a, int b); // Undefined: declared but not defined here
void print_result(int val); // Undefined: declared but not defined here
int main() {
print_result(add(3, 4));
print_result(global_count);
print_result(uninitialized_count);
return 0;
}
In nm output, undefined symbols show up as U:
$ nm main.o # Illustrative layout; addresses vary with the build
U _Z3addii
U _Z12print_resulti
U global_count
U uninitialized_count
0000000000000000 T main
Resolution Rules
For ordinary external references in the object files included in this link, the binding rules are:
Multiple Strong Symbols → Error
If two object files both contain a strong definition of the same symbol, the linker rejects the program:
// file_a.cpp
int config_value = 10; // Strong definition
// file_b.cpp
int config_value = 20; // Another strong definition — conflict!
$ g++ file_a.o file_b.o -o program
/usr/bin/ld: file_b.o: multiple definition of `config_value';
file_a.o: first defined here
collect2: error: ld returned 1 exit status
One Strong + One or More Weak → Strong Wins
If an included object file has a strong definition and others have weak definitions, the linker picks the strong one:
// default.cpp
__attribute__((weak))
int timeout = 30; // Weak definition (default)
// app.cpp
int timeout = 60; // Strong definition (override)
// The linker uses 60
Multiple Weak Symbols → Linker Picks One
If no strong definition exists and multiple weak definitions are present, the linker selects one, often according to input order. Do not rely on a diagnostic or use this as a way to choose between different inline/template bodies.
No Definition → Undefined Reference Error
If a symbol is referenced but never defined anywhere, the linker fails:
$ g++ main.o -o program
/usr/bin/ld: main.o: undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit status
Name Mangling
C++ supports function overloading — multiple functions can share the same name as long as their parameter types differ. But the linker works with flat symbol names, not C++ type information. To make overloading work at the linking level, the compiler mangles each function name by encoding the parameter types into the symbol.
A calculated Itanium ABI teaching example for GCC/Clang on Linux. Enter ordinary function, namespace or class names with builtin scalar types, optional parameter names, const/volatile, pointers and references. Trailing const/volatile assumes a class member. Templates, aliases, user-defined types, operators, constructors, defaults and other declarators require a compiler. The vector preset is a fixed example.
Presets
C++ Signature
void foo(int, double)
Mangled Symbol
_Z3fooid
How it works: _Z + 3foo + i (int) + d (double)
echo '_Z3fooid' | c++filtRun this command locally. This demo does not execute a compiler or c++filt.
Itanium ABI Mangling (GCC/Clang)
Most compilers on Linux and macOS follow the Itanium C++ ABI for name mangling. The mangled name starts with _Z, followed by the name length, the function name, and encoded parameter types:
void func(int) // _Z4funci (i = int)
void func(double) // _Z4funcd (d = double)
void func(int, int) // _Z4funcii (ii = int, int)
int add(int, int) // _Z3addii
Demangling with c++filt
When you see a mangled symbol in an error message or nm output, use c++filt to decode it:
$ echo '_Z3addii' | c++filt
add(int, int)
$ nm -u main.o | c++filt
U add(int, int)
U print_result(int)
U global_count
U uninitialized_count
extern "C" to Disable Mangling
When you need to call a C library from C++ (or expose a C++ function to C callers), use extern "C" to disable mangling:
// This function will have the symbol name "c_function", not "_Z10c_functionv"
extern "C" void c_function() {
// ...
}
// For C headers included in C++ code
extern "C" {
#include <legacy_c_library.h>
}
The declaration used by the caller and the definition must agree on language linkage. On these targets, C linkage gives this function the plain symbol name c_function. This does not make arbitrary C++ types or class APIs compatible with C.
The One Definition Rule (ODR)
The One Definition Rule governs C++ entities, including entities that never appear as linker symbols. A translation unit cannot contain multiple definitions of the same definable item. An odr-used ordinary non-inline function or variable needs one definition in the program. Certain entities, including inline functions, inline variables, classes, and templates, may have definitions in multiple translation units under matching-definition conditions.
The linker catches many duplicate ordinary definitions, but it cannot validate every ODR requirement. Some violations make the program ill-formed without requiring a diagnostic.
The Classic ODR Violation
The most common ODR violation is defining a variable in a header file:
// config.h — ODR VIOLATION
#ifndef CONFIG_H
#define CONFIG_H
int MAX_SIZE = 100; // This is a DEFINITION, not just a declaration!
#endif
// app.cpp
#include "config.h" // Defines MAX_SIZE in app.o
// utils.cpp
#include "config.h" // Defines MAX_SIZE again in utils.o
When you link app.o and utils.o, the linker sees two strong definitions of MAX_SIZE and rejects the program.
Fixes for ODR Violations
// Fix 1: extern declaration in header + definition in one .cpp
// config.h
extern int MAX_SIZE; // Declaration only
// config.cpp
int MAX_SIZE = 100; // Single definition
// Fix 2: inline variable (C++17)
// config.h
inline int MAX_SIZE = 100; // Matching definitions may appear across TUs
// Fix 3: one shared constant object (C++17)
// config.h
inline constexpr int MAX_SIZE = 100;
Permitted Definitions Across Translation Units
These entities remain subject to ODR, including token and name-lookup requirements:
- Inline functions and variables may have matching definitions in multiple translation units while denoting one entity.
- Templates and classes may have matching definitions in multiple translation units under the rule’s conditions. Full specializations need separate care; they are not automatically inline.
- Namespace-scope
constexprvariables normally have internal linkage, so each translation unit gets its own object. Useinline constexprfor one shared object.constexprfunctions and, since C++17,constexprstatic data members are implicitly inline.
Definitions Must Match Across Translation Units
Different macro expansions can change an inline function’s body between translation units. Even identical tokens can fail ODR if names resolve differently. A successful link does not establish that these definitions satisfy the language rules.
Template Instantiation
Templates are defined in header files, but the compiler only generates code for a template when it’s actually used. Each .cpp file that uses a template gets its own instantiation, and the linker deduplicates the identical copies.
Why Template Definitions Usually Live in Headers
If you put a template definition in a .cpp file, only that translation unit can instantiate it. Other TUs that include only the declaration won’t have the definition available to generate code:
// math.h
template<typename T>
T square(T x); // Declaration only — no definition
// math.cpp
template<typename T>
T square(T x) { return x * x; } // Definition in .cpp
// main.cpp
#include "math.h"
int main() { return square(5); }
// ERROR: undefined reference to 'int square<int>(int)'
The compiler compiling main.cpp sees the declaration of square but not its body, so it can’t generate the int specialization. The definition in math.cpp was never instantiated for int either, because math.cpp doesn’t call square(5).
Fixes
// Fix 1: Move the template definition to the header (most common)
// math.h
template<typename T>
T square(T x) { return x * x; }
// Fix 2: Explicit instantiation in the .cpp file
// math.cpp
template<typename T>
T square(T x) { return x * x; }
// Explicitly instantiate for the types you need
template int square<int>(int);
template double square<double>(double);
Explicit instantiation is useful for large templates where you want to control compile times and know all the types in advance. For most cases, keeping the definition in the header is simpler and more flexible.
Symbol Visibility
On ELF systems, externally linked definitions commonly have default visibility and can become part of a shared library’s exported interface. Internal-linkage entities are different: they are local regardless of this visibility default. Exporting unnecessary external symbols exposes implementation details and increases dynamic symbol lookup work.
Controlling Visibility
// Hide internal symbols from library users
__attribute__((visibility("hidden")))
void internal_helper() {
// Not accessible from outside the shared library
}
// Explicitly export public API
__attribute__((visibility("default")))
void public_api() {
internal_helper(); // Can still call hidden symbols internally
}
Compile-Time Default Visibility
For larger projects, compile the entire library with hidden default visibility and explicitly export only the public API:
# Hide all symbols by default
g++ -fPIC -fvisibility=hidden -shared -o libfoo.so foo.cpp
# Only symbols marked with visibility("default") are exported
Why Visibility Matters
- Smaller symbol tables — fewer symbols to look up at load time
- Faster library loading — less work for the dynamic linker
- Prevents accidental ABI exposure — internal functions can change without breaking users
- Enables better optimization — the compiler knows hidden functions can’t be overridden, so it can inline them more aggressively
Linking Order
The order in which you pass object files and libraries to the linker matters — especially for static libraries.
Static Libraries (.a)
Static libraries are archives of object files. In the usual GNU link, the linker extracts archive members needed by unresolved references at that point in the command line, including dependencies among members of that archive. Later inputs do not automatically make it revisit an earlier archive.
# main.o calls functions in libapp, which calls functions in libutils
# CORRECT: dependents before dependencies
g++ main.o -lapp -lutils
# WRONG: libutils is processed first, but nothing needs it yet — discarded
g++ main.o -lutils -lapp
# undefined reference to functions in libutils!
The rule is: put dependents before their dependencies on the command line.
Circular Dependencies
A cycle between archives can require another scan. GNU linker groups repeat archive searches until no further members are extracted:
# GNU ld: process the group repeatedly until no new symbols are resolved
g++ main.o -Wl,--start-group -lA -lB -Wl,--end-group
# Or list the library twice
g++ main.o -lA -lB -lA
Dynamic Libraries (.so)
Shared libraries still participate in link-time decisions. With GNU --as-needed, a library is kept only if it satisfies a qualifying unresolved reference at its position in the command line; object files appearing later do not make it needed retroactively. Put users before their libraries here too:
g++ main.o -Wl,--as-needed -L. -lutils -o program
Runtime binding is a separate step. Eligible function calls may use lazy binding, while eager binding resolves them during loading; data relocations generally cannot wait for a first call. Library search scope and symbol interposition can also make lookup order observable. See Loading for mapping and relocation, then Dynamic Linking for runtime lookup.
Viewing and Debugging Symbols
When linking fails, these command-line tools let you inspect exactly what symbols each object file defines and references.
Authored example of main.o, math.o and utils.o. Symbol sizes are illustrative; this page does not run a compiler. Binding and storage section are separate properties: BSS does not imply weak binding.
| Type | Symbol | Mangled | Size | Section | File |
|---|---|---|---|---|---|
| T | main | 64 | .text | main.o | |
| U | _Z3addii | — | *UND* | main.o | |
| U | _Z12print_resulti | — | *UND* | main.o | |
| U | global_count | — | *UND* | main.o | |
| U | uninitialized_count | — | *UND* | main.o | |
| T | _Z3addii | 18 | .text | math.o | |
| T | _Z8subtractii | 22 | .text | math.o | |
| T | _Z8multiplyii | 24 | .text | math.o | |
| D | global_count | 4 | .data | math.o | |
| B | uninitialized_count | 4 | .bss | math.o | |
| r | _ZL11MAX_RETRIES | 4 | .rodata | math.o | |
| R | PUBLIC_LIMIT | 4 | .rodata | math.o | |
| T | _Z12print_resulti | 56 | .text | utils.o | |
| U | printf | — | *UND* | utils.o | |
| V | debug_mode | 4 | .bss | utils.o | |
| W | _Z12default_hookv | 7 | .text | utils.o |
nm --demangle main.o math.o utils.oEssential Commands
# List all symbols with demangled C++ names
nm --demangle main.o
# Show only undefined symbols (what this file needs)
nm -u main.o
# Show only defined symbols (what this file provides)
nm --defined-only main.o
# Detailed section and symbol info
objdump -t main.o
# ELF-specific symbol table (more detail than nm)
readelf -s main.o
# Shared library dependencies
ldd ./program
# Demangle a single symbol
echo '_Z3addii' | c++filt
# add(int, int)
Reading nm Output
Defined-symbol lines normally contain an address, type letter, and symbol name; an undefined symbol has no address. GNU nm uses these letters:
| Type | Meaning |
|---|---|
T / t | Text (code) section — defined function |
D / d | Initialized data section |
B / b | BSS (uninitialized data) section |
R / r | Read-only data section |
U | Undefined — needs to be resolved |
W / w | Weak non-object symbol |
V / v | Weak object symbol |
Uppercase usually indicates an external symbol and lowercase usually indicates a local one. There are exceptions: lowercase w and v represent undefined weak symbols on ELF, and GNU u is a unique global symbol. A B symbol is not weak merely because its storage is zero-initialized. Use readelf -s to check LOCAL, GLOBAL, or WEAK binding explicitly. See the GNU nm reference.
Common Errors and Fixes
Authored source and diagnostic examples. Commands are shown for study; this page does not compile or run them. Actual diagnostics depend on the compiler and platform.
A function is declared in a header file but never defined in any source file. The linker cannot find the implementation.
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
// Declaration only — no definition provided anywhere
int add(int a, int b);
#endifg++ -o main main.cppIllustrative linker diagnostic
/usr/bin/ld: /tmp/ccA1B2C3.o: in function `main':
main.cpp:(.text+0x1a): undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit statusUndefined Reference
/usr/bin/ld: main.o: undefined reference to `MyClass::process(int)'
Common causes and fixes:
- Missing implementation — you declared a function but never wrote the body. Write it.
- Missing object file or library — you compiled the implementation but didn’t pass it to the linker. Add
-lmylibormyfile.oto the link command. - C/C++ mismatch — calling a C function from C++ without
extern "C". The C++ compiler mangled the name, but the C library has the unmangled version. - Template in .cpp file — move the template definition to the header or add explicit instantiations.
Multiple Definition
/usr/bin/ld: file_b.o: multiple definition of `config_value';
file_a.o: first defined here
Common causes and fixes:
- Variable defined in header — use
externdeclaration in the header and define in one.cppfile. - Duplicate source files — the same
.cppfile is compiled twice in your build system. - C++17 shared header variable — use
inline, orinline constexprfor a shared constant object.
Include guards prevent repeated inclusion within one translation unit. They do not prevent a header definition from appearing once in each of two translation units, so they cannot fix that cross-file linker conflict.
Symbol Not Found (macOS)
ld: symbol(s) not found for architecture arm64
This is macOS’s equivalent of "undefined reference". The same causes and fixes apply. Check that you’re linking the correct architecture and that the library was built for the same platform.
Further Reading
- Linkers and Loaders - John Levine’s comprehensive book on linking
- How C++ Linkers Work - Practical guide to C++ linking
- Itanium C++ ABI - The name mangling specification used by GCC and Clang
- GCC Symbol Visibility - Guide to controlling exported symbols
- GNU nm - Symbol letters, binding exceptions, and inspection options
- C++ linkage rules and constexpr rules - Internal linkage and implicit inline requirements
- C++ one-definition rule - Required definitions and the conditions for definitions across translation units
- GNU ld options - Archive search groups and shared-library
--as-neededbehavior
Related concepts
Follow a C++ function from tokens to a simplified AST, then inspect exact source ranges, declaration references and types in a keyboard-accessible explorer.
Understand the complete C++ compilation pipeline from source code to object files. Learn preprocessing, parsing, code generation, and optimization stages.
Master C++ OOP concepts including inheritance, polymorphism, virtual functions, and modern object-oriented design principles with interactive examples.
How C++ object files are linked into executables. Learn symbol resolution, static vs dynamic linking, and linker optimization.
Understand C++ pointers and references with step-by-step memory diagrams: addressing, dereferencing, aliasing, array arithmetic, and object lifetime.
C++ preprocessor visualized: macros, header guards, conditional compilation, and #include directives explained interactively.
