C Static Libraries and Shared Libraries

C programs are commonly divided into reusable components called libraries. Libraries allow developers to package functions and data structures once and reuse them across multiple applications.

The two major forms of libraries on Unix-like systems are static libraries and shared libraries. Static libraries are incorporated into an executable during linking, while shared libraries are normally loaded by the runtime linker when the program starts or when explicitly requested.

What Is a Library?

A library is a collection of compiled code and related symbols that other programs can use. Instead of implementing the same functionality repeatedly, an application can call functions provided by a library.

For example, a project might provide string utilities in one library and mathematical functions in another.

Source Files, Object Files, and Libraries

C source code is normally compiled into object files before the final executable is linked.

TEXT
math.c  ->  math.o
io.c    ->  io.o
main.c  ->  main.o

math.o + io.o + main.o -> program

A library packages reusable object code so applications can link against it without needing to compile the library's source files every time.

Static Libraries

A static library is an archive of object files. On Unix-like systems, static libraries conventionally use the .a extension.

TEXT
libmathutils.a

When a program is statically linked against a library, the linker extracts the required object modules from the archive and places their code into the executable.

Creating a Static Library

Suppose a project contains the following files:

TEXT
math_utils.c
math_utils.h
C
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);

#endif
C
#include "math_utils.h"

int add(int a, int b)
{
    return a + b;
}

int multiply(int a, int b)
{
    return a * b;
}

First compile the implementation into an object file.

TEXT
gcc -Wall -Wextra -std=c17 -c math_utils.c -o math_utils.o

Then create an archive containing the object file.

TEXT
ar rcs libmathutils.a math_utils.o

The resulting libmathutils.a is a static library.

The ar Command

The ar utility creates and modifies archive files. Static libraries are commonly created with ar.

TEXT
ar rcs libmathutils.a math_utils.o
OptionMeaning
rInsert or replace files in the archive
cCreate the archive if necessary
sCreate or update the archive symbol index

Using a Static Library

An application can include the library's header and link against the generated archive.

C
#include <stdio.h>
#include "math_utils.h"

int main(void)
{
    printf("%d\n", add(10, 20));
    printf("%d\n", multiply(4, 5));

    return 0;
}

If libmathutils.a is in the current directory, it can be linked with:

TEXT
gcc main.c -L. -lmathutils -o program

The -L. option tells the linker to search the current directory for libraries, while -lmathutils refers to a library named libmathutils.

Library Naming Convention

The -l option follows a naming convention. When you specify -lmathutils, the linker looks for a library such as libmathutils.a or a corresponding shared-library form in its configured search paths.

TEXT
-lmathutils

libmathutils.a
libmathutils.so

Static Linking

Static linking copies the required library code into the final executable at link time.

TEXT
main.o + required objects from libmathutils.a
                    |
                    v
               executable

Advantages of Static Libraries

  • The executable contains the required library code
  • The program can be deployed without a separate copy of that library
  • The executable is less dependent on the target system's shared-library versions
  • Library code can be selected at link time

Disadvantages of Static Libraries

  • Executables can become larger
  • Updating the library generally requires rebuilding applications that statically linked it
  • Multiple programs may contain duplicate copies of the same library code
  • Security fixes in the library do not automatically update already-built executables

Shared Libraries

A shared library is a separately stored library that can be loaded and used by multiple processes. On Linux and many Unix-like systems, shared libraries commonly use the .so extension.

TEXT
libmathutils.so

Instead of copying the library's implementation into every executable, the executable can contain references to symbols provided by the shared library.

Creating a Shared Library

Compile the source with position-independent code.

TEXT
gcc -Wall -Wextra -fPIC -c math_utils.c -o math_utils.o

Then create the shared library.

TEXT
gcc -shared -o libmathutils.so math_utils.o

What Is -fPIC?

The -fPIC option requests position-independent code. Position-independent code is useful for shared libraries because the code can operate correctly regardless of where the library is mapped into a process's address space.

What Does -shared Do?

The -shared option instructs the compiler driver to produce a shared library rather than a normal executable.

Linking Against a Shared Library

An application can be linked against the shared library using the same -l convention.

TEXT
gcc main.c -L. -lmathutils -o program

The resulting executable records a dependency on the shared library rather than incorporating the entire library implementation into the executable.

Runtime Library Search

At runtime, the dynamic linker must be able to locate the required shared library. Common mechanisms include system library directories, configured linker paths, and runtime search-path information embedded in the executable.

For development, an environment such as LD_LIBRARY_PATH can be useful for testing a library in a nonstandard directory, although relying on it for production deployment is often undesirable.

TEXT
LD_LIBRARY_PATH=. ./program

RPATH and RUNPATH

An executable can contain runtime search-path information. Modern toolchains commonly distinguish between DT_RPATH and DT_RUNPATH, with their lookup behavior differing in important ways.

For example, a build can request a runtime search path relative to the executable's location.

TEXT
gcc main.c -L./lib -lmathutils \
    -Wl,-rpath,'$ORIGIN/lib' \
    -o program

$ORIGIN is interpreted by the dynamic linker as the directory containing the executable.

Static vs Shared Libraries

FeatureStatic LibraryShared Library
Typical extension.a.so
Main link-time behaviorRequired object code is copied into executableExecutable records references to shared-library symbols
Runtime dependencyUsually no separate library requiredShared library must be available
Executable sizeUsually largerUsually smaller
Library updateRequires relinking applicationCan often update library independently if ABI remains compatible
Code sharingNo runtime sharing of the library imageMultiple processes can share mapped library pages

Object Files Inside Static Libraries

A static library can contain many object files.

TEXT
libutils.a
├── string_utils.o
├── math_utils.o
├── file_utils.o
└── logging.o

The linker can extract the archive members needed to resolve references from the application.

Building a Larger Static Library

TEXT
gcc -c math_utils.c -o math_utils.o
gcc -c string_utils.c -o string_utils.o
gcc -c file_utils.c -o file_utils.o

ar rcs libutils.a \
    math_utils.o \
    string_utils.o \
    file_utils.o

Static Library Link Order

Library ordering can matter when using traditional Unix linkers. A library generally needs to appear after the object files or libraries that reference its symbols.

TEXT
gcc main.o -lutils -o program

When multiple libraries depend on one another, their order can also affect symbol resolution.

Shared Library Versioning

Shared libraries often use versioned filenames to represent ABI versions.

TEXT
libexample.so
libexample.so.1
libexample.so.1.2.3

A symbolic link can provide the unversioned development name while the runtime linker uses a specific SONAME or versioned library.

ABI Compatibility

A shared library's Application Binary Interface, or ABI, describes binary-level details that clients depend on, including exported symbols, calling conventions, data layouts, and other binary interfaces.

Changing a public structure's layout or removing an exported function can break programs that were compiled against an older ABI.

API vs ABI

ConceptMeaning
APISource-level interface exposed to programmers
ABIBinary-level interface used by compiled programs

Symbol Visibility

Shared libraries can control which symbols are exported. Keeping the public symbol set small can make an ABI easier to maintain.

On GCC-compatible systems, visibility attributes or linker options can be used to control exported symbols.

C
__attribute__((visibility("default")))
int public_function(void)
{
    return 42;
}

__attribute__((visibility("hidden")))
int internal_function(void)
{
    return 10;
}

The exact visibility mechanism is compiler- and platform-dependent, so portable libraries often hide such details behind macros.

Inspecting a Static Library

The ar utility can list the object files contained in an archive.

TEXT
ar -t libutils.a

Inspecting Symbols

Tools such as nm can display symbols contained in object files, static libraries, executables, and shared libraries.

TEXT
nm libmathutils.a
nm -D libmathutils.so

The exact symbol output depends on the platform and binary format.

Inspecting Shared Library Dependencies

On Linux, ldd is commonly used to inspect the shared-library dependencies recorded by an executable.

TEXT
ldd ./program

For security-sensitive analysis, avoid using ldd on untrusted executables because its behavior can involve executing or loading code depending on the environment and implementation. Static inspection tools such as readelf can provide safer alternatives for many tasks.

Using readelf

readelf can inspect ELF binaries and display dynamic sections and dependencies.

TEXT
readelf -d ./program
readelf -Ws libmathutils.so

Dynamic Loading with dlopen()

A program can explicitly load a shared library at runtime using the POSIX dynamic loading interface.

C
#include <dlfcn.h>
#include <stdio.h>

int main(void)
{
    void *handle = dlopen("./libmathutils.so", RTLD_NOW);

    if (handle == NULL)
    {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    dlclose(handle);
    return 0;
}

On systems that provide this interface, programs commonly use dlopen(), dlsym(), dlerror(), and dlclose() for runtime loading.

Looking Up a Function with dlsym()

C
#include <dlfcn.h>
#include <stdio.h>

typedef int (*operation_fn)(int, int);

int main(void)
{
    void *handle = dlopen("./libmathutils.so", RTLD_NOW);

    if (handle == NULL)
    {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    dlerror();

    operation_fn add_function =
        (operation_fn)dlsym(handle, "add");

    const char *error = dlerror();

    if (error != NULL)
    {
        fprintf(stderr, "%s\n", error);
        dlclose(handle);
        return 1;
    }

    printf("%d\n", add_function(2, 3));

    dlclose(handle);
    return 0;
}

The function-pointer conversion shown above follows common POSIX practice, but dynamic loading APIs are not part of ISO C itself.

Linking dlopen() Programs

On systems where libdl is a separate library, programs using dlopen() may need to link with -ldl.

TEXT
gcc loader.c -ldl -o loader

On some modern Linux toolchains, libdl functionality may be integrated differently, so the exact linker requirements depend on the platform and toolchain.

Creating a Shared Library with Multiple Files

TEXT
gcc -fPIC -c math_utils.c -o math_utils.o
gcc -fPIC -c string_utils.c -o string_utils.o

gcc -shared -o libutils.so \
    math_utils.o \
    string_utils.o

Using a Library Makefile

MAKEFILE
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
PICFLAGS = -fPIC

LIB = libmathutils.a
OBJ = math_utils.o

$(LIB): $(OBJ)
	ar rcs $@ $^

math_utils.o: math_utils.c math_utils.h
	$(CC) $(CFLAGS) $(PICFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(OBJ) $(LIB)

Makefile for a Shared Library

MAKEFILE
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -fPIC

LIB = libmathutils.so
OBJ = math_utils.o

$(LIB): $(OBJ)
	$(CC) -shared -o $@ $^

math_utils.o: math_utils.c math_utils.h
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(OBJ) $(LIB)

Static and Shared Libraries in One Project

A project can produce both static and shared variants of the same library.

MAKEFILE
CC = gcc
CFLAGS = -Wall -Wextra -std=c17
PICFLAGS = -fPIC

STATIC = libmathutils.a
SHARED = libmathutils.so
OBJ = math_utils.o
PICOBJ = math_utils.pic.o

$(STATIC): $(OBJ)
	ar rcs $@ $^

math_utils.o: math_utils.c math_utils.h
	$(CC) $(CFLAGS) -c $< -o $@

$(SHARED): $(PICOBJ)
	$(CC) -shared -o $@ $^

math_utils.pic.o: math_utils.c math_utils.h
	$(CC) $(CFLAGS) $(PICFLAGS) -c $< -o $@

.PHONY: all clean

all: $(STATIC) $(SHARED)

clean:
	rm -f $(OBJ) $(PICOBJ) $(STATIC) $(SHARED)

When to Use Static Libraries

  • When a self-contained executable is desirable
  • When deployment environments are tightly controlled
  • When reducing runtime library dependencies is important
  • When distributing a library implementation as part of an executable

When to Use Shared Libraries

  • When multiple programs should share library code
  • When independent library updates are useful
  • When reducing executable size is beneficial
  • When a plugin or runtime-loading architecture is required
  • When a stable ABI can be maintained

Common Problems

ProblemPossible Cause
cannot find -lfooLibrary search path or library name is incorrect
undefined referenceMissing library, incorrect link order, or missing symbol
shared library not found at runtimeDynamic linker cannot locate the library
wrong architectureLibrary and application were built for incompatible architectures
symbol lookup errorRequired symbol is missing or incompatible
ABI incompatibilityLibrary changed in a way that breaks compiled clients

Static Library Troubleshooting

If the linker cannot find a symbol provided by a static library, verify that the library is actually included and that its position in the link command is appropriate.

TEXT
gcc main.o -L. -lmathutils -o program

You can inspect the archive with nm to confirm that the expected symbol exists.

TEXT
nm libmathutils.a | grep add

Shared Library Troubleshooting

If an executable links successfully but fails to start because a shared library cannot be found, inspect its dependencies and runtime search paths.

TEXT
readelf -d ./program
ldd ./program

Library Search Paths

The compiler's link-time search paths and the dynamic linker's runtime search paths are related but are not the same thing.

  • Use -L to add library search directories during linking
  • Use -I to add header search directories during compilation
  • Use configured system library directories for installed shared libraries
  • Use runtime search-path mechanisms when a nonstandard deployment location is required

Headers and Libraries

A library normally provides both an implementation and a public interface. The header describes the functions and types that client programs are allowed to use, while the compiled library contains the implementation.

TEXT
include/
└── math_utils.h

lib/
└── libmathutils.so

src/
└── math_utils.c

Do Not Expose Unnecessary Internals

A good library exposes a small, stable public API and keeps implementation details private. This makes future changes easier and reduces accidental dependencies on internal symbols.

Library Distribution

A library distribution commonly contains headers, library binaries, documentation, and sometimes development metadata.

TEXT
my-library/
├── include/
│   └── math_utils.h
├── lib/
│   ├── libmathutils.a
│   └── libmathutils.so
└── README.md

Best Practices

  • Keep public headers small and stable
  • Hide implementation details whenever possible
  • Use meaningful library names
  • Keep ABI compatibility in mind for shared libraries
  • Compile shared-library code with position-independent code when required by the target platform
  • Use versioning for public shared-library ABIs
  • Avoid unnecessary exported symbols
  • Document required linker and runtime dependencies
  • Use automated builds to produce libraries consistently
  • Test both clean builds and incremental builds

Quick Reference

CommandPurpose
gcc -c file.cCompile C source into an object file
ar rcs libx.a file.oCreate a static library
gcc -fPIC -c file.cCompile position-independent code
gcc -shared -o libx.so file.oCreate a shared library
gcc main.c -L. -lxLink against a library in the current directory
nm libx.aInspect symbols
readelf -d programInspect ELF dynamic information
ldd programDisplay shared-library dependencies on Linux
dlopen()Load a shared library at runtime
dlsym()Find a symbol in a dynamically loaded library
dlclose()Release a dynamically loaded library handle

Practice Exercises

  • Create a static library containing two C utility functions
  • Write a program that links against the static library
  • Inspect the static library with ar and nm
  • Create a shared version of the same library
  • Build an executable that links against the shared library
  • Use readelf to inspect the executable's shared-library dependencies
  • Experiment with a nonstandard shared-library directory
  • Load a shared library dynamically with dlopen() and call a function using dlsym()
  • Create a Makefile that builds both static and shared versions
  • Design a small public API while keeping implementation functions private

Conclusion

Static and shared libraries are fundamental building blocks for reusable C software. Static libraries package object files into archives that can contribute code directly to an executable, while shared libraries provide reusable binary code that can be loaded and shared at runtime. Understanding compilation, archiving, linking, symbol resolution, runtime search paths, and ABI compatibility is essential for building maintainable C applications and libraries.

Note: Note: Library formats and dynamic-linking behavior differ between operating systems. The examples in this topic primarily target GCC and Unix-like systems using ELF-style shared libraries. Static and shared library mechanisms on Windows and other platforms use different formats and toolchain conventions.