Program Library HOWTO: static and shared libraries on Linux
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
-lgreetfinds 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:
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
- Forgetting
-fPICfor objects that go into a shared library. With position-dependent code (-fno-pie) the link fails with "relocation … can not be used when making a shared object; recompile with -fPIC". Compilers that build PIE code by default, such as Debian's and Ubuntu's GCC, may link without it, but PIE code assumes its symbols cannot be overridden, which is wrong for a shared library: always pass-fPIC. - No soname: programs then record the file name, and every update breaks them or needs relinking.
- Overwriting a library in place with
cpwhile programs use it: running programs can crash. Install the new version under its own real name (libgreet.so.1.1.0) and letldconfigmove the soname symlink. - Exporting
LD_LIBRARY_PATHin~/.bashrcto hide an install problem. - Mixing static and shared copies of the same library in one program, which gives two sets of global state.
Sources
- ld.so(8): search order, $ORIGIN, LD_LIBRARY_PATH, LD_DEBUG, man7.org
- ldconfig(8), man7.org
- ldd(1), man7.org
- dlopen(3), man7.org
- Command-line options (-soname, -rpath, --enable-new-dtags), GNU ld manual
- pkg-config(1) and pc(5), pkgconf, Debian manpages
- Why glibc 2.34 removed libpthread (libdl merged into libc), Red Hat Developer