Question
I recently saw a reference to the LD_PRELOAD trick, but it was not explained clearly.
For example, I saw a comment like this:
do you know that you have allocation performance issue?
If the LD_PRELOAD trick leads to considerable performance increases you might have one. Trying it is easy.
I suspect this has something to do with replacing memory allocation behavior at runtime, but I am not sure. What exactly is the LD_PRELOAD trick, and how does it work?
Short Answer
By the end of this page, you will understand what LD_PRELOAD is on Linux, how it lets you load a shared library before others, and why that can change program behavior without recompiling. You will also see how it is used to override functions such as malloc, add logging, debug issues, and sometimes improve performance.
Concept
LD_PRELOAD is an environment variable used by the Linux dynamic linker/loader. It tells the system to load one or more shared libraries before the normal libraries a program would use.
Because those libraries are loaded first, functions inside them can be chosen instead of the usual implementations from system libraries such as libc.
Why this matters
Many programs on Linux use dynamic linking. That means functions like these are not always built directly into the executable:
malloc()
printf()
open()
read()
Instead, the program calls versions provided by shared libraries at runtime.
If you set LD_PRELOAD to your own shared library, and that library defines a function with the same name and compatible signature, your version can be used first.
Why people call it a “trick”
It feels like a trick because you can:
- change behavior without modifying source code
- inspect calls inside a program
- replace slow implementations with faster ones
- inject debugging, metrics, or validation
A classic example is replacing the default memory allocator with another allocator such as jemalloc or tcmalloc:
LD_PRELOAD=/path/to/libjemalloc.so ./my_program
If performance improves a lot, that may suggest the program spends significant time in memory allocation or suffers from allocator contention/fragmentation.
Important limitation
LD_PRELOAD mainly affects dynamically linked programs. It does not work the same way for statically linked binaries, and it may be restricted for security-sensitive programs such as setuid executables.
In simple terms
LD_PRELOAD lets you say:
“Before running this program, load my library first. If I provide versions of some functions, use mine.”
Mental Model
Imagine a program is about to enter a building and asks a receptionist where to find services like memory allocation, file access, and printing.
Normally, the receptionist points to the standard staff in the building.
LD_PRELOAD is like placing your own staff at the front desk before the normal staff arrive.
So when the program asks for malloc(), the receptionist sees your replacement first and sends the program there.
That replacement can:
- do the work itself
- log the request and then forward it
- reject the request
- optimize the behavior
So the key idea is:
- the program asks for a function name
- the dynamic linker decides which library provides it
LD_PRELOADchanges that search order
Syntax and Examples
The basic syntax is:
LD_PRELOAD=/path/to/library.so ./program
You can also preload multiple libraries:
LD_PRELOAD="/path/a.so /path/b.so" ./program
Example: overriding puts
Here is a small shared library that replaces puts:
#define _GNU_SOURCE
#include <stdio.h>
int puts(const char *s) {
return printf("[intercepted] %s\n", s);
}
Compile it as a shared library:
gcc -shared -fPIC -o libhook.so hook.c
Now run another program with it preloaded:
LD_PRELOAD=./libhook.so ./my_program
If my_program calls puts("Hello"), it may print:
[intercepted] Hello
Example: wrapping malloc
A more realistic use is intercepting malloc while still calling the original function:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
void *malloc(size_t size) {
static void *(*real_malloc)(size_t) = NULL;
if (!real_malloc) {
real_malloc = dlsym(RTLD_NEXT, "malloc");
}
void *ptr = real_malloc(size);
fprintf(stderr, "malloc(%zu) = %p\n", size, ptr);
return ptr;
}
Compile:
gcc -shared -fPIC -o libmalloc_log.so malloc_log.c -ldl
Run:
LD_PRELOAD=./libmalloc_log.so ./my_program
This logs allocation calls made by the program.
Why RTLD_NEXT is used
RTLD_NEXT tells dlsym:
“Find the next definition of this symbol after the current library.”
That is how your wrapper can call the original implementation instead of calling itself again forever.
Step by Step Execution
Consider this wrapper:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
void *malloc(size_t size) {
static void *(*real_malloc)(size_t) = NULL;
if (!real_malloc) {
real_malloc = dlsym(RTLD_NEXT, "malloc");
}
void *ptr = real_malloc(size);
fprintf(stderr, "allocated %zu bytes at %p\n", size, ptr);
return ptr;
}
Suppose the target program runs this code:
char *buffer = malloc(32);
Here is what happens step by step:
- The shell starts the program with
LD_PRELOAD=./libmalloc_log.so. - The dynamic linker loads
libmalloc_log.sobefore the standard libraries. - The target program calls
malloc(32). - Symbol resolution finds your
mallocfirst, because its library was preloaded. - Inside your
malloc,real_mallocis checked. - On the first call,
dlsym(RTLD_NEXT, "malloc")finds the original allocator in the next library. - Your wrapper calls the real allocator.
- The real allocator returns a memory address.
- Your wrapper logs the allocation.
- Your wrapper returns the pointer to the program.
So the target program still gets allocated memory, but your library has inserted itself into the process.
Real World Use Cases
LD_PRELOAD is useful in several practical situations.
1. Testing another memory allocator
A team suspects that allocation is a bottleneck in a multithreaded C or C++ program. They try:
LD_PRELOAD=/usr/lib/libjemalloc.so ./server
If latency or throughput improves, that points to allocator-related performance issues.
2. Logging file access
A developer wants to know which files a program opens. They wrap open() and log every path.
3. Debugging library calls
A bug only happens in production. Without changing the app code, a developer injects a wrapper around read(), write(), or connect() to capture parameters.
4. Temporary hotfixes
If a third-party binary uses a problematic library function in a known way, a wrapper can sometimes patch behavior until a proper fix is deployed.
5. Enforcing policy
An organization may preload a library that blocks dangerous calls or adds audit logging.
6. Fault injection
During testing, a wrapper can randomly fail malloc() or file operations to ensure the program handles errors correctly.
Real Codebase Usage
In real projects, LD_PRELOAD is usually not the main application architecture. It is more often a tooling or systems technique.
Common patterns
Guarded instrumentation
Teams build a shared library that wraps functions only for debugging environments:
LD_PRELOAD=./libdebug_hooks.so ./app
This avoids changing production code paths permanently.
Performance experiments
Instead of rewriting memory management immediately, developers first test whether a different allocator changes performance.
That is why comments like this appear:
“If the LD_PRELOAD trick helps, you may have an allocation performance issue.”
The idea is not that LD_PRELOAD itself is faster. The idea is that the replacement library may be faster or more scalable.
Validation and error detection
A preloaded library may:
- detect invalid frees
- count allocations
- track leaks
- verify API usage
Wrappers with early-return checks
A wrapper might validate arguments before forwarding the call:
if (size == 0) {
return NULL;
}
Configuration-based behavior
Some codebases enable hooks only when an environment variable is set, making it easy to turn instrumentation on or off.
In large systems
Developers often prefer dedicated tools when available, but LD_PRELOAD remains valuable when:
- source code is unavailable
- recompiling is inconvenient
- behavior must be changed only at runtime
- quick experiments are needed
Common Mistakes
1. Forgetting that function signatures must match
If your replacement function does not exactly match the original signature, behavior is undefined or broken.
Broken example:
int malloc(int size) {
return 0;
}
Correct:
void *malloc(size_t size) {
return NULL;
}
2. Causing infinite recursion
If your replacement malloc calls malloc again, it will call itself repeatedly.
Broken example:
void *malloc(size_t size) {
printf("allocating %zu\n", size);
return malloc(size);
}
Why it breaks:
malloccalls itself- recursion never ends
Use dlsym(RTLD_NEXT, "malloc") to call the real one.
3. Using functions inside hooks that also allocate memory
Even logging can be risky. Some library functions may internally call malloc, which can accidentally recurse.
Be careful when wrapping low-level functions like:
mallocfreecallocrealloc
4. Expecting it to work for static binaries
If a program is statically linked, LD_PRELOAD usually cannot override its internal function copies.
5. Ignoring security restrictions
Some privileged executables ignore LD_PRELOAD for security reasons.
6. Assuming any speedup means the original code is bad
A faster allocator may help because of workload patterns, thread contention, fragmentation, or allocation size distribution. It does not automatically mean the existing code is poorly written.
7. Forgetting thread-safety
If your wrapper stores shared state, make sure it behaves correctly in multithreaded programs.
Comparisons
| Concept | What it does | Best use | Limitation |
|---|---|---|---|
LD_PRELOAD | Loads a library first so its symbols can override others | Runtime interception, debugging, allocator replacement | Mostly for dynamically linked programs |
| Recompiling the program | Changes the source and rebuilds the binary | Permanent fixes and clean integration | Requires source code and build access |
| Static linking | Bundles library code into the executable | Self-contained binaries | Harder to override at runtime |
| Debugger breakpoints | Inspects behavior while running | Interactive debugging | Less convenient for always-on interception |
strace | Traces system calls | Observing kernel-facing behavior | Does not replace normal user-space library functions |
| Wrapper script | Changes how a program is launched | Passing environment variables or flags | Cannot itself override library symbols |
LD_PRELOAD vs changing PATH
These are different ideas:
PATHchanges which executable gets run.LD_PRELOADchanges which shared library implementation gets used inside a running process.
LD_PRELOAD vs strace
straceobserves system calls likeopen,read, andwriteat the kernel boundary.LD_PRELOADcan intercept higher-level library calls before they become system calls.
LD_PRELOAD vs custom allocators linked at build time
- build-time linking is cleaner and more explicit
LD_PRELOADis faster for experiments and third-party binaries
Cheat Sheet
# Preload one library
LD_PRELOAD=/path/to/libhook.so ./program
# Preload multiple libraries
LD_PRELOAD="/path/a.so /path/b.so" ./program
Key points
LD_PRELOADis an environment variable used by the dynamic linker.- It loads your shared library before normal libraries.
- Your library can override dynamically linked functions.
- Common uses: logging, debugging, allocator replacement, testing.
- Usually works with dynamically linked executables.
- Often restricted for security-sensitive binaries.
Common pattern for wrapping a function
#define _GNU_SOURCE
#include <dlfcn.h>
static return_type (*real_func)(args...) = NULL;
return_type func(args...) {
if (!real_func) {
real_func = dlsym(RTLD_NEXT, "func");
}
// custom logic
return real_func(...);
}
Watch out for
- exact function signatures
- recursion
- thread-safety
- functions that allocate inside allocation hooks
- static binaries
Performance interpretation
If preloading a different allocator improves speed:
- allocation may be a bottleneck
- allocator contention may exist
- fragmentation behavior may matter
- the application may benefit from fewer/smarter allocations
FAQ
What does LD_PRELOAD do in Linux?
It tells the dynamic linker to load specific shared libraries before the normal ones, allowing their functions to override standard implementations.
Is LD_PRELOAD only for memory allocation?
No. It can override many dynamically linked functions, such as file, network, and output functions, not just malloc.
Why would changing the allocator improve performance?
Different allocators perform differently under different workloads, especially with many threads or many small allocations.
Does LD_PRELOAD work on statically linked binaries?
Usually no. Static binaries include their own code directly, so there is no dynamic symbol lookup to override in the same way.
Is LD_PRELOAD safe to use in production?
It can be useful, but it should be used carefully. A bad wrapper can cause crashes, recursion, or hard-to-debug behavior.
What is RTLD_NEXT used for?
It finds the next matching symbol after the current shared library, which is how wrappers call the original function.
Why do privileged programs ignore LD_PRELOAD?
Because allowing arbitrary library injection into privileged processes would be a major security risk.
Is the “LD_PRELOAD trick” itself a performance optimization?
Not directly. The improvement comes from whatever library you preload, such as a faster allocator or instrumentation library.
Mini Project
Description
Build a small shared library that intercepts puts() and adds a prefix to every printed line. This project demonstrates the core idea behind LD_PRELOAD: changing a program’s behavior at runtime without editing or recompiling the target program.
Goal
Create a preloadable shared library in C and use it to override puts() in another dynamically linked program.
Requirements
- Write a C shared library that defines a replacement
puts()function. - Compile the library with position-independent code and shared library flags.
- Create a small test program that calls
puts(). - Run the test program normally and then again with
LD_PRELOADset. - Observe how the output changes when the library is preloaded.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.