Dynamic libraries (also called shared libraries) allow multiple programs to share a single copy of compiled code at runtime. Unlike static libraries, the linking happens at load time rather than compile time.

Why Dynamic Libraries?

With static libraries, each executable contains its own copy of the library code. If ten programs use the same library, ten copies exist on disk and in memory. Dynamic libraries solve this:

  • A single .so file is shared among all programs that use it
  • Updates to the library take effect for all programs without recompilation
  • Executable file sizes are smaller

The trade-off is that the library must be present on the system at runtime.

Position-Independent Code

To be used as a shared library, code must be compiled as position-independent code (PIC). This tells the compiler to generate code that works regardless of where in memory it is loaded.

Use the -fPIC flag:

gcc -fPIC -c mylib.c -o mylib.o

Creating a Shared Library

Once you have position-independent object files, create the shared library with -shared:

gcc -shared -o libmylib.so mylib.o

By convention, shared libraries are named lib<name>.so.

Linking Against a Shared Library

When compiling a program that uses the library:

gcc main.c -L. -lmylib -o myprogram
  • -L. tells the linker to look in the current directory
  • -lmylib links against libmylib.so

Runtime Library Path

At runtime, the dynamic linker must be able to find the .so file. Options:

  1. Copy the library to a standard location like /usr/lib
  2. Add the directory to /etc/ld.so.conf and run ldconfig
  3. Set the LD_LIBRARY_PATH environment variable:
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./myprogram

Dynamic Loading with dlopen

You can also load shared libraries at runtime programmatically using the dl API:

#include <dlfcn.h>

void *handle = dlopen("libmylib.so", RTLD_LAZY);
if (!handle) {
    fprintf(stderr, "%s\n", dlerror());
    return 1;
}

RTLD_LAZY defers symbol resolution until they are actually called. RTLD_NOW resolves all symbols immediately.

Looking Up Symbols with dlsym

Once a library is open, retrieve a pointer to any exported symbol:

typedef int (*add_func_t)(int, int);

add_func_t add = (add_func_t) dlsym(handle, "add");
char *error = dlerror();
if (error != NULL) {
    fprintf(stderr, "%s\n", error);
    return 1;
}

int result = add(3, 4);
printf("3 + 4 = %d\n", result);

Always call dlerror() after dlsym to check for errors, dlsym may legitimately return NULL for a symbol whose value is null.

Closing the Library with dlclose

When done with a library handle:

dlclose(handle);

This decrements the library's reference count. When the count reaches zero, the library is unloaded from memory.

Compile with -ldl

Programs that use dlopen, dlsym, and dlclose must link against the dl library:

gcc main.c -ldl -o myprogram

Summary

Step Command
Compile to PIC objects gcc -fPIC -c mylib.c
Create shared library gcc -shared -o libmylib.so mylib.o
Link program gcc main.c -L. -lmylib -o prog
Runtime loading dlopen() / dlsym() / dlclose()
Link dl API gcc main.c -ldl -o prog