Question

What Is the LD_PRELOAD Trick in Linux and C?

clinuxenvironment-variables

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_PRELOAD changes that search order
Take Quiz

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:

  1. The shell starts the program with LD_PRELOAD=./libmalloc_log.so.
  2. The dynamic linker loads libmalloc_log.so before the standard libraries.
  3. The target program calls malloc(32).
  4. Symbol resolution finds your malloc first, because its library was preloaded.
  5. Inside your malloc, real_malloc is checked.
  6. On the first call, dlsym(RTLD_NEXT, "malloc") finds the original allocator in the next library.
  7. Your wrapper calls the real allocator.
  8. The real allocator returns a memory address.
  9. Your wrapper logs the allocation.
  10. 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.

Take Quiz

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:

  • malloc calls 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:

  • malloc
  • free
  • calloc
  • realloc

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

ConceptWhat it doesBest useLimitation
LD_PRELOADLoads a library first so its symbols can override othersRuntime interception, debugging, allocator replacementMostly for dynamically linked programs
Recompiling the programChanges the source and rebuilds the binaryPermanent fixes and clean integrationRequires source code and build access
Static linkingBundles library code into the executableSelf-contained binariesHarder to override at runtime
Debugger breakpointsInspects behavior while runningInteractive debuggingLess convenient for always-on interception
straceTraces system callsObserving kernel-facing behaviorDoes not replace normal user-space library functions
Wrapper scriptChanges how a program is launchedPassing environment variables or flagsCannot itself override library symbols

LD_PRELOAD vs changing PATH

These are different ideas:

  • PATH changes which executable gets run.
  • LD_PRELOAD changes which shared library implementation gets used inside a running process.

LD_PRELOAD vs strace

  • strace observes system calls like open, read, and write at the kernel boundary.
  • LD_PRELOAD can 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_PRELOAD is faster for experiments and third-party binaries
Take Quiz

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_PRELOAD is 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.

Take Quiz

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

  1. Write a C shared library that defines a replacement puts() function.
  2. Compile the library with position-independent code and shared library flags.
  3. Create a small test program that calls puts().
  4. Run the test program normally and then again with LD_PRELOAD set.
  5. Observe how the output changes when the library is preloaded.
Take Quiz

Keep learning

Related questions

View more