Library Types Overview
Part 8 of 16 — GNU Make Mastery Series. We now have a working incremental build with automatic dependency tracking. This part extends it to produce reusable libraries — the building blocks of every serious C/C++ project.
1
Build Systems Foundations
Why Make, compilation pipeline, basics
2
Targets, Prerequisites & Execution
Rule anatomy, PHONY, build workflows
3
Variables, Expansion & Scope
= vs :=, ?=, +=, CLI overrides
4
Automatic Variables & Pattern Rules
$@, $<, $^, $?, %.o: %.c patterns
5
Built-in Functions & Make Language
subst, wildcard, foreach, $(shell)
6
Conditionals & Configurable Builds
ifeq, ifdef, platform detection, flags
7
Automatic Dependency Generation
-M, -MM, -MD, .d files, header deps
8
Compilation Workflow & Libraries
Static/shared libs, ar, -fPIC, SONAME
You Are Here
9
Project Architecture & Multi-Directory
Recursive make, include, out-of-source
10
Cross-Compilation & Toolchains
Toolchain prefixes, sysroots, embedded
11
Parallel Builds & Performance
make -j, jobserver, race conditions
12
Testing, Coverage & Debug Tooling
Test targets, gcov/lcov, sanitizers
13
Make as Automation & DevOps Tool
Task runner, Docker, install, packaging
14
CI/CD Integration
Deterministic builds, GitHub Actions, cache
15
Advanced Make & Debugging
--debug, dynamic rules, evaluation traps
16
Ecosystem, Alternatives & Mastery
CMake, Ninja, Meson, Bazel, production
Static vs Shared
Comparison
| Aspect | Static (.a) | Shared (.so) |
| Link time | Code copied into binary | Only symbol table linked |
| Binary size | Larger (contains lib code) | Smaller |
| Runtime deps | None | Library must be present on target |
| Updates | Must relink to update lib | Drop-in replacement |
| Compilation flag | Normal -c | Requires -fPIC |
Sample Source Files
Self-contained examples: Library-workflow examples build a libcrypto static and shared library from crypto.c, then link it into main.c. These stubs compile cleanly on any POSIX system.
crypto.h
/* crypto.h — public API for the crypto library */
#ifndef CRYPTO_H
#define CRYPTO_H
/* Simple XOR-based hash (demo only — not cryptographically secure) */
void crypto_hash(const char *input, char *output, int outlen);
int crypto_verify(const char *a, const char *b);
#endif /* CRYPTO_H */
crypto.c
/* crypto.c — crypto library implementation */
#include <string.h>
#include <stdio.h>
#include "crypto.h"
void crypto_hash(const char *input, char *output, int outlen) {
int i;
for (i = 0; i < outlen - 1 && input[i]; i++)
output[i] = (char)(input[i] ^ 0x5A); /* trivial XOR */
output[i] = '\0';
}
int crypto_verify(const char *a, const char *b) {
return strcmp(a, b) == 0;
}
main.c
/* main.c — uses the crypto library */
#include <stdio.h>
#include "crypto.h"
int main(void) {
char hash[64];
crypto_hash("hello world", hash, sizeof(hash));
printf("Hash: %s\n", hash);
printf("Verify: %s\n", crypto_verify(hash, hash) ? "OK" : "FAIL");
return 0;
}
Static Libraries
The ar Archive Tool
A static library is an archive of .o files. The ar command manages these archives.
# ar rcs syntax:
# r = insert/replace members
# c = create archive if it doesn't exist
# s = write an index (equivalent to ranlib)
ar rcs libmath.a add.o sub.o mul.o div.o
Static Library Makefile
CC := gcc
CFLAGS := -Wall -Wextra -std=c11 -O2
AR := ar # archive tool (creates static libraries)
ARFLAGS := rcs # r=replace, c=create, s=write index
LIBDIR := lib
SRCDIR := src
BUILDDIR := build
LIB_SRCS := $(wildcard $(SRCDIR)/math/*.c) # find all .c under src/math/
LIB_OBJS := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(LIB_SRCS)) # src/math/foo.c → build/math/foo.o
STATIC_LIB := $(LIBDIR)/libmath.a # output archive name
.PHONY: all clean
all: $(STATIC_LIB)
# ── Create static library from object files ──
$(STATIC_LIB): $(LIB_OBJS)
@mkdir -p $(LIBDIR)
$(AR) $(ARFLAGS) $@ $^ # $@ = libmath.a, $^ = all .o files
# ── Compile ──
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(dir $@) # $(dir $@) = directory part of target path
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(LIB_OBJS:.o=.d) # pull in auto-generated dependency files
clean:
rm -rf $(BUILDDIR) $(LIBDIR)
# Link app against static lib:
gcc -Wall main.c -Llib -lmath -Iinclude -o myapp
# The linker pulls only the .o modules it actually needs from libmath.a
Shared Libraries
-fPIC — Position Independent Code
Shared libraries may be mapped to different virtual addresses in each process. -fPIC generates code that uses a Global Offset Table (GOT) for data references, making it address-independent. Forgetting -fPIC causes a link error when building .so on most Linux targets:
# Compile with -fPIC for shared library objects:
gcc -Wall -O2 -fPIC -c src/crypto.c -o build/crypto.o
# Create the shared library:
gcc -shared -Wl,-soname,libcrypto.so.1 \
-o lib/libcrypto.so.1.0.0 build/crypto.o
SONAME Versioning
Linux shared library versioning uses three components embedded via the linker:
# Real name: libcrypto.so.1.0.0 (full version, what's on disk)
# SONAME: libcrypto.so.1 (ABI version, embedded in the .so)
# Linker name: libcrypto.so (used during -lcrypto lookup at build time)
# Create symlinks (ldconfig does this automatically on install):
ln -sf libcrypto.so.1.0.0 lib/libcrypto.so.1 # SONAME symlink
ln -sf libcrypto.so.1 lib/libcrypto.so # linker name symlink
# Read the SONAME embedded in an existing .so:
readelf -d lib/libcrypto.so.1.0.0 | grep SONAME
Shared Library Makefile
CC := gcc
CFLAGS := -Wall -Wextra -std=c11 -O2 -fPIC # -fPIC = position-independent code (required for .so)
LDFLAGS := -shared # tell linker to produce a shared library
# ── Versioning ──
LIBNAME := myutil
LIBVER := 1.0.0 # full version (real name on disk)
SONAME := lib$(LIBNAME).so.1 # ABI version (embedded in the .so)
REALNAME := lib$(LIBNAME).so.$(LIBVER) # libmyutil.so.1.0.0
LINKNAME := lib$(LIBNAME).so # linker name (used by -lmyutil)
LIBDIR := lib
BUILDDIR := build
SRCDIR := src
LIB_SRCS := $(wildcard $(SRCDIR)/$(LIBNAME)/*.c)
LIB_OBJS := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(LIB_SRCS))
.PHONY: all clean
all: $(LIBDIR)/$(REALNAME) symlinks
# ── Build the shared library with embedded SONAME ──
$(LIBDIR)/$(REALNAME): $(LIB_OBJS)
@mkdir -p $(LIBDIR)
$(CC) $(LDFLAGS) -Wl,-soname,$(SONAME) -o $@ $^ # -Wl passes option to linker
# ── Create SONAME and linker symlinks ──
symlinks: $(LIBDIR)/$(REALNAME)
cd $(LIBDIR) && ln -sf $(REALNAME) $(SONAME) && ln -sf $(SONAME) $(LINKNAME)
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(LIB_OBJS:.o=.d)
clean:
rm -rf $(BUILDDIR) $(LIBDIR)
# Link app against the shared library:
gcc main.c -Llib -Wl,-rpath,lib -lmyutil -Iinclude -o myapp
# -Wl,-rpath embeds the library search path into the binary for development.
# In production use ldconfig or LD_LIBRARY_PATH.
Link Order
Critical gotcha: The GNU linker resolves symbols left-to-right. If library A depends on library B, you must list A before B on the command line. Listing them in the wrong order produces "undefined reference" linker errors.
# WRONG — libapp.a uses symbols from libcore.a, but libcore.a listed first:
gcc main.c -lcore -lapp -o myapp
# => undefined reference to `app_function`
# CORRECT — list libraries in dependency order (dependers first):
gcc main.c -lapp -lcore -o myapp
# In your Makefile, define LIBS in dependency order:
LIBS := -lapp -lcore -lplatform -lpthread -lm
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^ -L$(LIBDIR) $(LIBS)
For circular dependencies between archives, use the --start-group / --end-group linker options (or the shorthand -( / -)):
# Linker rescans grouped archives until all references are resolved:
gcc main.c -Wl,--start-group -la -lb -lc -Wl,--end-group -o myapp
# Note: this is slower; fix circular deps if possible
macOS Dylibs
On macOS, shared libraries use the .dylib extension and the install_name mechanism instead of SONAME:
# Build a .dylib on macOS:
clang -dynamiclib \
-install_name @rpath/libmyutil.dylib \
-o lib/libmyutil.dylib build/*.o
# Fix or inspect rpath/install_name:
otool -L lib/libmyutil.dylib
install_name_tool -change old_path new_path myapp
# Platform-conditional library extension in Makefile:
UNAME := $(shell uname -s)
ifeq ($(UNAME),Darwin)
SHARED_EXT := dylib
SHARED_FLAGS := -dynamiclib -install_name @rpath/lib$(LIBNAME).$(SHARED_EXT)
else
SHARED_EXT := so
SHARED_FLAGS := -shared -Wl,-soname,lib$(LIBNAME).$(SHARED_EXT).$(MAJOR)
endif
Complete Example: App + Static + Shared
CC := gcc
CFLAGS := -Wall -Wextra -std=c11 -O2
CFLAGS_PIC := $(CFLAGS) -fPIC # PIC variant for shared library objects
BUILDDIR := build
LIBDIR := lib
# ── Static library (src/math/*.c → libmath.a) ──
STATIC_SRCS := $(wildcard src/math/*.c)
STATIC_OBJS := $(patsubst src/%.c,$(BUILDDIR)/%.o,$(STATIC_SRCS))
STATIC_LIB := $(LIBDIR)/libmath.a
# ── Shared library (src/io/*.c → libio.so) ──
SHARED_SRCS := $(wildcard src/io/*.c)
SHARED_OBJS := $(patsubst src/%.c,$(BUILDDIR)/pic/%.o,$(SHARED_SRCS)) # separate dir for PIC
SHARED_LIB := $(LIBDIR)/libio.so
# ── Application ──
APP_SRCS := main.c
APP_OBJS := $(BUILDDIR)/main.o
TARGET := myapp
.PHONY: all clean
all: $(TARGET)
# ── Create static library (archive of .o files) ──
$(STATIC_LIB): $(STATIC_OBJS)
@mkdir -p $(LIBDIR)
ar rcs $@ $^ # r=replace, c=create, s=write index
# ── Create shared library (PIC objects linked with -shared) ──
$(SHARED_LIB): $(SHARED_OBJS)
@mkdir -p $(LIBDIR)
$(CC) -shared -Wl,-soname,libio.so.1 -o $@ $^ # embed SONAME for ABI versioning
# ── Compile for static lib (no -fPIC needed) ──
$(BUILDDIR)/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# ── Compile for shared lib (with -fPIC) ──
$(BUILDDIR)/pic/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS_PIC) -MMD -MP -c $< -o $@
# ── Link application against both libraries ──
$(TARGET): $(APP_OBJS) $(STATIC_LIB) $(SHARED_LIB)
$(CC) $(LDFLAGS) -o $@ $(APP_OBJS) \
-L$(LIBDIR) -Wl,-rpath,$(LIBDIR) \ # -rpath embeds lib search path
-lio -lmath # order matters: dependers first
-include $(shell find $(BUILDDIR) -name '*.d' 2>/dev/null) # include all .d files
clean:
rm -rf $(BUILDDIR) $(LIBDIR) $(TARGET)
# ── Variable Inspector ──────────────────────────────────────
.PHONY: info
info:
@echo "CC = $(CC)"
@echo "CFLAGS = $(CFLAGS)"
@echo "CFLAGS_PIC = $(CFLAGS_PIC)"
@echo "BUILDDIR = $(BUILDDIR)"
@echo "LIBDIR = $(LIBDIR)"
@echo "TARGET = $(TARGET)"
@echo "STATIC_SRCS = $(STATIC_SRCS)"
@echo "STATIC_OBJS = $(STATIC_OBJS)"
@echo "STATIC_LIB = $(STATIC_LIB)"
@echo "SHARED_SRCS = $(SHARED_SRCS)"
@echo "SHARED_OBJS = $(SHARED_OBJS)"
@echo "SHARED_LIB = $(SHARED_LIB)"
@echo "APP_SRCS = $(APP_SRCS)"
@echo "APP_OBJS = $(APP_OBJS)"
Create Source Files
The Makefile expects src/math/*.c, src/io/*.c, and main.c. Create them so every target resolves:
mkdir -p src/math src/io
cat > src/math/math_ops.h << 'EOF'
#ifndef MATH_OPS_H
#define MATH_OPS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
EOF
cat > src/math/math_ops.c << 'EOF'
#include "math_ops.h"
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
EOF
cat > src/io/io_ops.h << 'EOF'
#ifndef IO_OPS_H
#define IO_OPS_H
void io_print(const char *msg);
#endif
EOF
cat > src/io/io_ops.c << 'EOF'
#include "io_ops.h"
#include <stdio.h>
void io_print(const char *msg) { printf("[IO] %s\n", msg); }
EOF
cat > main.c << 'EOF'
#include <stdio.h>
#include "src/math/math_ops.h"
#include "src/io/io_ops.h"
int main(void) {
printf("3 + 4 = %d\n", add(3, 4));
io_print("Hello from shared lib!");
return 0;
}
EOF
Try It
# Inspect all variables
make info
# Build static lib + shared lib + app
make
# Verify the outputs
ls lib/ # libmath.a libio.so
file myapp # ELF executable
LD_LIBRARY_PATH=lib ./myapp
# Incremental: touch a math source → only static lib rebuilds
touch src/math/math_ops.c
make
make clean
Continue the Series
Part 9: Project Architecture & Multi-Directory
Recursive $(MAKE) -C, non-recursive include, and structured out-of-source builds for large C projects.
Read Article
Part 7: Automatic Dependency Generation
Use GCC -MMD -MP to generate .d files so header changes always trigger the right recompilations.
Read Article
Part 10: Cross-Compilation & Toolchains
Set CROSS_COMPILE prefixes, sysroot flags, and toolchain variables to build for embedded ARM targets from an x86 host.
Read Article