Program Library HOWTO: static and shared libraries on Linux

Updated November 2026

A static library (libfoo.a) is copied into each program when you link it. A shared library (libfoo.so.1) stays a separate file that the dynamic loader finds and maps in when the program starts. This page builds both from the same source, shows how the loader searches for shared libraries, and how to load plugins at run time with dlopen.

The classic version of this HOWTO is kept by the Linux Documentation Project at tldp.org. The basics it described still hold; what changed is mostly the tooling around them (pkg-config, RUNPATH, glibc 2.34 merging libdl into the C library).

The example library

/* greet.h */
#ifndef GREET_H
#define GREET_H

int greet(const char *name);

#endif
/* greet.c */
#include <stdio.h>
#include "greet.h"

int greet(const char *name)
{
    return printf("Hello, %s!\n", name);
}
/* main.c */
#include "greet.h"

int main(void)
{
    greet("library");
    return 0;
}

Static libraries

A static library is an archive of object files made with ar:

gcc -c -O2 -Wall greet.c -o greet.o
ar rcs libgreet.a greet.o          # r: insert, c: create, s: write an index
gcc main.c -L. -lgreet -o main-static
./main-static                      # Hello, library!

-lgreet means "look for libgreet.so or libgreet.a", and -L. adds the current directory to the search. List an archive's members with ar t libgreet.a and its symbols with nm libgreet.a. Put -l options after the files that use them: the linker reads left to right.

Static linking makes one self-contained program, but every program carries its own copy, and a security fix in the library means relinking all of them.

Shared libraries and sonames

A shared library has three names:

Real name: libgreet.so.1.0.0
The actual file, with the full version.
Soname: libgreet.so.1
Stored inside the library. Programs record this name and look for it at run time. Change the number only when you break compatibility.
Linker name: libgreet.so
A symlink used only when building, so that -lgreet finds the library.
gcc -c -O2 -Wall -fPIC greet.c -o greet.pic.o
gcc -shared -Wl,-soname,libgreet.so.1 -o libgreet.so.1.0.0 greet.pic.o
ln -sf libgreet.so.1.0.0 libgreet.so.1
ln -sf libgreet.so.1 libgreet.so
gcc main.c -L. -lgreet -o main

-fPIC produces position-independent code, which can be loaded at any address. -Wl,-soname,… passes the soname to the linker. Check both ends:

$ readelf -d libgreet.so.1.0.0 | grep SONAME
 0x000000000000000e (SONAME)             Library soname: [libgreet.so.1]
$ readelf -d main | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libgreet.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

Now run it:

$ ./main
./main: error while loading shared libraries: libgreet.so.1: cannot open shared object file: No such file or directory

The program built, but the loader does not look in the current directory. The next sections show three ways to fix that.

How the loader finds libraries

The dynamic loader (ld.so, on x86-64 /lib64/ld-linux-x86-64.so.2) searches for each needed soname in this order:

1. DT_RPATH in the program (only if it has no DT_RUNPATH) 2. LD_LIBRARY_PATH (ignored for setuid programs) 3. DT_RUNPATH in the program or library 4. /etc/ld.so.cache (built by ldconfig from /etc/ld.so.conf) 5. Default directories: /lib, /usr/lib (or /lib64, /usr/lib64)
The first match wins. A NEEDED entry that contains a slash is loaded from that path directly.

Fix 1: install it and run ldconfig

For a library that other programs will use, install it into a system directory and update the cache:

sudo install -m 755 libgreet.so.1.0.0 /usr/local/lib/
sudo ln -sf libgreet.so.1 /usr/local/lib/libgreet.so
sudo install -m 644 greet.h /usr/local/include/
sudo ldconfig
ldconfig -p | grep greet

ldconfig reads the soname from each library, creates the libgreet.so.1 symlink and rebuilds /etc/ld.so.cache. Debian and Ubuntu list /usr/local/lib in /etc/ld.so.conf.d/libc.conf. Check your system with grep -r /usr/local /etc/ld.so.conf /etc/ld.so.conf.d/; if nothing is listed, add a file such as /etc/ld.so.conf.d/local.conf containing /usr/local/lib and run sudo ldconfig again. Files installed by your package manager should stay in /usr/lib; keep your own in /usr/local.

Fix 2: LD_LIBRARY_PATH, for testing

LD_LIBRARY_PATH=. ./main

Good for a quick test or a test suite. Do not set it globally in your shell profile: it affects every program you start, and it is ignored for set-user-ID programs anyway.

Fix 3: rpath with $ORIGIN, for bundled applications

An application shipped in its own directory tree can record where its libraries are, relative to the executable. $ORIGIN means "the directory this file is in"; the single quotes stop the shell from expanding it:

mkdir -p app/bin app/lib
cp -P libgreet.so* app/lib/
gcc main.c -L. -lgreet -Wl,-rpath,'$ORIGIN/../lib' -o app/bin/main
readelf -d app/bin/main | grep -E 'RPATH|RUNPATH'
app/bin/main

Current toolchains usually write a RUNPATH entry; with -Wl,--disable-new-dtags you get the older RPATH. The difference: LD_LIBRARY_PATH overrides RUNPATH but not RPATH, and RUNPATH applies only to the file's own direct dependencies, while RPATH applies to the whole tree. Never put a relative path without $ORIGIN, or a world-writable directory, into an rpath.

Inspecting dependencies

ldd ./app/bin/main                     # full tree, resolved paths
objdump -p ./main | grep NEEDED        # direct dependencies only
LD_DEBUG=libs ./main                   # watch the loader search
nm -D --defined-only libgreet.so.1     # exported symbols

ldd may run code from the program it inspects, so never use it on a file you do not trust; use objdump -p instead. LD_DEBUG=help ./main lists the other debug categories.

Versions and exported symbols

Keep the soname as long as old programs still work with the new library: adding functions is fine, so release libgreet.so.1.1.0. Removing a function, changing its arguments or changing a public struct breaks the ABI (binary interface): bump the soname to libgreet.so.2, and both versions can be installed side by side.

By default every non-static function is exported. Export only your API, which keeps the symbol table small and stops programs depending on internals:

#define GREET_API __attribute__((visibility("default")))

GREET_API int greet(const char *name);

Compile with -fvisibility=hidden, and only functions marked GREET_API appear in nm -D.

pkg-config files

Ship a .pc file so other projects can find your library's flags. Install it as /usr/local/lib/pkgconfig/greet.pc:

prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include

Name: greet
Description: Example greeting library
Version: 1.0.0
Libs: -L${libdir} -lgreet
Cflags: -I${includedir}
$ pkg-config --cflags --libs greet
-I/usr/local/include -L/usr/local/lib -lgreet
$ gcc main.c $(pkg-config --cflags --libs greet) -o main

If pkg-config cannot find the file, add its directory to PKG_CONFIG_PATH. Use Requires: for other pkg-config packages yours depends on, and Libs.private: for libraries only needed when linking statically (pkg-config --static). On Debian 13 the pkg-config command is provided by pkgconf, a compatible rewrite. CMake projects can read the file with pkg_check_modules().

Loading plugins with dlopen

A program can also open a shared library while running, for example to load plugins named in a configuration file:

/* plugin.c */
#include <stdio.h>

int plugin_run(int x)
{
    printf("plugin_run(%d)\n", x);
    return x * 2;
}
/* loader.c */
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    const char *path = argc > 1 ? argv[1] : "./plugin.so";

    void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
    if (!handle) {
        fprintf(stderr, "dlopen: %s\n", dlerror());
        return EXIT_FAILURE;
    }

    dlerror();                      /* clear any old error */
    int (*run)(int);
    *(void **)&run = dlsym(handle, "plugin_run");
    const char *err = dlerror();
    if (err) {
        fprintf(stderr, "dlsym: %s\n", err);
        dlclose(handle);
        return EXIT_FAILURE;
    }

    printf("result: %d\n", run(21));
    dlclose(handle);
    return EXIT_SUCCESS;
}
gcc -Wall -O2 -fPIC -shared plugin.c -o plugin.so
gcc -Wall -O2 loader.c -o loader
./loader                     # plugin_run(21), then result: 42

RTLD_NOW resolves all symbols at once, so a broken plugin fails at dlopen rather than later. RTLD_LOCAL (the default) keeps the plugin's symbols away from other plugins. Since glibc 2.34 the dl* functions are part of the main C library, so -ldl is no longer needed; it is still accepted, so older build scripts keep working. A path without a slash is searched like a normal dependency, so pass ./plugin.so or a full path. In C++, declare plugin functions extern "C" so their names are not mangled.

Mistakes to avoid

Sources