dlsym(), dlclose() & dladdr()
Advanced dlopen API
Deep-dive into symbol lookup with dlsym(), function-pointer casting rules, pseudohandles RTLD_DEFAULT & RTLD_NEXT, reference-counted unloading with dlclose(), and introspection via dladdr() — with a complete working program.
1. dlsym() — Looking Up a Symbol at Runtime
dlsym() is the core lookup function of the dlopen API. Once you have a library handle from dlopen(), you call dlsym() to resolve a named symbol — whether a variable or a function — to its in-process virtual address.
#include <dlfcn.h>
void *dlsym(void *handle, char *symbol); Returns: address of symbol on success, NULL if not found (or on error)
The handle is either:
- A library handle returned by dlopen(), or
- One of the special pseudohandles: RTLD_DEFAULT or RTLD_NEXT (see Section 3).
The symbol argument is the C name of the symbol (e.g., “malloc”, “my_func”, “global_counter”).
1.1 The NULL Return Ambiguity
A tricky corner case: dlsym() returns NULL both when a symbol is not found and when a symbol’s actual value is NULL (e.g., a global pointer variable initialised to NULL).
The correct pattern is to clear any prior error first, call dlsym(), then inspect dlerror():
void *sym;
const char *err;
dlerror(); /* 1. Clear any previous error */
sym = dlsym(handle, "my_symbol");
err = dlerror(); /* 2. Check for error AFTER the call */
if (err != NULL) {
fprintf(stderr, "dlsym error: %s\n", err);
exit(EXIT_FAILURE);
}
/* sym == NULL now means the symbol's value genuinely IS NULL */
if (sym == NULL)
printf("Symbol value is NULL\n");
1.2 Accessing a Variable Symbol
When the symbol names a global variable in the shared library, cast the void * return to the correct pointer type, then dereference:
#include <dlfcn.h>
#include <stdio.h>
/* --- In the shared library (libdemo.so) --- */
// int myvar = 42;
/* --- In the calling program --- */
int *ip;
dlerror();
ip = (int *) dlsym(handle, "myvar");
if (dlerror() == NULL && ip != NULL)
printf("myvar = %d\n", *ip); /* prints: myvar = 42 */
1.3 Calling a Function Symbol — The C99 Cast Problem
This is the most nuanced part of dlsym() usage. The C99 standard forbids direct conversion between void * (a data pointer) and a function pointer. Attempting the intuitive assignment:
int (*funcp)(int);
/* ❌ WRONG — C99 forbids direct void* → function-pointer conversion */
funcp = dlsym(handle, "my_function");
produces a compiler warning (“assignment from incompatible pointer type”). Similarly, the apparently equivalent:
/* ❌ Also WRONG — gcc -pedantic warns: */
/* "ANSI C forbids use of cast expressions as lvalues" */
(void *) funcp = dlsym(handle, "my_function");
The correct, portable workaround — sanctioned by SUSv3 TC1 — is:
int (*funcp)(int); /* function pointer: takes int, returns int */
/* ✅ CORRECT — cast address of funcp to void**, then assign */
*(void **) (&funcp) = dlsym(handle, "my_function");
*(void **)(&funcp) work?We are not converting a function pointer to/from void *. Instead, we take the address of funcp (which is int (**)(int)), cast that address to void ** (a pointer to a data pointer), and assign the void * return of dlsym() into that storage location. The assignment target is a data pointer (void *), so C99 permits it.
After obtaining the function pointer, invoke it with standard C function-pointer syntax:
int result;
/* Invoke the dynamically loaded function */
result = (*funcp)(42);
/* Most compilers also accept: */
result = funcp(42);
funcp = (int (*)(int)) dlsym(handle, sym);
compiles cleanly, but C99-conformant compilers must still warn. SUSv4 did not introduce a cleaner API, so *(void **) remains the standard idiom.
2. Pseudohandles: RTLD_DEFAULT and RTLD_NEXT
Instead of a real library handle from dlopen(), dlsym() accepts two special pseudohandle constants. These extend the symbol search scope beyond a single library.
| Pseudohandle | Search Scope | Typical Use Case |
|---|---|---|
| RTLD_DEFAULT | Main program first, then all loaded shared libraries in load order (including RTLD_GLOBAL dlopen’d libs). Mirrors the dynamic linker’s default resolution. | Find a symbol anywhere in the process — useful for optional/feature-detection checks. |
| RTLD_NEXT | Shared libraries loaded after the calling library (i.e., next in the load-order chain after the current object). | Implementing interposers / wrappers that delegate to the real symbol after custom logic. |
2.1 RTLD_DEFAULT — Global Symbol Lookup
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
int main(void)
{
void *sym;
/* Search everywhere in the process for "printf" */
dlerror();
sym = dlsym(RTLD_DEFAULT, "printf");
if (dlerror() == NULL && sym != NULL)
printf("Found printf at: %p\n", sym);
return 0;
}
Compile and run:
$ gcc -o rtld_default rtld_default.c -ldl
$ ./rtld_default
Found printf at: 0x7f8a3c2b1234
2.2 RTLD_NEXT — Wrapping / Interposing a Function
The classic use of RTLD_NEXT is to wrap a standard library function. Your wrapper has the same name; it does its extra work, then calls the real implementation via RTLD_NEXT.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
/* Custom malloc: log every allocation, then call the real malloc */
void *malloc(size_t size)
{
static void *(*real_malloc)(size_t) = NULL;
if (real_malloc == NULL) {
/* One-time initialisation: find the real malloc */
dlerror();
*(void **) (&real_malloc) = dlsym(RTLD_NEXT, "malloc");
if (dlerror() != NULL) {
fprintf(stderr, "Cannot locate real malloc!\n");
return NULL;
}
}
fprintf(stderr, "[TRACE] malloc(%zu)\n", size);
return real_malloc(size); /* delegate to real implementation */
}
/* Build as a preload library */
$ gcc -shared -fPIC -o libmymalloc.so mymalloc.c -ldl
/* Preload it so every malloc goes through our wrapper */
$ LD_PRELOAD=./libmymalloc.so ls /tmp
[TRACE] malloc(552)
[TRACE] malloc(120)
...
3. Complete Example: dynload Program
Listing 42-1 from TLPI demonstrates a minimal but complete use of the dlopen API. The program accepts a shared library path and a function name on the command line, dynamically loads the library, resolves the function, calls it, then unloads the library cleanly.
/* shlibs/dynload.c */
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
void *libHandle; /* Handle for the shared library */
void (*funcp)(void); /* Pointer: function taking no args */
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);
}
/* ── Step 1: Load the shared library ── */
libHandle = dlopen(argv[1], RTLD_LAZY);
if (libHandle == NULL) {
fprintf(stderr, "dlopen: %s\n", dlerror());
exit(EXIT_FAILURE);
}
/* ── Step 2: Look up the named symbol ── */
dlerror(); /* clear prior errors */
*(void **) (&funcp) = dlsym(libHandle, argv[2]);
err = dlerror();
if (err != NULL) {
fprintf(stderr, "dlsym: %s\n", err);
exit(EXIT_FAILURE);
}
/* ── Step 3: Call or report ── */
if (funcp == NULL)
printf("%s is NULL\n", argv[2]);
else
(*funcp)(); /* call the function */
/* ── Step 4: Unload ── */
dlclose(libHandle);
exit(EXIT_SUCCESS);
}
Build and run:
$ gcc -o dynload dynload.c -ldl
/* Using a relative library path (slash present → relative pathname) */
$ ./dynload ./libdemo.so.1 x1
Called mod1-x1
/* Using LD_LIBRARY_PATH to set search path, library name only */
$ LD_LIBRARY_PATH=. ./dynload libdemo.so.1 x2
Called mod1-x2
| Step | Call | What Happens |
|---|---|---|
| 1 | dlopen(argv[1], RTLD_LAZY) | Library mapped into process; symbol table loaded; dependency libs opened. Returns opaque handle. |
| 2 | dlerror(); dlsym(handle, argv[2]) | Symbol table searched; virtual address of argv[2] returned (or NULL). |
| 3 | (*funcp)() | The resolved function executed in the calling process. |
| 4 | dlclose(handle) | Reference count decremented; library unmapped when count hits zero. |
4. dlclose() — Unloading a Shared Library
#include <dlfcn.h>
int dlclose(void *handle); Returns: 0 on success, -1 on error
dlclose() decrements an internal reference counter maintained by the dynamic linker for the library identified by handle. The library is only actually unmapped from memory when:
- Its reference count drops to zero, and
- No other currently loaded library depends on it (dependency-tree check, applied recursively).
| Event | Reference Count Change | Library State |
|---|---|---|
| First dlopen() | 0 → 1 | Loaded and mapped |
| Second dlopen() (same lib) | 1 → 2 | Stays loaded; same handle |
| First dlclose() | 2 → 1 | Still loaded |
| Second dlclose() | 1 → 0 | Unmapped (if no dependents) |
| Process exit (implicit) | — | All libraries unloaded |
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
void *h1 = dlopen("./libfoo.so", RTLD_LAZY); /* refcount = 1 */
void *h2 = dlopen("./libfoo.so", RTLD_LAZY); /* refcount = 2 */
/* h1 and h2 are the same handle value (same library) */
dlclose(h1); /* refcount = 1 — library still loaded */
dlclose(h2); /* refcount = 0 — library now unmapped */
return 0;
}
5. dladdr() — Introspecting a Symbol Address
dladdr() is the reverse lookup function: given a virtual address (typically one obtained from dlsym() or a function pointer), it returns metadata about the symbol and the shared library that contains it.
#define _GNU_SOURCE#include <dlfcn.h>
int dladdr(const void *addr, Dl_info *info); Returns: non-zero if addr found in a shared library, 0 otherwise
Note: NOT available on all UNIX implementations; not standardised by SUSv3
The caller provides a Dl_info structure (allocated on the stack or heap); dladdr() fills it in:
typedef struct {
const char *dli_fname; /* Pathname of shared library containing addr */
void *dli_fbase; /* Runtime base address of that library */
const char *dli_sname; /* Name of nearest symbol with address ≤ addr */
void *dli_saddr; /* Exact address of the symbol in dli_sname */
} Dl_info;
| Field | Type | Meaning | Notes |
|---|---|---|---|
| dli_fname | const char * | Full pathname of the shared library that contains addr | Empty string for the main executable |
| dli_fbase | void * | Address at which that library is loaded (ASLR base) | Useful for offset calculations |
| dli_sname | const char * | Name of the nearest exported symbol with address ≤ addr | May be NULL if no exported symbol found |
| dli_saddr | void * | Actual address of the symbol named in dli_sname | Equals addr when addr is an exact symbol address |
If addr points to the exact start of a symbol, dli_saddr == addr. If addr points inside a function body (e.g., a return address from a stack frame), dli_sname gives the enclosing function’s name and dli_saddr its start — useful for stack-trace generation.
5.1 dladdr() — Working Example
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <math.h>
int main(void)
{
Dl_info info;
void (*fp)(void);
/* Look up sin() from the math library */
dlerror();
*(void **) (&fp) = dlsym(RTLD_DEFAULT, "sin");
if (dlerror() != NULL) {
fprintf(stderr, "sin not found\n");
return 1;
}
/* Reverse-lookup: which library and symbol does this address belong to? */
if (dladdr(fp, &info)) {
printf("Library : %s\n", info.dli_fname);
printf("Load base: %p\n", info.dli_fbase);
printf("Symbol : %s\n", info.dli_sname);
printf("Sym addr : %p\n", info.dli_saddr);
} else {
printf("Address not found in any shared library\n");
}
return 0;
}
$ gcc -o dladdr_demo dladdr_demo.c -ldl -lm
$ ./dladdr_demo
Library : /lib/x86_64-linux-gnu/libm.so.6
Load base: 0x7f3a1c000000
Symbol : sin
Sym addr : 0x7f3a1c045600
6. Common Mistakes and Best Practices
| Mistake | Consequence | Fix |
|---|---|---|
| Direct funcp = dlsym(…) assignment | C99 compiler warning; may silently truncate on platforms where sizeof(void *)≠sizeof(fptr) | Use *(void **)(&funcp) = dlsym(…) |
| Forgetting dlerror() before dlsym() | Stale error string from a previous call may be mistaken for a new error | Always call dlerror() immediately before dlsym() |
| Using pointers after dlclose() | Undefined behaviour / segfault — library is unmapped | NULL all cached pointers before or after dlclose() |
| Assuming one dlclose() unloads immediately | Library stays loaded until all reference counts are zero | Understand reference counting; call dlclose() once per dlopen() |
| Using RTLD_NEXT from the main executable | Search starts after the main program — may miss the intended symbol | Use RTLD_NEXT only from within a shared library / LD_PRELOAD interposer |
| Omitting -ldl at link time | Linker error: undefined references to dlopen, dlsym, etc. | Always link with -ldl when using the dlopen API |
7. Interview Questions & Answers
dlsym()‘s return value to a function pointer, and what is the correct idiom?dlsym() returns NULL. How do you determine whether the symbol was not found or whether its value is legitimately NULL?dlclose() always immediately unload the library?dladdr() provide, and name two practical use cases?atexit(). When is that function called if the library is loaded with dlopen() and later unloaded with dlclose()?