dlclose(), dladdr() Closing Libraries · Inspecting Symbol Addresses

 

42.1.4–42.1.6 — dlclose(), dladdr() & –export-dynamic
Closing Libraries · Inspecting Symbol Addresses · Exporting Main Program Symbols | Chapter 42 · TLPI
EmbeddedPathashala.com — Free Embedded & Linux Tutorials

What This File Covers

This file completes the dlopen API with three final topics: dlclose() for properly unloading libraries, dladdr() for reverse-lookup of symbol information, and the –export-dynamic linker option that allows loaded plugins to call back into the main program. Finally, we bring it all together with the complete dynload example from TLPI.

Key Concepts

dlclose() dladdr() Dl_info struct –export-dynamic -rdynamic atexit() in library Callback from plugin dli_fname

42.1.4 — dlclose(): Closing a Shared Library

#include <dlfcn.h>
int dlclose(void *handle);
/* Returns: 0 on success, -1 on error */

dlclose() does the following:

  1. Decrements the reference count of the library referred to by handle.
  2. If the reference count falls to 0 AND no other loaded library still needs symbols from it, the library is unmapped from the process’s address space.
  3. Any libraries in the dependency tree are also dlclosed recursively.
  4. When the library is actually unloaded, all destructor functions (__attribute__((destructor))) are called first.
atexit() in shared libraries (glibc 2.2.3+): A function inside a shared library can call atexit() to register a cleanup function. That function is automatically called when the library is unloaded via dlclose(). This is useful for releasing resources allocated by the library.
/* Correct dlclose usage with error check */
if (dlclose(handle) != 0) {
    fprintf(stderr, "dlclose error: %s\n", dlerror());
}
Using symbols after dlclose(): Any function pointer or variable pointer obtained via dlsym() becomes invalid after the library is unloaded. Calling through such a pointer after dlclose() causes undefined behavior (usually a segfault). Always set such pointers to NULL after calling dlclose().

42.1.5 — dladdr(): Reverse Symbol Lookup

#define _GNU_SOURCE
#include <dlfcn.h>

int dladdr(const void *addr, Dl_info *info);
/* Returns: nonzero if addr was found in a shared library, 0 otherwise */

dladdr() is the reverse of dlsym(). Given an address (typically one returned by dlsym()), it fills in a Dl_info structure with information about where that address came from.

The Dl_info structure:

typedef struct {
    const char *dli_fname;  /* Pathname of the shared library containing addr */
    void       *dli_fbase;  /* Base address at which that library is loaded */
    const char *dli_sname;  /* Name of nearest symbol with address <= addr */
    void       *dli_saddr;  /* Actual address of the symbol in dli_sname */
} Dl_info;
Field Meaning Example
dli_fname Full path of the .so file /lib/x86_64-linux-gnu/libm.so.6
dli_fbase Address where library is mapped in memory 0x7f8a3c400000
dli_sname Name of the nearest symbol at or before addr “sin”
dli_saddr Actual address of that symbol 0x7f8a3c412340
Not in SUSv3: dladdr() is a glibc extension, not specified in the POSIX standard. It is available on Linux and Solaris but not all UNIX systems. Requires #define _GNU_SOURCE.

42.1.6 — –export-dynamic: Callbacks into the Main Program

Consider this scenario: your main program loads a plugin using dlopen(). The plugin calls a function callback_fn() which is defined in the main program. Will it work?

By default — NO. Global symbols in the main program are not available to dynamically loaded libraries unless you explicitly export them.

The –export-dynamic linker option adds all global-scope symbols of the main program to its dynamic symbol table, making them available to any dynamically loaded library:

# Without --export-dynamic: plugin cannot find main program functions
gcc -o myprog main.c -ldl

# With --export-dynamic: plugin CAN find and call main program functions
gcc -Wl,--export-dynamic -o myprog main.c -ldl

# These are all equivalent:
gcc -export-dynamic -o myprog main.c -ldl    # same
gcc -rdynamic       -o myprog main.c -ldl    # same (gcc shorthand)
gcc -Wl,-E          -o myprog main.c -ldl    # same (another synonym)
Without –export-dynamic

Plugin tries to call main_callback() → dlsym returns NULL → crash or silent failure

With –export-dynamic

Plugin calls dlsym(RTLD_DEFAULT, “main_callback”) → finds it → callback works correctly

When do you need this? Any time a dynamically loaded plugin needs to call functions defined in the main program. Common examples: GUI application plug-ins that call main UI functions, game engines with scripting plugins, and server applications with module callbacks.

Code Example 1: dlclose() and atexit() in a Library

/* cleanup_lib.c — library that registers cleanup via atexit() */
#include <stdio.h>
#include <stdlib.h>

static void library_cleanup(void) {
    printf("[cleanup_lib] atexit cleanup running — freeing resources\n");
}

void library_init(void) {
    printf("[cleanup_lib] library_init() called\n");
    atexit(library_cleanup);  /* Register cleanup for when library is unloaded */
}
/* dlclose_demo.c — shows when the atexit cleanup fires */
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>

int main(void) {
    void *lib = dlopen("./cleanup_lib.so", RTLD_LAZY);
    if (!lib) { fprintf(stderr, "%s\n", dlerror()); exit(1); }

    void (*init_fn)(void);
    dlerror();
    *(void **)(&init_fn) = dlsym(lib, "library_init");
    if (!dlerror()) (*init_fn)();

    printf("About to call dlclose()...\n");
    dlclose(lib);
    printf("dlclose() returned — library unloaded\n");

    return 0;
}
gcc -fPIC -shared -o cleanup_lib.so cleanup_lib.c
gcc dlclose_demo.c -ldl -o dlclose_demo
./dlclose_demo
# [cleanup_lib] library_init() called
# About to call dlclose()...
# [cleanup_lib] atexit cleanup running — freeing resources
# dlclose() returned — library unloaded

Code Example 2: dladdr() — Inspecting Symbol Information

/* dladdr_demo.c — use dladdr() to inspect addresses */
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <math.h>
#include <stdlib.h>

void print_symbol_info(const char *label, void *addr) {
    Dl_info info;
    printf("\n--- %s (addr=%p) ---\n", label, addr);
    if (dladdr(addr, &info)) {
        printf("  Library path : %s\n", info.dli_fname ? info.dli_fname : "(null)");
        printf("  Library base : %p\n", info.dli_fbase);
        printf("  Symbol name  : %s\n", info.dli_sname ? info.dli_sname : "(null)");
        printf("  Symbol addr  : %p\n", info.dli_saddr);
        printf("  Offset       : %ld bytes\n",
               (long)((char*)addr - (char*)info.dli_saddr));
    } else {
        printf("  Not found in any shared library\n");
    }
}

int main(void) {
    /* Look up sin() from libm */
    void *lib = dlopen("libm.so.6", RTLD_LAZY | RTLD_GLOBAL);
    if (!lib) { fprintf(stderr, "%s\n", dlerror()); exit(1); }

    double (*sin_fn)(double);
    dlerror();
    *(void **)(&sin_fn) = dlsym(lib, "sin");
    if (!dlerror()) {
        print_symbol_info("sin()", (void*)sin_fn);
    }

    /* Also inspect main() itself — it's in the main executable */
    print_symbol_info("main()", (void*)main);

    dlclose(lib);
    return 0;
}
gcc dladdr_demo.c -ldl -lm -o dladdr_demo
./dladdr_demo
# --- sin() (addr=0x7f...) ---
#   Library path : /lib/x86_64-linux-gnu/libm.so.6
#   Library base : 0x7f...
#   Symbol name  : sin
#   Symbol addr  : 0x7f...
#   Offset       : 0 bytes
#
# --- main() (addr=0x55...) ---
#   Library path : ./dladdr_demo
#   Library base : 0x55...
#   Symbol name  : main
#   Symbol addr  : 0x55...
#   Offset       : 0 bytes

Code Example 3: Complete dynload.c (from TLPI) + –export-dynamic

This is the complete dynload.c example from TLPI Listing 42-1, extended with dladdr() and export-dynamic callback support:

/* dynload_full.c — complete example: load a library, find a function,
   call it, inspect its address with dladdr() */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>

/* This function is in the main program.
   With --export-dynamic, a loaded library can call it back. */
void main_program_callback(const char *msg) {
    printf("[MAIN CALLBACK] received: %s\n", msg);
}

int main(int argc, char *argv[]) {
    void *libHandle;
    void (*funcp)(void);
    const char *err;

    if (argc != 3 || strcmp(argv[1], "--help") == 0) {
        fprintf(stderr, "Usage: %s lib-path func-name\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* Load the shared library */
    libHandle = dlopen(argv[1], RTLD_LAZY);
    if (libHandle == NULL) {
        fprintf(stderr, "dlopen: %s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    printf("Loaded: %s\n", argv[1]);

    /* Look up the requested function */
    dlerror();
    *(void **)(&funcp) = dlsym(libHandle, argv[2]);
    err = dlerror();
    if (err != NULL) {
        fprintf(stderr, "dlsym: %s\n", err);
        dlclose(libHandle);
        exit(EXIT_FAILURE);
    }

    if (funcp == NULL) {
        printf("Symbol '%s' has NULL value\n", argv[2]);
    } else {
        printf("Calling %s()...\n", argv[2]);
        (*funcp)();

        /* Use dladdr to inspect the function's origin */
        Dl_info info;
        if (dladdr((void*)funcp, &info)) {
            printf("Function '%s' is in: %s (loaded at %p)\n",
                   info.dli_sname ? info.dli_sname : "?",
                   info.dli_fname ? info.dli_fname : "?",
                   info.dli_fbase);
        }
    }

    dlclose(libHandle);
    exit(EXIT_SUCCESS);
}
# Build with --export-dynamic so loaded libs can call main_program_callback:
gcc -Wl,--export-dynamic -o dynload_full dynload_full.c -ldl

# Create a test library:
# (libdemo.c defines x1() which may call back into main)
gcc -fPIC -shared -o libdemo.so libdemo.c

# Run:
./dynload_full ./libdemo.so x1

Interview Questions & Answers

Q1. What exactly does dlclose() do when the reference count is still above 0?
When dlclose() is called and the reference count decrements to a value still above 0, nothing is unloaded. The library remains in memory and all function pointers obtained from it remain valid. The library is only actually unmapped from the process’s address space when the reference count reaches exactly 0 and no other library depends on its symbols.
Q2. What is the Dl_info structure and what information does dladdr() fill into it?
Dl_info is a structure with four fields. dli_fname is the pathname of the shared library containing the queried address. dli_fbase is the base address at which that library was loaded in memory. dli_sname is the name of the nearest symbol whose address is at or before the queried address. dli_saddr is the actual memory address of that symbol. Together these let you reverse-engineer where any code or data address came from.
Q3. Why does a dynamically loaded library need –export-dynamic to call functions in the main program?
When a program is compiled normally, its global symbols are placed in the static symbol table but not in the dynamic symbol table. The dynamic linker only looks at the dynamic symbol table when resolving references from shared libraries. Without –export-dynamic, the main program’s functions are invisible to any library loaded via dlopen(). The –export-dynamic linker flag copies all global symbols into the dynamic symbol table, making them findable by dlsym() and resolvable by the dynamic linker from within plugins.
Q4. What are the synonyms for –export-dynamic?
There are four equivalent ways to achieve the same effect: -Wl,–export-dynamic (passing the flag to the linker via gcc), -export-dynamic (gcc handles it directly), -rdynamic (a gcc alias), and -Wl,-E (another linker alias). All four add the main program’s global symbols to the dynamic symbol table.
Q5. What happens to function pointers obtained from a library after dlclose() unloads it?
After a library is actually unloaded (reference count drops to 0), all code and data from that library is unmapped from the process’s address space. Any function pointer or variable pointer previously obtained via dlsym() now points into unmapped memory. Calling through such a pointer causes a segmentation fault (SIGSEGV). Best practice is to NULL out all such pointers immediately after calling dlclose().

Leave a Reply

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