Back to Technology

GNU Make Mastery Part 8: Compilation Workflow & Libraries

February 24, 2026 Wasil Zafar 22 min read

Extend your Makefile to produce static archives (.a) and shared objects (.so), apply -fPIC correctly, manage SONAME versioning, and control link order to avoid the subtle errors that bite large multi-library C projects.

Table of Contents

  1. Library Types Overview
  2. Static Libraries
  3. Shared Libraries
  4. Link Order
  5. macOS Dylibs
  6. Complete Example
  7. Next Steps

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.

Static vs Shared

Comparison
AspectStatic (.a)Shared (.so)
Link timeCode copied into binaryOnly symbol table linked
Binary sizeLarger (contains lib code)Smaller
Runtime depsNoneLibrary must be present on target
UpdatesMust relink to update libDrop-in replacement
Compilation flagNormal -cRequires -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.

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

Next in the Series

In Part 9: Project Architecture & Multi-Directory Builds, we scale to large codebases using recursive $(MAKE) -C sub-makes, non-recursive include patterns, and disciplined out-of-source build layouts.

Technology