Dynamic Library Loading in C with dlopen and dlsym
In POSIX-compliant C programming, the dynamic linking interface
defined in <dlfcn.h>—primarily dlopen
and dlsym—enables an application to load shared object
files (.so) into memory and bind functions or variables at
runtime. Rather than resolving all external dependencies at compile or
startup time, this mechanism allows a running process to inspect, load,
and execute compiled code on demand, forming the backbone for extensible
architectures such as plugin systems, lazy-loading modules, and runtime
driver interfaces.
The Role of dlopen
The dlopen function makes the dynamic library's
executable code and data available to the calling process. Its function
prototype is:
void *dlopen(const char *filename, int flags);When called with a path to a shared library, the dynamic linker (such
as ld.so on Linux) maps the target object into the calling
process's virtual address space. If the library depends on other shared
objects, dlopen recursively loads those dependencies as
well.
The flags argument determines symbol resolution and
visibility behaviors:
RTLD_LAZY: Resolves function references only when code contained within the library is executed.RTLD_NOW: Resolves all undefined symbols beforedlopenreturns, failing if any unresolved symbols exist.RTLD_GLOBAL: Makes the symbols loaded in this library available for symbol resolution of subsequently loaded libraries.RTLD_LOCAL: Keeps symbols private to the library, preventing other modules from resolving symbols against it unless explicitly referenced.
Upon mapping the shared object, dlopen executes any
initialization routines defined in the library, such as functions marked
with __attribute__((constructor)) or the legacy
_init routine. It returns an opaque void*
handle that acts as an identifier for the loaded object in subsequent
operations.
Resolving Symbols with dlsym
Once a library is loaded, the process needs an entry point into its
functions or variables. The dlsym function locates the
memory address of an exported symbol using the handle returned by
dlopen:
void *dlsym(void *handle, const char *symbol);dlsym parses the dynamic symbol table
(.dynsym) of the targeted library to find the offset of the
specified symbol name. Once found, it calculates the absolute runtime
memory address by adding the library’s base load address to the symbol
offset.
Because dlsym returns a generic pointer
(void*), the calling program must cast this address into
the appropriate function pointer or data pointer type to execute it. In
strict ISO C, casting between a data pointer (void*) and a
function pointer is technically undefined, but POSIX mandates that
void* can represent function pointers to ensure
compatibility with dlsym.
Error Handling with dlerror
Because runtime resolution introduces failure points such as missing
files, unresolved symbols, or syntax mismatches, POSIX provides
dlerror:
char *dlerror(void);Calling dlerror returns a human-readable null-terminated
string describing the most recent dynamic linking error that occurred in
dlopen, dlsym, or dlclose. It
also resets the internal error status, meaning consecutive calls return
NULL unless another error occurs.
Releasing Resources with dlclose
When a library is no longer needed, it is detached using
dlclose:
int dlclose(void *handle);The dynamic linker maintains an internal reference count for every
dynamically loaded object. Each successful call to dlopen
increments this count, and each dlclose decrements it. When
the reference count drops to zero, the linker executes any cleanup
routines (such as destructor functions marked with
__attribute__((destructor))) and unmaps the library from
the process's address space via munmap, freeing memory.
Complete Runtime Dynamic Flow
The runtime dynamic linking process executes in the following sequence:
- Load: The application calls
dlopen("libmath_plugin.so", RTLD_NOW). The operating system locates the file, maps its ELF segments into memory, resolves symbols, executes constructors, and returns a handle. - Inspect: The program calls
dlerror()to verify that the load succeeded without errors. - Resolve: The application queries
dlsym(handle, "compute_value"). The linker traverses the library's dynamic symbol table, retrieves the relative offset ofcompute_value, adds the base load address, and returns the pointer. - Execute: The returned pointer is cast to a function
pointer signature (e.g.,
double (*)(double)) and executed directly by the CPU. - Unload: The application calls
dlclose(handle), decreasing the reference count and unmapping the memory once the library is no longer referenced.