libstdc++, libc++, glibc and Symbol Versioning

The three C++ standard libraries, how glibc symbol versioning sets the minimum host your binary can run on, and the linking choices that decide whether your artifact is portable or pinned.

Overview

Three libraries you did not write

Every C++ binary on Linux is a negotiation between three libraries it did not write: the C++ standard library, libgcc and the C library. Most of the time the negotiation is invisible. When it is not, the cost clusters around two questions: which standard library is this, and what is the oldest host this artifact can run on.

Three standard libraries

  • libstdc++ (GNU) — g++ on Linux — libstdc++.so.6, GLIBCXX_3.4.x and CXXABI_1.3.x version nodes
  • libc++ (LLVM) — Apple platforms; elsewhere with -stdlib=libc++ — Everything in an inline namespace tied to _LIBCPP_ABI_VERSION: std::__1 under stable version 1
  • MSVC standard library — Windows — A separate world entirely

> [!NOTE] They share a standard and nothing else at the binary level. The inline namespace means an accidental mix produces link errors instead of silent layout corruption.

glibc decides portability

glibc symbols carry version nodes like GLIBC_2.x. The linker binds each reference to the default version on the machine doing the link and stamps that requirement into the binary.

  • Old binary, new glibc — Runs forever; glibc never removes old version nodes
  • New binary, old glibc — Fails hard, naming the missing version node

> [!WARNING] Everyone eventually meets GLIBC_2.34. glibc 2.34 merged libpthread, libdl, librt and libanl into libc.so.6, giving an enormous set of everyday symbols a new node in one release.

Your build host sets your floor

Your minimum supported host is determined by the machine you build on. No source change or compiler flag lowers it afterwards. Every remedy is the same idea: build against an old glibc while using a new compiler.

  • manylinux images — Python packaging; named by glibc version under PEP 600
  • Red Hat gcc-toolset — RHEL
  • --sysroot at an old distribution root — Manual, and good for cross builds

What you link statically

  • -static-libstdc++ -static-libgcc — C++ runtime travels with the binary; libc stays dynamic. The usual sweet spot — Two components in one process each carry their own C++ runtime and exchange C++ objects or throw exceptions across the boundary
  • Fully static glibc — No dependencies, in theory — NSS: glibc dlopens modules at runtime, so hostname and user lookups still need matching glibc shared objects. The linker warns about it
  • Static musl — Genuinely one file — You trade glibc behaviors; test again

> [!IMPORTANT] GCC documents the exception case: code that throws and catches across shared library boundaries must use the shared libgcc.

What interviewers probe

They describe a symptom and wait: a binary that will not start on the production host, a plugin that crashes on free, a library that works until linked next to another one.

> [!TIP] Good answers name the version node, name the ABI boundary, and say which build-time decision caused it.

Key concepts

Symbol version node
A label such as GLIBC_2.34 or GLIBCXX_3.4.33 attached to an exported symbol in a shared library. Multiple implementations of one name can coexist under different nodes, with one marked default. The linker records which node it bound to, and the dynamic linker enforces it at load. This is what allows glibc and libstdc++ to keep the same soname across decades.
Backward but not forward compatible
glibc retains every old version node forever, so a binary built in 2010 still runs today. It cannot work the other way: a binary built against glibc 2.39 names nodes that glibc 2.28 has never defined. Your build machine, not your source, sets the floor.
GLIBC_2.34
The version node introduced when glibc 2.34 merged libpthread, libdl, librt and libanl into libc.so.6. Because so many ordinary symbols moved at once, it is the node most commonly named in "version not found" failures when a binary built on a 2021-or-later distribution is run on an older one.
manylinux and sysroots
manylinux is a set of container images defined by PEP 600 and named after the glibc they target, such as manylinux_2_28 for glibc 2.28. A sysroot achieves the same thing manually: --sysroot points the compiler and linker at an old distribution root so headers and link-time libraries come from the target rather than the host.
-static-libstdc++ / -static-libgcc
Link the C++ standard library and the GCC support library into the binary while leaving libc dynamic. Removes the libstdc++ version floor. GCC warns that applications throwing and catching exceptions across shared library boundaries must instead use the shared libgcc, so this is unsafe for components that unwind across a .so edge.
The NSS trap
glibc loads Name Service Switch modules with dlopen at runtime. A fully static (-static) glibc binary therefore still requires matching glibc shared objects on the host for getaddrinfo, gethostbyname and getpwnam. The linker warns at link time. musl has no equivalent design, which is why truly self-contained binaries are usually built against musl.
Itanium C++ ABI
The cross-vendor C++ ABI that GCC and Clang implement on Linux and macOS: name mangling (_ZN3foo3barEi), vtable and RTTI layout, and the unwinding interface. It defines what a symbol name means but says nothing about class layout, which is why adding a data member or a virtual function is an ABI break with no symbol change and no diagnostic.
Version script
A linker input, passed with --version-script, that names version nodes and lists which symbols are global and which are local. The idiom { global: mylib_*; local: *; }; exports only the public prefix and hides everything else, shrinking the dynamic symbol table and preventing accidental dependence on internals.

When to use it

  • Shipping a compiled artifact to hosts you do not control, where the runtime glibc and libstdc++ versions are unknown
  • Diagnosing a binary that links and tests cleanly but refuses to start on an older production host
  • Deciding what to link statically for a CLI tool, an agent or a plugin that must drop into a foreign process
  • Publishing a shared library with an intentional, stable, documented export surface rather than every symbol that happens to be non-static
  • Evaluating Alpine or musl for container images and needing to explain what actually changes
  • Integrating a vendor binary that was built against a different standard library implementation than yours

Interview questions

  • A binary built on Ubuntu 24.04 fails on a RHEL 8 host with: ./svc: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./svc). Diagnose it and give the options.
  • When is -static-libstdc++ -static-libgcc the right call, and when does it actively break things?
  • Compare libstdc++, libc++ and the MSVC standard library. What happens if code built against two of them ends up in one program?
  • What is a fully static binary, what actually breaks, and when should you reach for musl instead?
  • How would you add symbol versioning to a shared library you own, and what does it get you?

Sign up free to read the full topic

More DevOps topics

Start free on Cariara · Coding problems · System design studio