Loading content...
Consider a web browser. It supports dozens of media formats: JPEG, PNG, WebP, MP4, WebM, PDF, and countless others. Each format requires specialized decoding code. But what if you never browse sites with PDF files? Why should your browser load PDF decoding code at startup?
Dynamic loading solves this problem. Instead of loading all code at program startup, programs can load code on demand, at runtime. The PDF decoder loads only when you first encounter a PDF. A camera app loads the portrait mode AI only when you select that feature. A game loads levels and assets only as needed.
Dynamic loading is the ultimate expression of flexible address binding. Not only can code load at any address (thanks to execution-time binding and PIC), but it can load at any time—or not at all. This flexibility enables plugin architectures, modular software design, and efficient resource utilization.
By the end of this page, you will understand the distinction between dynamic linking and dynamic loading, how runtime loading APIs work (dlopen, LoadLibrary), how symbols are resolved at runtime, plugin architecture design, and the tradeoffs of dynamic versus static loading.
These terms are often confused, but they represent different concepts:
Dynamic Linking:
Dynamic Loading:
| Aspect | Dynamic Linking | Dynamic Loading |
|---|---|---|
| When libraries specified | Compile/link time | Runtime (code decides) |
| When libraries loaded | Program startup | When program requests |
| Loader responsible | Dynamic linker (ld-linux.so) | Application code (via dlopen) |
| Library must exist at startup | Yes (program fails otherwise) | No (load on demand) |
| Symbol resolution | Automatic (via PLT/GOT) | Manual (dlsym, GetProcAddress) |
| Use case | Standard shared libraries | Plugins, optional features |
The spectrum of loading strategies:
Static Linking ──────▶ Dynamic Linking ──────▶ Dynamic Loading
│ │ │
│ All code embedded │ Shared libs at │ Load code
│ in executable │ startup │ on demand
│ │ │
│ No runtime │ Required libs │ Full runtime
│ dependencies │ must exist │ flexibility
│ │ │
│ Largest binary │ Smaller binary │ Smallest initial
│ │ │ footprint
Most modern programs use a combination: critical libraries are dynamically linked (loaded at startup), while optional features use dynamic loading.
Operating systems provide APIs for programs to load libraries at runtime. The primary APIs are:
POSIX (Linux, macOS, Unix): dlopen, dlsym, dlclose, dlerror
Windows: LoadLibrary, GetProcAddress, FreeLibrary, GetLastError
These APIs follow a similar pattern:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
#include <stdio.h>#include <stdlib.h>#include <dlfcn.h> // Dynamic loading header // Define the function signature we expect from the plugintypedef int (*math_operation_t)(int, int); int main(int argc, char *argv[]) { void *handle; math_operation_t operation; char *error; // ══════════════════════════════════════════════════════════════════ // STEP 1: Load the shared library at runtime // ══════════════════════════════════════════════════════════════════ // dlopen() loads a shared library into the process // RTLD_LAZY: Resolve symbols lazily (on first use) // RTLD_NOW: Resolve all symbols immediately (catches errors early) handle = dlopen("./libmathplugin.so", RTLD_LAZY); if (!handle) { fprintf(stderr, "Failed to load library: %s\n", dlerror()); return EXIT_FAILURE; } // ══════════════════════════════════════════════════════════════════ // STEP 2: Look up a function by name // ══════════════════════════════════════════════════════════════════ // Clear any existing error dlerror(); // dlsym() finds a symbol (function or variable) in the loaded library operation = (math_operation_t)dlsym(handle, "add"); // Check for errors (dlsym returns NULL for missing symbols, // but NULL might be a valid address, so check dlerror too) error = dlerror(); if (error != NULL) { fprintf(stderr, "Failed to find symbol: %s\n", error); dlclose(handle); return EXIT_FAILURE; } // ══════════════════════════════════════════════════════════════════ // STEP 3: Use the function! // ══════════════════════════════════════════════════════════════════ int result = operation(10, 5); printf("Result: %d\n", result); // Output: Result: 15 // Can load other functions from the same library operation = (math_operation_t)dlsym(handle, "multiply"); if ((error = dlerror()) == NULL) { printf("Multiply result: %d\n", operation(10, 5)); // Output: 50 } // ══════════════════════════════════════════════════════════════════ // STEP 4: Unload the library (optional, freed on process exit anyway) // ══════════════════════════════════════════════════════════════════ dlclose(handle); return EXIT_SUCCESS;}When you call dlopen(), a complex sequence of operations occurs to make the library's code available:
The dlopen process:
DLOPEN FLAGS REFERENCE═══════════════════════════════════════════════════════════════════════════════ SYMBOL RESOLUTION FLAGS (mutually exclusive): RTLD_LAZY - Resolve symbols when they're actually used (lazy binding) - Faster library loading - Errors discovered when undefined symbol is called - Default behavior on many systems RTLD_NOW - Resolve ALL symbols immediately when library loads - Slower loading, but catches missing symbols early - Required for RTLD_GLOBAL to work reliably - Use when: security-critical, need all-or-nothing loading ───────────────────────────────────────────────────────────────────────────────── SYMBOL VISIBILITY FLAGS (can be combined with above): RTLD_GLOBAL - Loaded library's symbols available for subsequently loaded libraries - Affects library dependency resolution - Example: Load A with RTLD_GLOBAL, then load B that uses A's symbols RTLD_LOCAL (default) - Symbols in loaded library only visible to that library - Does NOT affect other dlopen calls - More encapsulated, prevents symbol pollution ───────────────────────────────────────────────────────────────────────────────── SPECIAL FLAGS: RTLD_NOLOAD - Don't actually load the library - Just check if it's already loaded - Returns existing handle or NULL RTLD_NODELETE - Library won't be unloaded even when refcount hits 0 - Use for libraries with complex cleanup issues RTLD_DEEPBIND - Library prefers its own symbols over global ones - Prevents symbol interposition for this library ───────────────────────────────────────────────────────────────────────────────── USAGE EXAMPLES: // Plugin that loads quickly, tolerates lazy errorsvoid *h = dlopen("plugin.so", RTLD_LAZY); // Critical library, verify all symbols existvoid *h = dlopen("security.so", RTLD_NOW); // Library whose symbols other plugins needvoid *h = dlopen("framework.so", RTLD_NOW | RTLD_GLOBAL);The dlsym() function looks up symbols by name. This is fundamentally different from compile-time linking where the linker resolves symbols:
At compile time: The linker records that your code needs symbol "foo" and produces relocation entries. The dynamic linker resolves "foo" to an address at load time.
At runtime: Your code executes dlsym(handle, "foo") and gets back a pointer you can call. No compiler/linker involvement—pure runtime resolution.
DLSYM SYMBOL SEARCH BEHAVIOR═══════════════════════════════════════════════════════════════════════════════ dlsym(handle, "symbol_name") The behavior depends on the 'handle' value: ─────────────────────────────────────────────────────────────────────────────────handle = dlopen() return value───────────────────────────────────────────────────────────────────────────────── Search: Only in the specified library and its dependencies Example: void *h = dlopen("libA.so", RTLD_LAZY); // libA depends on libB dlsym(h, "funcA") → Found in libA ✓ dlsym(h, "funcB") → Found in libB (dependency of A) ✓ dlsym(h, "funcC") → NOT found (libC not a dependency) ✗ ─────────────────────────────────────────────────────────────────────────────────handle = RTLD_DEFAULT───────────────────────────────────────────────────────────────────────────────── Search: All loaded objects, in load order (like normal symbol resolution) Example: dlsym(RTLD_DEFAULT, "printf") → Found in libc ✓ dlsym(RTLD_DEFAULT, "main") → Found in main executable ✓ Use case: Find a symbol without knowing which library provides it ─────────────────────────────────────────────────────────────────────────────────handle = RTLD_NEXT───────────────────────────────────────────────────────────────────────────────── Search: Start searching AFTER the current library Use case: Function interposition (wrapping functions) Example (in an interposition library): // Wrap malloc to add logging void *malloc(size_t size) { // Get the "real" malloc from the next library in search order static void *(*real_malloc)(size_t) = NULL; if (!real_malloc) { real_malloc = dlsym(RTLD_NEXT, "malloc"); } printf("malloc called with size %zu\n", size); return real_malloc(size); // Call the real malloc }Libraries often have multiple versions of the same function for backward compatibility (e.g., glibc's symbol versioning). dlsym() returns the default version, but dlvsym() can request a specific version: dlvsym(handle, "symbol", "VERSION_1.0"). This enables fine-grained control over which implementation you get.
Dynamic loading enables plugin architectures—systems where functionality can be added, removed, or updated without modifying or recompiling the main application. Well-designed plugins are the foundation of extensible software.
Common plugin architecture patterns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
/* ══════════════════════════════════════════════════════════════════ * HOST APPLICATION - plugin_host.h * Defines the plugin interface that all plugins must implement * ══════════════════════════════════════════════════════════════════ */ #ifndef PLUGIN_HOST_H#define PLUGIN_HOST_H #define PLUGIN_API_VERSION 1 // Plugin metadata structure - every plugin must provide thistypedef struct { int api_version; // Must match PLUGIN_API_VERSION const char *name; // Human-readable plugin name const char *version; // Plugin version string const char *author; // Plugin author} plugin_info_t; // Plugin interface - functions every plugin must implementtypedef struct { // Required: Get plugin information plugin_info_t* (*get_info)(void); // Required: Initialize the plugin int (*init)(void); // Required: Cleanup and shutdown void (*cleanup)(void); // Optional: Process data (or whatever the host needs) int (*process)(const void *input, void *output);} plugin_interface_t; // The function name every plugin must export#define PLUGIN_ENTRY_SYMBOL "plugin_entry" // Plugin entry function type - returns the interfacetypedef plugin_interface_t* (*plugin_entry_fn)(void); #endif /* PLUGIN_HOST_H */ /* ══════════════════════════════════════════════════════════════════ * HOST APPLICATION - plugin_loader.c * Loads and manages plugins * ══════════════════════════════════════════════════════════════════ */ #include <stdio.h>#include <dirent.h>#include <dlfcn.h>#include "plugin_host.h" #define MAX_PLUGINS 100 typedef struct { void *handle; plugin_interface_t *interface;} loaded_plugin_t; loaded_plugin_t plugins[MAX_PLUGINS];int plugin_count = 0; int load_plugin(const char *path) { // 1. Load the shared library void *handle = dlopen(path, RTLD_NOW); if (!handle) { fprintf(stderr, "Failed to load %s: %s\n", path, dlerror()); return -1; } // 2. Find the entry point plugin_entry_fn entry = dlsym(handle, PLUGIN_ENTRY_SYMBOL); if (!entry) { fprintf(stderr, "No entry symbol in %s\n", path); dlclose(handle); return -1; } // 3. Get the plugin interface plugin_interface_t *iface = entry(); if (!iface) { fprintf(stderr, "Plugin entry returned NULL\n"); dlclose(handle); return -1; } // 4. Verify API version compatibility plugin_info_t *info = iface->get_info(); if (info->api_version != PLUGIN_API_VERSION) { fprintf(stderr, "Plugin API version mismatch\n"); dlclose(handle); return -1; } // 5. Initialize the plugin if (iface->init() != 0) { fprintf(stderr, "Plugin init failed\n"); dlclose(handle); return -1; } // 6. Store the plugin plugins[plugin_count].handle = handle; plugins[plugin_count].interface = iface; plugin_count++; printf("Loaded plugin: %s v%s by %s\n", info->name, info->version, info->author); return 0;}Dynamically loaded libraries can execute code automatically when loaded or unloaded. These are called constructors and destructors (not to be confused with C++ class constructors/destructors).
Constructors: Run when the library is loaded (dlopen or program startup) Destructors: Run when the library is unloaded (dlclose or program exit)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
/* Library with constructor and destructor */ #include <stdio.h> // GCC/Clang constructor attribute__attribute__((constructor))void library_init(void) { printf("Library loaded! Performing initialization...\n"); // Initialize resources, register handlers, etc.} // Multiple constructors with priorities (lower = earlier)__attribute__((constructor(101)))void early_init(void) { printf("Early initialization (priority 101)\n");} __attribute__((constructor(102)))void late_init(void) { printf("Late initialization (priority 102)\n");} // GCC/Clang destructor attribute__attribute__((destructor))void library_cleanup(void) { printf("Library unloading! Cleaning up...\n"); // Free resources, unregister handlers, etc.} /* Execution order: * * On dlopen("libexample.so") or program start: * 1. early_init() (priority 101) * 2. late_init() (priority 102) * 3. library_init() (default priority 65535) * * On dlclose() or program exit: * 1. library_cleanup() * 2. late_init (destructor with priority 102) * 3. early_init (destructor with priority 101) * (reversed order from constructors) */ /* Alternative: .init_array and .fini_array sections * * The compiler generates these from constructor/destructor attributes, * but you can also add pointers directly: */ void my_init_func(void) { printf("Init via .init_array\n"); }void my_fini_func(void) { printf("Fini via .fini_array\n"); } // Place pointers in the appropriate sections__attribute__((section(".init_array")))static void (*init_ptr)(void) = my_init_func; __attribute__((section(".fini_array")))static void (*fini_ptr)(void) = my_fini_func;Library constructors run before main() and before the program can handle errors. Keep constructor code minimal and robust. Avoid:
Dynamic loading is everywhere in modern software. Understanding these use cases helps you recognize when dynamic loading is the right solution.
Case study: Python's extension modules
Python uses dynamic loading extensively. When you import numpy, Python:
This is why C extensions can crash your Python interpreter—they run native code loaded via dlopen.
| Use Dynamic Loading When | Use Static/Startup Linking When |
|---|---|
| Functionality is optional/rarely used | Functionality is always needed |
| Third-party plugins/extensions expected | Closed, self-contained application |
| Memory efficiency is critical | Startup time is critical (no runtime loading) |
| Hot-swapping code at runtime needed | Reliability/stability paramount |
| Many possible implementations (drivers) | Single, known implementation |
Dynamic loading is powerful but fraught with potential issues. These best practices help you avoid common problems:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
/* Robust plugin loading with proper error handling */ #include <dlfcn.h>#include <stdio.h>#include <stdlib.h> typedef struct plugin_handle { void *dl_handle; void *interface; int ref_count;} plugin_handle_t; plugin_handle_t* load_plugin_robust(const char *path) { // Clear any previous errors dlerror(); // Load with RTLD_NOW to catch all errors immediately void *dl_handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); if (!dl_handle) { char *err = dlerror(); fprintf(stderr, "dlopen failed for '%s': %s\n", path, err ? err : "unknown error"); return NULL; } // Look for entry point dlerror(); // Clear errors void *(*get_interface)(void) = dlsym(dl_handle, "get_interface"); char *err = dlerror(); if (err) { fprintf(stderr, "dlsym failed: %s\n", err); dlclose(dl_handle); return NULL; } // Get the interface void *interface = get_interface(); if (!interface) { fprintf(stderr, "Plugin returned NULL interface\n"); dlclose(dl_handle); return NULL; } // Verify version (assuming interface has version field) // ... version check code ... // Create our handle wrapper plugin_handle_t *handle = malloc(sizeof(plugin_handle_t)); if (!handle) { dlclose(dl_handle); return NULL; } handle->dl_handle = dl_handle; handle->interface = interface; handle->ref_count = 1; return handle;} void release_plugin(plugin_handle_t *handle) { if (!handle) return; handle->ref_count--; if (handle->ref_count == 0) { // Call cleanup if defined void (*cleanup)(void) = dlsym(handle->dl_handle, "cleanup"); if (cleanup) cleanup(); dlclose(handle->dl_handle); free(handle); }}We've explored dynamic loading—the ability to load code at runtime, on demand. Let's consolidate the key concepts:
Module Complete:
Congratulations! You've completed the Address Binding module. You now understand the complete journey of address resolution—from compile-time binding's rigid simplicity, through load-time binding's multiprogramming enablement, to execution-time binding's virtual memory foundation, and finally to dynamic loading's runtime flexibility.
These concepts underpin memory management, security (ASLR, DEP), shared libraries, plugin systems, and operating system architecture. Whether you're debugging a loader error, designing a plugin system, or optimizing memory usage, this knowledge forms the foundation of your understanding.
You now understand the complete spectrum of address binding—from source code to execution, from absolute addresses to dynamic loading. You can design plugin architectures, debug loading issues, and appreciate the sophisticated machinery that makes modern software possible. This knowledge is fundamental to systems programming, operating system development, and secure software design.