ABItarget

Name Mangling

A linker symbol table maps names to addresses and knows nothing about types, so `foo(int)` and `foo(double)` must arrive as different names. The Itanium ABI spells them `_Z3fooi` and `_Z3food`. C mangles nothing, which is the entire reason `extern "C"` exists.

The question

Why is my symbol called _ZN3foo3barEid, and why does extern "C" fix my link error?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

The program as a flat namespace of byte-string symbol names. That is all a linker sees: no types, no scopes, no namespaces, no templates — a table mapping strings to addresses. Mangling exists to answer the question that representation cannot otherwise express: how do two entities that share a source-level name but differ in type, scope or instantiation get distinct entries?

What this phase may assume or do

A mangling scheme is correct only if it is injective over every entity that can have external linkage: any two distinct declarations that could coexist in one program must produce different strings, and the same declaration compiled in two translation units must produce identical strings. That second half is what makes separate compilation work at all, and it forces the encoding to be a pure function of the declaration — it may not depend on file order, on the compiler's internal identifiers, or on anything not visible in the source declaration.

Key points

  • A linker symbol table is a flat map from byte strings to addresses, with no type information; mangling encodes the declaration into the string so that overloads become distinct entries.
  • Itanium ABI names are decodable: _Z3fooi is foo(int), and c++filt reverses it because the string contains the whole declaration.
  • Return types are not encoded for ordinary functions, because C++ does not overload on them — but they are encoded for template instantiations, which can differ only there.
  • extern "C" disables mangling and requests C linkage; it costs overloading and the accidental type checking that mangled names provide.
  • "Undefined reference" has four common causes — missing definition, mangling mismatch, link order and hidden visibility — and the message alone does not distinguish them.
  • Mangling schemes are per-toolchain, not per-language: MSVC and Clang produce different symbols for identical C++ source, which is why C++ boundaries are not portable across compilers.

The linker sees strings

targetThese are Itanium C++ ABI manglings, which Clang, GCC, ICC and most Unix C++ compilers implement on Linux, macOS and the BSDs. MSVC on Windows uses a completely different and incompatible scheme in which void foo(int) becomes ?foo@@YAXH@Z, decoded with undname rather than c++filt. Two C++ compilers on the same machine producing different symbol names for the same declaration is exactly why a C++ library boundary is not portable across toolchains and a plain C one is.

A relocatable object file contains a symbol table, and a symbol table entry is a name, a section, an offset and a few flags. There is no type. When a caller references foo and some object defines foo, the linker matches them because the bytes of the two names are equal, and it does nothing else — this is the whole of [[symbols-and-references]].

That is fine for C, where a program may contain at most one external entity called foo. It is immediately fatal for any language with overloading, namespaces, member functions, or generics instantiated per type argument, because all of those permit several distinct entities to share a source-level name. The resolution is to encode the distinguishing information into the name itself, so that the flat string namespace becomes an injective image of the structured one.

The name that comes out is therefore not a decoration. It is a serialisation of a declaration — scope, name, parameter types, qualifiers, template arguments — into a byte string, chosen so it can be decoded back. That is why c++filt can turn _ZN3foo3barEid back into foo::bar(int, double) with no other information: the string contains the declaration.

Itanium C++ ABI manglings, as emitted by Clang and GCC on Unix-likes
1void foo(int); // _Z3fooi
2void foo(double); // _Z3food
3void foo(int, double); // _Z3fooid
4void foo(const char*); // _Z3fooPKc
5namespace ns { void foo(int); } // _ZN2ns3fooEi
6
7struct S {
8 void m(int); // _ZN1S1mEi
9 void m(int) const; // _ZNK1S1mEi <- K for const
10};
11
12template <class T> void g(T);
13// g<int> instantiated: // _Z1gIiEvT_
14
15extern "C" void plain(int); // plain <- no mangling at all

Read _Z3fooi as: _Z starts a mangled name, 3foo is a length-prefixed identifier, i is int. The length prefix is what makes the grammar unambiguous without separators. PKc is pointer-to-const-char, read outside in. Note that the *return* type is absent from _Z3fooi — C++ does not overload on return type, so encoding it would be redundant. Template specialisations do encode it (vT_ above), because two instantiations can differ only in the return type.

Reading `undefined reference to` correctly

typicalThat nm -C and c++filt demangle the symbol is typical of GNU and LLVM binutils on ELF and Mach-O platforms. On Windows the equivalents are dumpbin /symbols and undname, and the mangled forms are unrelated. On Mach-O every C symbol additionally carries a leading underscore, so C foo appears as _foo in nm output and the mangled C++ names gain one too.

The single most useful skill here is reading a linker error as evidence about mangling. When the message names a mangled symbol, the caller was compiled as C++ and expects a C++ symbol. When it names a bare symbol, the caller expected a C symbol. Comparing that to what the library actually defines — nm -C will show you — identifies the mismatch immediately.

The classic case is a C++ program calling a C library whose header lacks extern "C" guards. The C++ compiler reads the declaration, mangles it, and emits a reference to _Z6strlenPKc. The C library defines strlen. Both objects are perfectly well formed and the names do not match, so the link fails with an error naming a symbol that appears nowhere in anyone's source.

The inverse also happens: a C++ function is declared extern "C" in one header and not in another, so one translation unit calls the mangled name and the other defines the unmangled one. And a subtler variant that is not about mangling at all — declaring a function and never defining it — produces the identical message, which is why the error alone does not tell you which of the four causes you have.

  • Declared but never defined: the commonest cause and the one with nothing to do with C++. A header promised a definition that no object file provides.
  • Mangling mismatch: C++ calling C without extern "C", or the two sides disagreeing about which declaration is extern "C".
  • Link order: with static archives, gcc -lfoo main.o can fail where gcc main.o -lfoo succeeds, because a traditional linker takes members out of an archive only to satisfy references it has already seen — see [[symbol-resolution-order]].
  • A definition that exists but is not exported: static in C, an anonymous namespace in C++, or -fvisibility=hidden without an explicit export attribute.
  • A one-definition-rule violation: the same class defined differently in two translation units, so a member function is mangled the same but laid out differently. This one usually links successfully and then misbehaves, which is worse.

What `extern "C"` actually turns off

The declaration extern "C" void f(int); does two things and it is worth being precise about both. It tells the compiler to emit and reference the symbol under its unmangled name, and it gives the function C language linkage, which on most platforms means the platform C calling convention. It does *not* change the language the body is written in: an extern "C" function may be defined in C++, use C++ types internally, and call C++ code.

What it costs is exactly what mangling bought. Two extern "C" functions cannot share a name, so overloads are impossible on the boundary. Templates cannot be extern "C" because each instantiation would need the same name. And because a C symbol carries no type information, nothing checks that the caller and the definition agree about the signature — the type safety that mangling accidentally provided is gone.

That accidental type safety is worth pausing on. Because a mangled name encodes the parameter types, a C++ caller whose header disagrees with the definition's types gets a link error rather than a silent mismatch. C gets no such protection: change a parameter from int to long in the definition and not in the header, and the program links and passes the wrong-width value. Mangling is a naming scheme that turns out to be a weak ABI checker, which is precisely the property [[abi]] says the system otherwise lacks.

The standard header guard, and why it is written that way
1#ifdef __cplusplus
2extern "C" {
3#endif
4
5void plain(int); /* symbol: plain — same from C and from C++ */
6
7#ifdef __cplusplus
8}
9#endif

The __cplusplus guard is required because extern "C" is not valid C syntax. This block is what turns a header into one that both languages can include and agree about, and its absence is the direct cause of the most common C++-to-C link failure. Note that extern "C" applies to declarations, so the definition must see the same declaration — including it in the defining file is the usual way to guarantee that.

Every language with generics faces this

Mangling is not a C++ oddity; it is what any language needs the moment two distinct entities can share a name. Rust mangles because it has modules, traits and generics, and its scheme includes a hash of the crate identity and version so that two versions of the same crate in one binary produce distinct symbols — a deliberate design response to the diamond-dependency problem that C++ does not solve. Swift mangles for the same reasons plus protocol conformances.

The interesting contrast is with languages that avoid the problem. Go does not overload and its symbols are essentially package-qualified names, so its mangling is nearly trivial. Java has overloading but resolves it inside the class file format, which stores a full descriptor next to each name rather than encoding it into the name — the JVM's "symbol table" is structured, so it does not need the trick at all.

That last point is the general lesson. Mangling is a workaround for a flat, untyped symbol table. A linker that understood types would need none of it. Nobody has replaced the flat symbol table because every language and every tool on every platform depends on it, which is a fair description of most of what is in [[toolchains]].

How five languages get distinct linker symbolsimplementation
LanguageSchemeWhat forces it
CNone — the identifier is the symbolNo overloading, no namespaces, so no collisions to resolve
C++ (Itanium ABI)target_Z + length-prefixed scopes + encoded parameter typesOverloading, namespaces, member functions, templates
C++ (MSVC)target?name@@ + a different type encoding, decoded by undnameThe same requirements, an independent and incompatible answer
Rustimplementation_R-prefixed v0 scheme including crate name and a disambiguating hashModules, traits, generics, and two versions of one crate coexisting
GoimplementationPackage-qualified names, essentially unmangledNo overloading; generics are handled by shape-based instantiation inside the compiler
JavaspecNo name mangling; the class file stores a type descriptor beside each nameA structured symbol table makes the whole technique unnecessary

How it works

The steps, in the order the compiler takes them.

  • The compiler determines whether a declaration has external linkage; internal-linkage entities need no stable name and often get none.
  • For a mangled language it serialises the enclosing scopes, the identifier, the qualifiers and the parameter types into a byte string according to the ABI's grammar, with length prefixes making the result parseable without separators.
  • Template instantiations additionally encode the template arguments and the return type, since two instantiations may otherwise collide.
  • The resulting string is written into the object file's symbol table as a definition or an undefined reference.
  • The linker matches undefined references to definitions by byte equality of these strings and knows nothing about what they encode.
  • A demangler parses the string back into a declaration, which is how debuggers, profilers and error messages present readable names.

How it breaks

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

  • A link fails with undefined reference to '_Z6strlenPKc' — a mangled name for a function that exists unmangled — because a C header was included without extern "C" guards.
  • A link succeeds and the program crashes on a call, because a function was declared extern "C" on one side and not the other in a way that happened to produce matching names but different conventions.
  • A profiler or a crash report shows unreadable symbols, and the stack is uninterpretable until it is piped through the right demangler for the right toolchain.
  • Two translation units define the same class differently — a header included with different macros defined — so member functions mangle identically and have different layouts. It links, and objects are read at the wrong offsets.
  • A C library adds a parameter and only the definition is updated, and because C symbols carry no type information the program links and passes garbage. The same mistake in C++ would have been a link error.
  • A symbol exists in the library but is hidden by -fvisibility=hidden, and the link error names a function that nm shows is plainly there — because nm -D shows it is not dynamic.

When it helps

  • Diagnosing link errors in seconds rather than minutes: the shape of the symbol in the message tells you which side expected which language.
  • Reading profiles, core dumps and crash reports from native code, where every frame is a mangled name until you demangle it.
  • Designing a library boundary: knowing that mangled names are toolchain-specific is what makes the case for a plain C interface concrete rather than superstitious.

When it hurts

  • Relying on a mangled name as a stable identifier. It encodes the signature, so any signature change — including one that is source-compatible, such as adding a defaulted parameter — changes the symbol and breaks every existing caller.
  • Assuming a demangled name proves compatibility. Two identically demangled symbols from different toolchains still have different mangled forms and different object models.

What it costs

Every one of these is paid by something.

  • Encoding types into symbol names buys overloading, namespaces and per-instantiation generics on a flat symbol table, and costs symbol-table size — template-heavy C++ binaries routinely spend a large fraction of their symbol table on names hundreds of characters long — plus unreadable tool output until something demangles them.
  • A mangling that encodes the full signature buys a weak link-time type check, and costs binary compatibility on any signature change at all, including ones the language considers source-compatible.
  • Rust's inclusion of a crate hash buys the ability to link two versions of one crate into a single binary, and costs symbol stability across compiler and dependency versions, which is one reason Rust deliberately declines to offer a stable ABI.
  • extern "C" buys interoperability and a name that does not change, and costs overloading, templates and the type checking the mangled name was providing.

What else you could do

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

  • No mangling at all, as C manages, by forbidding the language features that create collisions. Simple, and it moves the type-mismatch failure from link time to runtime.
  • A structured symbol table, as the JVM class file and .NET metadata use: store a type descriptor beside the name instead of inside it. Cleaner in every respect and only available if you also control the linker and the loader.
  • Hash-based symbols, used by some build systems and by Rust's legacy scheme: hash the declaration and use the digest. Short, collision-resistant, and not decodable, so every tool needs a side table to say anything readable.
  • Explicit export lists — a .def file on Windows, a version script on ELF — which control which names are exported and can alias them to stable spellings. Verbose, and it is how a library keeps a stable exported surface while its internals move.

See it for yourself

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

  • See the symbols: nm -g file.o lists them, nm -gC file.o demangles with the GNU scheme, and nm -D lib.so shows the dynamic symbols — the ones that actually matter for a shared library.
  • Demangle one by hand: c++filt _ZN2ns3fooEi on Unix-likes, undname ?foo@@YAXH@Z for MSVC symbols.
  • See a mangled name at the source: clang -S -o - file.cpp and look at the .globl directives. The name in the assembly is the name in the object file.
  • See what a shared library actually exports: readelf --dyn-syms lib.so on ELF, objdump -p for the export table on PE, nm -gU lib.dylib on Mach-O.
  • Read the grammar: the Itanium C++ ABI document specifies the mangling as a formal grammar, and it is the authority for every non-MSVC C++ compiler.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "Mangled names are compiler-internal gibberish." They are a specified, decodable serialisation of a declaration. Anything that can read the grammar can reconstruct the signature exactly.
  • "extern "C" makes the function C code." It changes the symbol name and the linkage convention. The body is still C++ and may use anything C++ offers.
  • "If the demangled names match, the symbols match." They match only if the mangled strings match. Different toolchains produce different manglings for identical declarations.
  • "Undefined reference means the library is missing." It means no object provided that exact string. A missing definition, a mangling mismatch, a hidden symbol and a bad link order all produce the same message.

Misconceptions

The claim, and what is actually true.

Name mangling exists to obfuscate symbols.
It exists because the linker's namespace is flat and untyped. The encoding is fully documented and reversible, which is the opposite of obfuscation.
C++ mangles the return type into the name.
Not for ordinary functions — C++ does not overload on return type, so it would add nothing. Template instantiations do encode it, because two of those can differ only there.
Two C++ compilers on the same platform produce compatible object files.
Only if they implement the same C++ ABI. Clang and GCC do on Unix-likes; MSVC does not, and the symbol names alone make the objects unlinkable.

Go deeper

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

overview

A linker matches functions by name and knows nothing about types, so a language that lets two functions share a name has to make the names different before the linker sees them. C++ encodes the scope and the parameter types into the symbol, which is why foo(int) becomes _Z3fooi. C has no overloading and therefore needs none of this, and extern "C" is how you ask for a C-style name from C++.

practical

When a link fails, look at the shape of the symbol in the message. Mangled means a C++ caller; bare means a C caller. Then run nm -gC on the library and see which shape it defines. If the two disagree, you are missing extern "C" guards in a header. If they agree, the symbol is genuinely absent, hidden by visibility settings, or in an archive that came before the object that needed it on the command line.

advanced

The property that makes mangling load-bearing beyond overloading is injectivity across translation units combined with determinism. Every compiler compiling the same declaration must produce byte-identical output, which forces the encoding to be a pure function of the declaration — no counters, no file paths, no hash-map ordering. That constraint recurs throughout the toolchain and is the same one [[reproducible-compilation]] is about. Rust took the constraint further by folding crate identity and version into the symbol, converting what in C++ is a one-definition-rule violation — two incompatible definitions of one name, linking successfully and misbehaving — into a link-time distinction. The cost is that no Rust symbol is stable across compiler versions, which is a deliberate choice to make [[abi-stability]] someone's explicit opt-in rather than an accidental promise.

How much this depends on

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

targetAll the manglings shown are the Itanium C++ ABI, used by Clang and GCC on Linux, macOS and BSD. MSVC uses an unrelated scheme on Windows, so void foo(int) is _Z3fooi on one and ?foo@@YAXH@Z on the other. Mach-O additionally prefixes every C symbol with an underscore, so what nm prints differs from what the source says even for unmangled names.
specThe C++ standard requires that overloaded functions be distinguishable and specifies no mechanism. Mangling is the implementation technique every C++ ABI chose; the standard mentions only "language linkage" and extern "C". Java, by contrast, specifies its descriptor format in the class file specification, so its equivalent is normative rather than conventional.
implementationRust's v0 mangling including a crate disambiguator hash is a property of current rustc and is deliberately not stabilised; the legacy scheme it replaced was different again. Go's symbol naming is likewise an implementation detail of the gc toolchain and differs in gccgo. Neither should be relied on across versions.

If you were asked this in an interview

  • Why does foo(int) need a different linker symbol from foo(double), and what would break if it did not?
  • A C++ file fails to link against a C library with an undefined reference to a mangled name. What is wrong and what is the fix?
  • What does extern "C" disable, and what do you lose by using it?

Connections

Domains that do not exist yet
  • Programming Languages & Runtime Internals — Dynamic symbol lookup and reflection over names at runtime
    A runtime that resolves methods by name at execution time — the JVM, .NET, Objective-C — needs the structured symbol table this lesson says a native linker lacks. How that lookup is implemented and cached is owned there; why native toolchains cannot do it is here.