Linkingtarget

Symbols and References

Defined, undefined, global, local, weak — five categories that decide every link outcome. And how to actually read `undefined reference to 'foo'`, which has four common causes and names none of them.

The question

What do the letters in nm output mean, and why does undefined reference appear for a function I definitely wrote?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

The program as a bipartite matching problem: a set of undefined references, a set of definitions, and a rule for pairing them. Each symbol is a name plus a binding (global, local, weak), a visibility, a section and an offset. That representation exists because the compiler could not resolve names across translation units, so it deferred the whole name-resolution problem to a phase that can see all of them at once.

What this phase may assume or do

A reference may bind to a definition only if the definition is visible at link time — global or weak binding, not local, and not hidden by visibility — and if exactly one strong definition exists for that name across all included objects. Two strong definitions is an error; a strong plus any number of weak ones resolves to the strong; weak ones alone resolve to the first encountered. A local symbol may satisfy only references within its own object file. The rule the linker cannot enforce, and which the language nevertheless requires, is that all definitions of an entity with external linkage be identical — the one-definition rule — because the linker compares names and cannot compare bodies.

Key points

  • Five categories cover every case: defined global, defined local, undefined, weak definition, weak reference.
  • nm's letters encode both binding and section — uppercase global, lowercase local, U undefined, W weak.
  • undefined reference has four common causes — missing definition, mangling mismatch, link order, hidden visibility — and the message distinguishes none of them.
  • Weak definitions are how inline functions and template instantiations end up as one copy; the linker keeps one and discards the rest without comparing them.
  • The one-definition rule is a language requirement the linker cannot enforce, so violating it links cleanly and produces a program that reads objects at the wrong offsets.
  • A symbol is a name, a binding and an address. It carries no type, so nothing checks that a caller and a definition agree about anything.

Five categories decide everything

targetThe nm letter codes are a GNU/LLVM binutils convention over ELF and Mach-O. Weak symbols are an ELF and Mach-O concept; PE/COFF has no direct equivalent and achieves comparable effects with COMDAT sections and explicit export tables. __attribute__((weak)) is a GCC and Clang extension, not standard C or C++.

Every symbol in an object file is either defined here or referenced from here, and every definition is global, local or weak. That is the whole vocabulary, and every linking outcome follows from it.

A defined global is the ordinary case: this file provides it and anyone may use it. A defined localstatic in C, an anonymous namespace in C++ — provides it only to this file, and its name may collide freely with locals in other files. An undefined symbol is a promise the linker must keep from elsewhere. A weak definition yields to any strong one and is what inline functions, template instantiations and default implementations use. And a weak reference is the unusual one: it may go unresolved, binding to zero instead, which is how optional features are detected at runtime.

nm reports all of this in a single letter per symbol, and reading those letters is the fastest way to diagnose a link problem. Uppercase means global, lowercase means local, U means undefined, W and V mean weak, and the letter itself names the section — T for text, D for data, B for bss, R for read-only.

Symbol categories and what each one does at link timetarget
Category`nm` letterComes fromLink behavior
Defined globaltargetT D B RAn ordinary non-static definitionSatisfies references anywhere; two of them for one name is an error
Defined localtargett d b rstatic, or an anonymous namespaceSatisfies references only within this object; never collides across files
UndefinedUA declaration used but not defined hereMust be satisfied by some other object or library, or the link fails
Weak definitiontargetW VInline functions, template instantiations, __attribute__((weak))Yields to any strong definition; several are permitted and one is chosen
Weak referencetargetw__attribute__((weak)) on a declarationMay go unresolved and bind to zero, which the program is expected to test for
CommonimplementationCHistorically, an uninitialised C global without externMerged with other commons of the same name — the legacy behavior that -fno-common turned off

Reading `undefined reference to 'foo'`

This message says one thing: no included object provided a definition for that exact string. It does not say why, and there are four common reasons that require four different fixes. Working through them in order takes about a minute and beats guessing.

Declared but never defined. The commonest and the most boring: a header promised a function and no .cpp implements it, or the implementing file was never added to the build. nm on every object will show U everywhere and T nowhere.

A mangling mismatch. A C++ file calls a C function through a header with no extern "C" guard, so the reference is to _Z3fooi and the definition is foo. The tell is in the message itself: a mangled name in the error and a plain one in nm -g output on the library, or the reverse. See [[name-mangling]].

Link order. With traditional static archives, the linker takes a member out of libfoo.a only to satisfy a reference it has *already seen*. Put -lfoo before the object that needs it and the archive is scanned when nothing is pending, contributes nothing, and the later reference has no definition. [[symbol-resolution-order]] covers the rule.

Not exported. The definition exists but is local — static, an anonymous namespace, -fvisibility=hidden without an export annotation, or excluded by a version script. nm shows a lowercase letter, or nm -D on the shared library shows nothing at all while nm shows it plainly.

  • Look at the shape of the name in the message: mangled means a C++ caller, plain means a C one.
  • nm -gC *.o | grep foo across every object — you are looking for a T and finding only Us.
  • For a library: nm -DC lib.so | grep foo shows what is actually exported, which is not the same as what is present.
  • Check the command line order: objects first, then archives, and repeat an archive or use --start-group if there is a cycle.
  • If everything looks right and it links but misbehaves, suspect the fifth cause — an ODR violation, which does not produce this message at all.

Weak symbols, and the rule the linker cannot check

typicalThat inline functions and template instantiations are emitted as mergeable weak definitions is typical of C++ implementations on ELF and Mach-O, using COMDAT groups or equivalent section flags; MSVC achieves the same with COMDAT sections in PE. The merge is required behavior in the sense that C++ would not work otherwise, and the failure to detect non-identical definitions is a property of every mainstream implementation.

Weak definitions exist because C++ requires several translation units to be able to contain the same inline function or the same template instantiation, and to end up with one copy in the program. The compiler emits each of them as a weak definition in its own COMDAT-style section, and the linker keeps one and discards the rest. This is invisible machinery that every C++ program depends on.

The rule it implements is the one-definition rule: an entity with external linkage may be defined many times across translation units *provided every definition is identical*. The linker cannot check that. It compares names, keeps whichever copy it saw first, and discards the others without looking at them.

So when the definitions are not identical — the same class compiled with a different -D flag, a different -std, a different packing pragma, or one build with assertions on and one off — the program links cleanly and runs one of the two bodies, chosen by link order, in all callers. Objects are constructed with one layout and read with another. This is the ODR violation, and it is the reason that "it works in debug and fails in release" is sometimes not an optimizer bug at all but two translation units disagreeing about a type.

The practical defences are all about making the definitions genuinely identical: build every translation unit with the same flags, do not let macros change class layouts, and prefer tools that detect it — the ODR checker in -flto, the sanitizer's detect_odr_violation, and gold's --detect-odr-violations.

The same class, two flag sets, one link
Before
// a.cpp compiled with -DFEATURE
struct Cfg { int a; int extra; int b; };
inline int get_b(Cfg* c) { return c->b; }   // weak: reads offset 8
After
// b.cpp compiled without -DFEATURE
struct Cfg { int a; int b; };
inline int get_b(Cfg* c) { return c->b; }   // weak: reads offset 4
// The linker keeps ONE of these two bodies and uses it in both files.
Legal only when

Merging weak definitions of an inline function is legal only under the one-definition rule: every translation unit's definition must be token-for-token identical and must mean the same thing in each. Under that condition, discarding all but one copy is unobservable and is exactly what C++ requires.

Illegal when

Whenever a macro, a compiler flag, a different -std, a packing pragma or a differently ordered include changes the definition between translation units. The linker still merges, because it compares names and not bodies, so the program silently uses one layout's accessor on the other layout's objects. No diagnostic is produced at any stage.

What a symbol is not

A symbol carries no type. Nothing in an object file records that foo takes an int and returns a double. A C program can declare void foo(void) in one file, define double foo(int) in another, link successfully, and call it with the wrong convention. C++ gets accidental protection from [[name-mangling]] — the parameter types are in the name, so a mismatch becomes an undefined reference — but that protection stops at extern "C" and at anything the mangling does not encode, such as a struct's layout.

A symbol also carries no ownership, no lifetime and no thread-safety information. It is a name and an address. Every richer contract in the program — that this pointer must be freed by this function, that this function may only be called after that one — exists in documentation and in nothing the toolchain can check.

This is the same observation [[abi]] makes from the other side, and it explains the division of labour: the linker checks that every name has exactly one definition, and nothing anywhere checks that the definition is the one the caller had in mind.

How it works

The steps, in the order the compiler takes them.

  • Each object file lists the symbols it defines, with a binding and a section and offset, and the symbols it references but does not define.
  • The linker builds a global table, adding definitions as objects are included and recording references that are not yet satisfied.
  • When an archive is encountered, it is scanned for members defining any currently-unsatisfied reference; those members are pulled in, which may add new unsatisfied references and require rescanning.
  • A strong definition replaces any weak one for the same name; two strong definitions are a hard error; several weak ones resolve to one chosen copy and the rest are discarded.
  • Local symbols are never entered into the global table and satisfy only references from their own object.
  • Any reference still unsatisfied at the end is an error, unless it is a weak reference, which is resolved to zero.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A link fails with an undefined reference to a function that is plainly defined, because the archive containing it appeared before the object that needed it on the command line.
  • A link fails with a mangled symbol name that appears nowhere in the source, because a C header was included from C++ without extern "C" guards.
  • A link fails with multiple definition because a variable was defined rather than declared extern in a header, so every including translation unit contributed one.
  • The program links and behaves differently depending on link order, because two libraries define the same symbol and the first one encountered wins silently.
  • A class is compiled with different macros in two translation units, the linker merges their inline accessors, and objects are read at the wrong offsets with no diagnostic from any stage of the build.
  • A symbol exists in a shared library and cannot be used, because it was compiled with hidden visibility — nm shows it and nm -D does not.
  • A weak reference intended as a feature check binds to zero on one platform and to a real definition on another, and the feature-detection branch takes the wrong path.

When it helps

  • Every link failure. The category of the symbol on each side of the mismatch is the diagnosis, and two nm invocations produce it.
  • Understanding why a C++ program with heavy template use links slowly: every instantiation is a weak definition in every translation unit that used it, and the linker discards most of them.
  • Designing a library boundary: knowing that hidden visibility removes a symbol from the dynamic table is what makes -fvisibility=hidden a deliberate choice rather than a magic flag.

When it hurts

  • Using weak symbols as a design mechanism in application code. It works and it makes which implementation you get depend on link order, which is a property no source file states.
  • Relying on the linker to catch interface mismatches. It matches names. Everything about types, layouts and conventions passes through it untouched.

What it costs

Every one of these is paid by something.

  • Weak definitions buy the ability to have inline functions and templates in headers at all, and cost the one-definition rule becoming an unchecked obligation whose violation is silent.
  • A flat global symbol namespace buys simple resolution and interposition, and costs collisions between unrelated libraries and the possibility that the first definition encountered silently wins.
  • Hidden visibility by default buys a small dynamic symbol table, faster loading, more inlining within the library and no accidental interposition; it costs an explicit annotation on everything intended to be public and a class of "it is right there and I cannot link to it" confusion.
  • Untyped symbols buy a linker that works for every language at once, and cost every form of interface checking, which each language must then reconstruct through its own naming scheme.

What else you could do

What a different compiler or language does instead, and when that is better.

  • A two-level namespace, as Mach-O uses: every undefined reference records which library it expects, so collisions between libraries cannot silently resolve to the wrong one. It costs the interposition that LD_PRELOAD provides.
  • Explicit export lists — PE's export table, an ELF version script, a Windows .def file — where nothing is exported unless named. Verbose and unambiguous.
  • Typed symbol tables, as JVM class files and .NET assemblies use: the descriptor sits beside the name, so a signature mismatch is a load-time error rather than a runtime corruption.
  • Module systems that resolve by structured identity rather than by flat name, as WebAssembly component imports and Rust crate resolution do, which removes the collision problem instead of managing it.

See it for yourself

The flag, dump or tool that shows you this directly.

  • What a file defines and needs: nm -gC file.o, reading the letter before each name. nm --defined-only and nm -u split the two halves.
  • What a shared library actually exports: nm -DC lib.so, or readelf --dyn-syms lib.so. This is the list that matters and it is not what nm alone shows.
  • Which definition won: -Wl,-y,foo makes the linker report every file that references or defines foo, which settles link-order arguments immediately.
  • ODR violations: build with -flto and read the warnings, run with ASAN_OPTIONS=detect_odr_violation=2, or link with gold's --detect-odr-violations.
  • On Apple platforms nm -m shows the full binding and visibility information; on Windows dumpbin /symbols and dumpbin /exports are the equivalents.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "Undefined reference means a missing library." It means no definition for that exact string was found among the included objects. A missing library is one of at least four causes.
  • "static makes a function faster." static gives it internal linkage, which keeps it out of the global symbol table and enables more aggressive inlining — the speed is a consequence of the visibility, not of the keyword.
  • "If two definitions disagree, the linker will tell me." Only if both are strong. Weak definitions — every inline function and template instantiation — are merged silently without comparison.
  • "A symbol in the library means I can call it." Only if it is in the *dynamic* symbol table. Hidden visibility leaves it visible to nm and unusable from outside.

Misconceptions

The claim, and what is actually true.

The linker checks that a caller and a definition agree.
It matches names. Types, parameter counts, struct layouts and calling conventions are all invisible to it.
Two definitions of the same function always cause an error.
Two strong ones do. Weak ones — which is what every inline function and template instantiation is — are merged silently, and if they differ the program is quietly wrong.
A weak reference that fails to resolve is a link error.
It resolves to zero by design, which is the point: it lets a program test at runtime whether an optional symbol was present.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

Each compiled file lists the names it provides and the names it needs. The linker matches them up. A name can be global (anyone may use it), local (only this file), or weak (yields to a real definition). When a name is needed and nothing provides it, you get an undefined reference — which usually means the file defining it was not in the build, or its name is spelled differently than the caller expected.

practical

For any link error, run nm -gC on the objects and nm -DC on the libraries and compare the exact strings. Check that objects come before archives on the command line. If the name in the error is mangled and the library defines a plain one, you need extern "C" guards. And if it links but behaves oddly across translation units, stop looking at the linker and start checking whether every file was compiled with the same flags — the ODR is the one rule here that fails silently.

advanced

The structural point is that the linker enforces a *cardinality* constraint on names and nothing else, and that C++ then builds a language feature on top of the deliberate weakening of that constraint. Inline functions and templates require many definitions of one entity, so the format grew weak binding and COMDAT merging, and the correctness obligation — that all those definitions be identical — moved from the toolchain to the programmer. That transfer is the origin of the ODR violation as a bug class, and it is worth recognising as a general pattern: whenever a checking mechanism is relaxed to permit a feature, the check does not disappear, it becomes an unchecked obligation. The tooling response has been to reconstruct the check elsewhere — LTO compares definitions because it has them in IR form, the sanitizers compare at load time, and modules propose to make the problem structurally impossible by not textually re-including declarations at all.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

targetWeak symbols, the nm letter codes and default-export visibility are ELF and Mach-O concepts. PE/COFF has no weak binding as such and uses COMDAT sections plus an explicit export table, and exports nothing by default — the opposite of ELF. A visibility strategy learned on one platform is a different strategy on the other.
typicalThat inline functions and template instantiations become mergeable weak definitions, and that the linker merges them without comparing bodies, is typical of every mainstream C++ implementation. The lack of comparison is what makes ODR violations silent, and no mainstream toolchain checks by default — the checks that exist are opt-in and imperfect.
implementationCommon symbols (nm letter C) were the historical C behavior for tentative definitions and allowed a variable defined in two files to be silently merged. GCC changed the default to -fno-common in version 10, turning that into a multiple definition error, which broke a number of long-standing codebases that had been relying on the old behavior without knowing it.

If you were asked this in an interview

  • What are the possible causes of undefined reference to 'foo' when foo is defined in a file that was compiled?
  • Why does C++ need weak symbols, and what obligation does that transfer to the programmer?
  • What is the difference between nm and nm -D on a shared library, and when does it matter?

Connections

Domains that do not exist yet
  • Testing & Reliability Engineering — Detecting configuration divergence across a build
    An ODR violation is a build-configuration bug that no compilation catches. Catching it requires comparing artifacts across translation units — an infrastructure and testing problem as much as a compiler one, and the general technique of cross-artifact consistency checking is owned there.