Linking, libraries and the ABI

C++ · Expert · 18 min

Shipping libraries, debugging link errors and upgrading compilers safely all require understanding symbols and the ABI.

Each object file has a symbol table: the functions and variables it defines and those it needs. The linker matches them up. Linkage decides visibility: external (shared across translation units, the default for functions and globals), internal (static or an unnamed namespace, private to one file) or none (locals).

Overloading and namespaces mean C++ names can't be used as raw symbols, so compilers mangle them: geo::area(double) becomes _ZN3geo4areaEd under the Itanium C++ ABI used on Linux and macOS (MSVC has its own scheme). nm -C and c++filt demangle. extern "C" turns mangling off, giving a plain C symbol; use it for C interop and for stable plugin entry points.

Libraries come in two kinds. A static library (.a, .lib) is an archive of object files copied into the executable at link time. With GNU ld, order matters: list libraries after the objects that use them. A shared library (.so, .dylib, .dll) is loaded at run time and shared between processes. That makes its ABI, the binary interface, a contract.

The ABI covers calling conventions, name mangling, object layout, vtable layout and exception handling. Changing a public class's data members, adding a virtual function, or changing an inline function or template in a header breaks the ABI: existing binaries silently use the old layout. Even standard library types change, as with libstdc++'s std::string switch, controlled by _GLIBCXX_USE_CXX11_ABI. Mixing code built against different standard libraries or with incompatible flags causes link errors or worse.

To keep a stable ABI, hide data behind the pimpl idiom (class Widget { struct Impl; std::unique_ptr<Impl> p_; };), so the layout lives in the .cpp; or expose a C API. On ELF platforms, build with -fvisibility=hidden and export only the public symbols.

Example

# lib.cpp:  namespace geo { double area(double r) { return 3.14159 * r * r; } }
#           extern "C" double geo_area(double r) { return geo::area(r); }
g++ -std=c++20 -O2 -fPIC -c lib.cpp -o lib.o
nm lib.o
#   T _ZN3geo4areaEd        mangled C++ name (Itanium ABI)
#   T geo_area              extern "C": plain, unmangled
nm -C lib.o                 # demangled: geo::area(double)
echo _ZN3geo4areaEd | c++filt

# static library: archive of objects, copied into the executable
ar rcs libgeo.a lib.o
g++ main.o -L. -lgeo -o app            # libraries AFTER the objects that need them

# shared library: loaded at run time, its ABI is now a contract
g++ -shared -o libgeo.so lib.o
g++ main.o -L. -lgeo -Wl,-rpath,'$ORIGIN' -o app
ldd app | grep geo

# undefined reference to `geo::area(double)'  -> definition not linked, or wrong order
# multiple definition of `helper()'           -> non-inline function defined in a header

Remember

  • C++ names are mangled into symbols; extern "C" gives plain C symbols
  • Static libraries are copied in at link time; shared libraries make the ABI a contract
  • Pimpl or a C API keeps a library's ABI stable across changes

Watch out for

  • Adding a data member or virtual function to a class exported from a shared library without rebuilding all its users.

Check yourself

  1. A shared library adds a private data member to a public class whose definition is in its header. Existing apps aren't rebuilt. What's the risk?
  2. Why is `extern "C"` used for plugin entry points like `create_plugin()`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/storage_duration