dlopen() in Depth Reference Counting & Dependency Trees

 

42.1.1 — dlopen() in Depth
All Flags, Reference Counting & Dependency Trees | Chapter 42 · TLPI
EmbeddedPathashala.com — Free Embedded & Linux Tutorials

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.

Key Flags

RTLD_LAZY RTLD_NOW RTLD_GLOBAL RTLD_LOCAL RTLD_NODELETE RTLD_NOLOAD RTLD_DEEPBIND Reference Count Dependency Tree LD_BIND_NOW

Mandatory Flags: RTLD_LAZY vs RTLD_NOW

You must choose exactly one of these two flags in every dlopen() call:

RTLD_LAZY — Lazy Symbol Resolution

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.
RTLD_NOW — Immediate Symbol Resolution

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 environment variable: Setting 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

RTLD_GLOBAL

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.

RTLD_LOCAL (default)

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.

RTLD_LOCAL (default)

libA.so symbols
↳ hidden from other libs
RTLD_GLOBAL

libA.so symbols
↳ visible to libB.so, libC.so…
RTLD_NODELETE (since glibc 2.2)

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.

RTLD_NOLOAD (since glibc 2.2)

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_GLOBAL to change a previously RTLD_LOCAL library to RTLD_GLOBAL without reloading it.
RTLD_DEEPBIND (since glibc 2.3.4)

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
Common bug: Calling dlopen() 3 times but dlclose() only once means the library stays loaded permanently. Always match every dlopen() with a dlclose().

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
Real use case: Plugin managers use RTLD_NOLOAD to avoid double-loading a plugin that was already requested by another part of the program. They check first, and only call dlopen() without RTLD_NOLOAD if the library isn’t loaded yet.

Interview Questions & Answers

Q1. What is the difference between RTLD_LAZY and RTLD_NOW?
RTLD_LAZY defers resolution of function symbols until the code that uses them is actually executed. RTLD_NOW resolves all symbols immediately before dlopen() returns, causing dlopen() to fail if any symbol is undefined. RTLD_LAZY is faster at load time; RTLD_NOW catches errors earlier. Variable references are resolved immediately under both flags.
Q2. What does RTLD_GLOBAL do, and when would you use it?
RTLD_GLOBAL makes the symbols of the loaded library visible to all subsequently loaded libraries. Without it (the default RTLD_LOCAL), a library’s symbols are private. You would use RTLD_GLOBAL when loading a “base” shared library whose symbols need to be available to plugin libraries loaded later — for example, a scripting engine loaded globally so that plugins can use its API.
Q3. How does reference counting work with dlopen() and dlclose()?
Each dlopen() call on a library increments its reference count by 1. The library is only loaded into memory on the first call. Each dlclose() decrements the count. The library is only removed from memory when the count reaches 0. This means if your code calls dlopen() 3 times, you must call dlclose() 3 times to actually unload the library.
Q4. What is RTLD_NOLOAD and what are its two main uses?
RTLD_NOLOAD prevents dlopen() from actually loading a library. Its two uses are: (1) checking if a library is currently loaded — if dlopen() returns non-NULL, the library is present in memory; (2) promoting the flags of an already-loaded library — for example, changing a library from RTLD_LOCAL to RTLD_GLOBAL by calling dlopen() with RTLD_NOLOAD|RTLD_GLOBAL without incurring the cost of actually reloading it.
Q5. What is RTLD_DEEPBIND and when is it useful?
RTLD_DEEPBIND makes the loaded library prefer its own symbol definitions over globally loaded symbols with the same name. Without it, if two libraries define a function with the same name and one is RTLD_GLOBAL, there can be unexpected symbol interposition. RTLD_DEEPBIND makes the library self-contained — useful when loading plugins that must use their own private implementations regardless of what the main program has loaded.
Q6. What happens when you dlopen() a library that has its own shared library dependencies?
dlopen() automatically loads all the dependencies of the requested library, recursively. This is called the dependency tree. All libraries in the tree have their reference counts incremented. When the top-level library’s reference count reaches 0 after dlclose(), its dependencies are also dlclosed recursively.

Leave a Reply

Your email address will not be published. Required fields are marked *