Building and linking: static and shared libraries

Read a symbol table, build archives and shared objects, get link order right, and debug the undefined reference errors that follow.

Object files and symbols

Compiling (-c) and linking are separate steps. A link error means the compiler was satisfied but the linker could not resolve a name; reading the symbol table tells you which side is missing.

cc -std=c17 -Wall -Wextra -c mathx.c -o mathx.o

nm mathx.o            # T = defined text, U = undefined, D = data
nm -C mathx.o         # demangle C++ names
objdump -d mathx.o    # disassembly
readelf -h mathx.o    # ELF header, architecture and type

# why a symbol is missing
nm -u main.o          # everything main.o still needs
Symbol letterMeaningLinker view
T / tText (code), global / localProvides the symbol
D / BInitialised / uninitialised dataProvides the symbol
UUndefinedMust be satisfied by another object or library
RRead-only dataString literals and const tables
WWeak symbolOverridable by a strong definition

Archives and shared objects

# static library: an archive of object files, copied into the final binary
cc -c src/a.c src/b.c
ar rcs libmylib.a a.o b.o
ranlib libmylib.a          # build the index (rcs already does this)
cc main.c -L. -lmylib -o app_static

# shared library: position-independent code, loaded at run time
cc -fPIC -c src/a.c src/b.c
cc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 a.o b.o
ln -sf libmylib.so.1.0.0 libmylib.so.1
ln -sf libmylib.so.1     libmylib.so
cc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN' -o app_shared

ldd app_shared             # which shared libraries it will load
nm -D --defined-only libmylib.so.1.0.0
  • -fPIC is mandatory for shared objects; forgetting it gives a relocation error at link time.
  • -lfoo means libfoo.so then libfoo.a, searched in each -L directory. Put libraries after the objects that need them.
  • ldconfig and /etc/ld.so.conf.d control the system search path; -Wl,-rpath bakes a path into the binary instead.
  • --whole-archive forces every object out of a static archive, useful for registration-based designs.

Link order is the most common cause of an undefined reference. The linker resolves left to right and does not revisit a library it has already passed, so a library that depends on another must come first.

pkg-config and diagnosing link failures

pkg-config --cflags --libs libcurl     # -> -I... -lcurl
cc main.c $(pkg-config --cflags --libs libcurl) -o app

# typical failures and their real meaning
# undefined reference to 'sqrt'        -> add -lm
# cannot find -lmylib                  -> -L path wrong or library not built
# relocation R_X86_64_PC32 ... recompile with -fPIC
# duplicate symbol                     -> two strong definitions, often a header definition
# version GLIBC_2.34 not found         -> binary built on a newer distribution
💡
A static library only pulls in the object files that are actually referenced. If your library registers callbacks through a file nothing calls directly, the linker discards it — link that object explicitly or use --whole-archive.

FAQ

Static or shared?
Static for self-contained tools and predictable deployment; shared to save disk and memory across many processes and to ship security fixes without relinking. Shared also means you must manage symbol visibility and ABI compatibility.
What does -fvisibility=hidden do?
It makes every symbol private unless explicitly exported with __attribute__((visibility("default"))). This shrinks the dynamic symbol table, speeds loading and prevents accidental collisions between libraries.

Preprocessor, headers and multi-file projects Debugging with gdb, sanitizers and valgrind

Last refreshed 2026-09-18.