dlopen() Function Signature
#include <dlfcn.h>
void *dlopen(const char *libfilename, int flags);
/* Returns: library handle on success, NULL on error */
The first argument is the path to the shared library. The second argument is a bitmask of flags that control how the library is loaded. At minimum, you must pick exactly one of RTLD_LAZY or RTLD_NOW. Additional flags can be OR-ed in.
Mandatory Flags: RTLD_LAZY vs RTLD_NOW
You must choose exactly one of these two flags in every dlopen() call:
Function symbols are resolved only when the code that uses them is actually executed. If a code path is never taken, the symbols on that path are never resolved. This is the normal behavior of the dynamic linker for startup-loaded libraries.
- Faster library opening — no upfront symbol lookups.
- An unresolved symbol only causes a crash when that code runs.
- Note: variable references are always resolved immediately, even with RTLD_LAZY.
All undefined function symbols are resolved before dlopen() returns. If any symbol cannot be found, dlopen() fails immediately with a NULL return.
- Slower library opening — all symbols resolved upfront.
- Any missing symbol is detected immediately — useful for debugging.
- Safer for long-running servers that must not crash after hours of operation.
| Scenario | Recommended Flag | Why |
|---|---|---|
| Development / debugging | RTLD_NOW | Catch missing symbols immediately |
| Production plug-ins (performance matters) | RTLD_LAZY | Faster startup, acceptable risk |
| Safety-critical embedded server | RTLD_NOW | Fail-fast rather than crash later |
LD_BIND_NOW=1 in the environment forces RTLD_NOW behavior for all shared libraries loaded at startup, and also overrides RTLD_LAZY in dlopen() calls. This was introduced in glibc 2.1.1.Optional Flags
Symbols defined in this library (and its dependency tree) are made available globally — they can be used to resolve references in other libraries loaded later, and can be found by dlsym() with the RTLD_DEFAULT pseudohandle.
Symbols are kept private to this library and its dependency tree. They are not available for resolving references in subsequently loaded libraries. This is the default if neither RTLD_GLOBAL nor RTLD_LOCAL is specified.
The library will not be unloaded even if dlclose() is called and the reference count drops to 0. Static variables in the library retain their values if the library is later reopened. Equivalent to the gcc -Wl,-znodelete option at build time.
Do not load the library. Instead:
- Check if loaded: If the library is already in memory, returns its handle. Otherwise returns NULL.
- Promote flags: You can use
RTLD_NOLOAD | RTLD_GLOBALto change a previously RTLD_LOCAL library to RTLD_GLOBAL without reloading it.
When this library makes a symbol reference, search the library itself first before looking at globally loaded libraries. This makes the library self-contained — its own definitions take priority over same-named globals in other libraries. Similar to the -Bsymbolic linker option but applied at load time.
Reference Counting — How dlopen/dlclose Work Together
| Action | Ref Count | Library in Memory? |
|---|---|---|
| Initial state | 0 | No |
| dlopen() call 1 | 1 | Yes — loaded |
| dlopen() call 2 (same library) | 2 | Yes — same handle returned |
| dlopen() call 3 | 3 | Yes |
| dlclose() call 1 | 2 | Yes — still loaded |
| dlclose() call 2 | 1 | Yes — still loaded |
| dlclose() call 3 | 0 | Unloaded from memory |
Automatic Dependency Loading
When you dlopen() a library that itself depends on other libraries, those dependencies are automatically loaded too. This is called the dependency tree.
|
your program
dlopen(“plugin.so”) |
| ↓ auto-loads |
|
plugin.so
(depends on libssl.so, libcrypto.so) |
| ↓ auto-loads recursively |
|
libssl.so
libcrypto.so
|
All libraries in the dependency tree get their reference counts incremented. When you dlclose(“plugin.so”) and its count drops to 0, its dependencies are also dlclosed (recursively).
Code Example 1: RTLD_LAZY vs RTLD_NOW Comparison
/* flags_demo.c — demonstrates RTLD_LAZY vs RTLD_NOW */
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
void try_load(const char *lib, int flags, const char *flag_name) {
printf("\nAttempting dlopen(\"%s\", %s)...\n", lib, flag_name);
void *handle = dlopen(lib, flags);
if (handle) {
printf(" SUCCESS — library loaded\n");
dlclose(handle);
} else {
printf(" FAILED — %s\n", dlerror());
}
}
int main(void) {
/* Try loading an existing library with RTLD_LAZY */
try_load("libm.so.6", RTLD_LAZY, "RTLD_LAZY");
/* Try loading an existing library with RTLD_NOW */
try_load("libm.so.6", RTLD_NOW, "RTLD_NOW");
/* Try loading a non-existent library */
try_load("libdoesnotexist.so", RTLD_LAZY, "RTLD_LAZY");
return 0;
}
gcc flags_demo.c -ldl -o flags_demo
./flags_demo
# Output:
# Attempting dlopen("libm.so.6", RTLD_LAZY)...
# SUCCESS — library loaded
#
# Attempting dlopen("libm.so.6", RTLD_NOW)...
# SUCCESS — library loaded
#
# Attempting dlopen("libdoesnotexist.so", RTLD_LAZY)...
# FAILED — libdoesnotexist.so: cannot open shared object file: No such file or directory
Code Example 2: RTLD_GLOBAL vs RTLD_LOCAL
/* Demonstrates how RTLD_GLOBAL makes symbols available to later-loaded libraries */
/* shared_util.c — utility library loaded first */
#include <stdio.h>
void util_function(void) {
printf("util_function() called from shared_util.so\n");
}
/* consumer.c — library that calls util_function() without linking it */
/* This only works if shared_util.so was loaded with RTLD_GLOBAL */
extern void util_function(void);
void consumer_run(void) {
printf("consumer_run(): calling util_function...\n");
util_function();
}
/* main_global.c — loads shared_util.so with RTLD_GLOBAL,
then loads consumer.so (which uses util_function) */
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
int main(void) {
/* Load utility library globally so its symbols are available */
void *util = dlopen("./shared_util.so", RTLD_LAZY | RTLD_GLOBAL);
if (!util) { fprintf(stderr, "%s\n", dlerror()); exit(1); }
/* Now load consumer — it can find util_function because of RTLD_GLOBAL */
void *cons = dlopen("./consumer.so", RTLD_LAZY);
if (!cons) {
fprintf(stderr, "consumer load failed: %s\n", dlerror());
/* With RTLD_LOCAL above, this would fail */
dlclose(util);
exit(1);
}
void (*run)(void);
*(void **)(&run) = dlsym(cons, "consumer_run");
if (run) (*run)();
dlclose(cons);
dlclose(util);
return 0;
}
Code Example 3: RTLD_NOLOAD — Check If Library Is Loaded
/* noload_check.c — use RTLD_NOLOAD to check if a library is in memory */
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
int is_library_loaded(const char *libname) {
/* RTLD_NOLOAD: do NOT load if absent; just return handle if present */
void *h = dlopen(libname, RTLD_NOLOAD | RTLD_LAZY);
if (h) {
dlclose(h); /* decrement reference count we just incremented */
return 1;
}
return 0;
}
int main(void) {
/* libc is always loaded */
printf("libc.so.6 loaded? %s\n",
is_library_loaded("libc.so.6") ? "YES" : "NO");
printf("libm.so.6 loaded? %s\n",
is_library_loaded("libm.so.6") ? "YES" : "NO");
/* Now explicitly load libm */
void *libm = dlopen("libm.so.6", RTLD_LAZY);
printf("libm.so.6 loaded now? %s\n",
is_library_loaded("libm.so.6") ? "YES" : "NO");
dlclose(libm);
return 0;
}
gcc noload_check.c -ldl -o noload_check
./noload_check
# libc.so.6 loaded? YES
# libm.so.6 loaded? NO
# libm.so.6 loaded now? YES
