What Is LD_PRELOAD?
The dynamic linker on Linux supports an environment variable called LD_PRELOAD that lists one or more shared libraries to be loaded before all other libraries — even before libc. Because the dynamic linker resolves symbols in load order (first match wins), any function you define in a preloaded library overrides (interposes on) the same-named function in all other libraries.
This powerful mechanism lets you change the behavior of any program without modifying its source code, recompiling it, or even having root access to the system (for per-process use). It is widely used for:
- Debugging: logging all malloc/free calls to track memory usage
- Testing: replacing network calls with mock implementations
- Security research: auditing system calls or library usage
- Performance profiling: measuring time spent in specific library functions
- Bug workarounds: patching a buggy library function without recompiling
How LD_PRELOAD Works — Symbol Resolution Order
The dynamic linker searches for symbols in this order:
Since LD_PRELOAD libraries are searched first, any function they define shadows the same function in libc or any other library. The original function is still accessible via RTLD_NEXT.
Two ways to specify LD_PRELOAD:
# Per-process: set for one command only
LD_PRELOAD=./mywrapper.so ./my_program
# Per-process: set in the shell environment (affects all subsequent commands)
export LD_PRELOAD=./mywrapper.so
./my_program
unset LD_PRELOAD
# Multiple libraries: colon or space separated
LD_PRELOAD="./lib1.so:./lib2.so" ./my_program
# System-wide: affects ALL programs on the system (root access needed)
echo "/path/to/mywrapper.so" >> /etc/ld.so.preload
Code Example 1: Tracking malloc/free Calls
This is a classic LD_PRELOAD use case — intercept every malloc and free call to log them for debugging:
/* malloc_trace.c — intercepts malloc and free for debugging */
#define _GNU_SOURCE /* Required for RTLD_NEXT */
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
/* Pointers to the real malloc and free from libc */
static void *(*real_malloc)(size_t) = NULL;
static void (*real_free)(void *) = NULL;
/* Initialize pointers to real functions on first use */
static void init_real_funcs(void) {
if (!real_malloc) {
dlerror();
*(void **)(&real_malloc) = dlsym(RTLD_NEXT, "malloc");
if (dlerror()) {
fprintf(stderr, "[TRACE] Cannot find real malloc!\n");
_exit(1);
}
}
if (!real_free) {
dlerror();
*(void **)(&real_free) = dlsym(RTLD_NEXT, "free");
if (dlerror()) {
fprintf(stderr, "[TRACE] Cannot find real free!\n");
_exit(1);
}
}
}
/* Our malloc override */
void *malloc(size_t size) {
init_real_funcs();
void *ptr = real_malloc(size);
/* Use write() instead of printf to avoid malloc recursion */
fprintf(stderr, "[TRACE] malloc(%zu) = %p\n", size, ptr);
return ptr;
}
/* Our free override */
void free(void *ptr) {
init_real_funcs();
fprintf(stderr, "[TRACE] free(%p)\n", ptr);
real_free(ptr);
}
# Build as a shared library
gcc -fPIC -shared -o malloc_trace.so malloc_trace.c -ldl
# Test: run ls with malloc/free tracing
LD_PRELOAD=./malloc_trace.so ls /tmp 2>&1 | head -20
# Sample output:
# [TRACE] malloc(472) = 0x55a1b2c3d4e0
# [TRACE] malloc(120) = 0x55a1b2c3f5a0
# [TRACE] malloc(1024) = 0x55a1b2c40010
# ... (many more)
# (actual ls output appears after)
Code Example 2: Timing Wrapper for Any Function
/* time_open.c — measures how long open() takes */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <fcntl.h>
#include <time.h>
#include <stdarg.h>
static int (*real_open)(const char *, int, ...) = NULL;
static void init_real_open(void) {
if (!real_open) {
dlerror();
*(void **)(&real_open) = dlsym(RTLD_NEXT, "open");
if (dlerror()) _exit(1);
}
}
/* Override open() to measure its duration */
int open(const char *pathname, int flags, ...) {
init_real_open();
mode_t mode = 0;
if (flags & O_CREAT) {
va_list ap;
va_start(ap, flags);
mode = va_arg(ap, int);
va_end(ap);
}
struct timespec t1, t2;
clock_gettime(CLOCK_MONOTONIC, &t1);
int fd = real_open(pathname, flags, mode);
clock_gettime(CLOCK_MONOTONIC, &t2);
long ns = (t2.tv_sec - t1.tv_sec) * 1000000000L + (t2.tv_nsec - t1.tv_nsec);
fprintf(stderr, "[TIMER] open(\"%s\") took %ld ns, fd=%d\n", pathname, ns, fd);
return fd;
}
gcc -fPIC -shared -o time_open.so time_open.c -ldl
LD_PRELOAD=./time_open.so cat /etc/hostname
# Output (stderr):
# [TIMER] open("/etc/hostname") took 12453 ns, fd=3
# (then hostname is printed on stdout)
Code Example 3: Mock connect() for Unit Testing
/* mock_connect.c — replaces connect() with a mock for testing */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
static int (*real_connect)(int, const struct sockaddr *, socklen_t) = NULL;
static void init(void) {
if (!real_connect) {
dlerror();
*(void **)(&real_connect) = dlsym(RTLD_NEXT, "connect");
}
}
/*
* Mock connect(): For testing purposes, pretend every connection
* to 192.168.1.100 succeeds instantly (useful in CI/CD environments
* where the real server is not available).
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
init();
if (addr->sa_family == AF_INET) {
struct sockaddr_in *in = (struct sockaddr_in *)addr;
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &in->sin_addr, ip, sizeof(ip));
int port = ntohs(in->sin_port);
if (in->sin_addr.s_addr == inet_addr("192.168.1.100")) {
fprintf(stderr, "[MOCK] connect() to %s:%d — MOCKED SUCCESS\n", ip, port);
return 0; /* pretend success */
}
}
/* For all other addresses, call the real connect() */
return real_connect(sockfd, addr, addrlen);
}
gcc -fPIC -shared -o mock_connect.so mock_connect.c -ldl
# Run your program with the mock, without changing any source code:
LD_PRELOAD=./mock_connect.so ./my_server_program
System-Wide Preloading: /etc/ld.so.preload
/etc/ld.so.preload is a text file listing shared libraries to be preloaded for every process on the system, one path per line. Unlike LD_PRELOAD, it applies even to setuid programs (because it is set by root).
# /etc/ld.so.preload — affects ALL processes system-wide
/usr/lib/libmymalloc.so
/usr/lib/libsecurity_audit.so
| Method | Scope | Works with setuid? | Root needed? |
|---|---|---|---|
| LD_PRELOAD env var | Per-process (one command) | No — ignored | No |
| /etc/ld.so.preload | System-wide (all processes) | Yes | Yes |
