Shared libraries are the modern default for good reason: they save disk space, allow all programs to share one copy in memory, and allow library bug fixes to benefit all programs at once (by just replacing the .so file). But there are genuine situations where static linking is the right choice.
This section explains when static libraries are appropriate and shows you the three ways to force the linker to use them.
| Property | Static Library (.a) | Shared Library (.so) |
|---|---|---|
| Code location at run time | Copied into the executable | Remains in a separate .so file |
| Executable size | Larger | Smaller |
| Memory usage (multiple programs) | Each process has its own copy | Single shared copy in RAM |
| Run without shared lib installed | Yes — everything is inside the binary | No — .so must be present |
| Library update benefits running programs | No — must relink | Yes — replace .so, all programs benefit |
| Immune to library ABI changes | Yes — uses exact version it was linked with | No — may break on incompatible .so upgrade |
| Works in chroot jail / minimal environment | Yes | Only if .so files are copied in too |
| Dynamic loading / plugins | Not possible | Yes (dlopen) |
The target machine may not have the right shared libraries installed, and you can’t install them (e.g., delivering a binary to a customer’s server).
chroot jails, containers without /lib, embedded systems with read-only filesystems, or rescue shells where shared libs aren’t available.
You need the exact library version you tested with. A compatible shared library upgrade might silently introduce a bug. Statically linked binaries are immune.
You want a self-contained single binary that users can just download and run — no installer, no dependency management (think busybox, Go binaries).
/* math_ops.c — source for our static library */
#include "math_ops.h"
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int subtract(int a, int b) { return a - b; }
/* math_ops.h */
int add(int a, int b);
int multiply(int a, int b);
int subtract(int a, int b);
# Step 1: Compile to object file (NO -fPIC needed for static libs)
gcc -c -Wall math_ops.c -o math_ops.o
# Step 2: Create the static library archive (.a file)
ar rcs libmath_ops.a math_ops.o
# ar options: r=insert, c=create, s=add index (for faster linking)
# Check what's inside the archive
ar t libmath_ops.a
# Output: math_ops.o
# Step 3: Link the application against the static library
gcc -o myapp main.c -L. -lmath_ops
# or explicitly: gcc -o myapp main.c ./libmath_ops.a
# Verify: no runtime dependency on libmath_ops
ldd myapp
# libmath_ops does NOT appear — its code is inside myapp itself
/* main.c */
#include <stdio.h>
#include "math_ops.h"
int main(void) {
printf("3 + 4 = %d\n", add(3, 4));
printf("3 * 4 = %d\n", multiply(3, 4));
printf("7 - 2 = %d\n", subtract(7, 2));
return 0;
}
If both libdemo.so and libdemo.a exist in the same directory and you link with -ldemo, the linker automatically prefers the shared version. You have to explicitly request static.
# Both exist: libdemo.so AND libdemo.a
ls ./libs/
# libdemo.a libdemo.so
# Default: shared library is used
gcc -o prog main.c -L./libs -ldemo
ldd prog
# libdemo.so => ./libs/libdemo.so <-- shared is chosen
# Check file size difference
ls -lh prog # smaller (shared)
Method 1: Give the explicit .a file path
# Specify the .a file directly — no ambiguity
gcc -o prog main.c ./libs/libdemo.a
# Can also use absolute path
gcc -o prog main.c /usr/local/lib/libdemo.a
Method 2: -static flag (link EVERYTHING statically)
# Link everything statically, including libc
gcc -o prog main.c -static -L./libs -ldemo
# The resulting binary is completely self-contained
# but VERY large (includes libc, libm, etc.)
ldd prog
# not a dynamic executable <-- fully static!
# Check size — much larger
ls -lh prog
# Typical: 800KB+ vs 8KB for dynamic version
-static links everything statically, including glibc. This makes very large binaries. Some Linux features (like NSS for hostname resolution) don’t work well with a statically linked glibc.Method 3: -Wl,-Bstatic and -Wl,-Bdynamic (selective static linking)
# Link only libdemo statically, but other libraries dynamically
# The linker processes these flags left-to-right
gcc -o prog main.c \
-Wl,-Bstatic -L./libs -ldemo \
-Wl,-Bdynamic -lm -lpthread
# Effect:
# libdemo → static (copied into binary)
# libm, libpthread → dynamic (loaded at run time from .so)
# This is the most flexible approach:
# use static for YOUR library, dynamic for system libraries
# Practical example: embed OpenSSL statically, use system libc dynamically
gcc -o secure_app main.c \
-Wl,-Bstatic \
-L/opt/openssl-static/lib -lssl -lcrypto \
-Wl,-Bdynamic \
-lc
# Result: OpenSSL code is baked in, libc is still dynamic
ldd secure_app
# Shows: libc.so => ... (dynamic)
# Does NOT show libssl or libcrypto (static)
With static libraries, link order matters. The linker scans archives left-to-right and only includes object files that satisfy unresolved symbols at that point. Circular dependencies between static libraries require listing a library multiple times.
# libA needs a symbol from libB, and libB needs a symbol from libA
# Solution: list libA twice (or use --start-group / --end-group)
gcc -o prog main.c -lA -lB -lA # list libA again at the end
# Or use the group option (scans the group repeatedly until resolved):
gcc -o prog main.c -Wl,--start-group -lA -lB -Wl,--end-group
2. When users cannot or should not install shared libraries (single-binary distribution).
3. When you need immunity to shared library upgrades that might break your application.
4. For security-critical programs where you want to pin exact library versions.
ar rcs libname.a file1.o file2.o file3.o. The options mean: r=insert/replace members, c=create archive if it doesn’t exist, s=add a symbol index (makes linking faster). The ar (archiver) command is essentially tar for object files.gcc main.c -Wl,-Bstatic -lmylib -Wl,-Bdynamic -lm -lpthread — this links libmylib statically and libm/libpthread dynamically. The linker processes these flags in order, left to right.ldd ./binary: shows dynamic dependencies; for a static binary it says “not a dynamic executable”–
file ./binary: shows “statically linked” or “dynamically linked”–
readelf -d ./binary | grep NEEDED: shows DT_NEEDED entries (empty for fully static binary)