Static and shared libraries

A library is a file of compiled code that a program links against. The static form, an archive of object files, is copied into the executable at link time; the shared form, a .so, is loaded when the program starts and can be shared by every process that uses it.

CMake does this through targets, as lesson 100 showed. This lesson does it with the compiler and ar, which is what the build system runs underneath, and then looks at the link error that a missing search path produces.

The build system does the same three steps with targets, and the flags above are what it writes into the generated command. Knowing them makes a link error readable.

A shared library can be replaced without relinking the program, which is why system libraries are shared. A static library cannot, which is why a release binary is easier to ship.

The linker resolves the -l names in the order they appear on the command line, which is why a library that depends on another comes first. A build system writes that order from the target graph, another reason to let it.

The archive and the shared object are the same objects with different packaging, and the linker treats them as two forms of one idea.

A library that is used by two programs saves memory only in the shared form, which is the reason a desktop system is built almost entirely from them.

An archive with ar

The library's object is compiled once with -c, and ar rcs packs it into libmath.a. The program then compiles main.cpp and links the archive with -L. for the directory and -lmath for the name.

The program prints 25. The archive is a container of object files, and the linker copies only the members the program uses, which is why a static binary grows by what it needs.

The name matters: the linker turns -lmath into libmath.a on Linux, so the file must have the lib prefix and the .a suffix. A file named math.a is not found by -lmath.

square.h

#pragma once int square(int value);

math.cpp

#include "square.h" int square(int value) { return value * value; }

main.cpp

#include <iostream> #include "square.h" int main() { std::cout << square(5) << '\n'; }

g++ -std=c++23 -Wall -Wextra -c math.cpp -o math.o
ar rcs libmath.a math.o
g++ -std=c++23 -Wall -Wextra main.cpp -L. -lmath -o app
./app

25

A shared object

The same source built with -fPIC and -shared becomes libmath.so. Position-independent code can be loaded at any address, which the shared form requires.

The program links the .so and runs it. The binary is smaller than the static one and refers to the library by name, which it finds through the runtime search path.

The -Wl,-rpath,. in the link line puts the current directory into the runtime search path, so the program finds the library without LD_LIBRARY_PATH. A system library is installed in a directory the loader already searches.

square.h

#pragma once int square(int value);

math.cpp

#include "square.h" int square(int value) { return value * value; }

main.cpp

#include <iostream> #include "square.h" int main() { std::cout << square(6) << '\n'; }

g++ -std=c++23 -Wall -Wextra -fPIC -shared math.cpp -o libmath.so
g++ -std=c++23 -Wall -Wextra main.cpp -L. -lmath -Wl,-rpath,. -o app_shared
./app_shared

36

What the binary needs

ldd lists the shared libraries a binary needs and where the loader finds them. The program asks for the library's line, and the output shows the path next to the name.

The path is the current directory because the binary was linked with an rpath of dot. A system binary shows /usr/lib and similar paths instead.

A missing library at run time is the error error while loading shared libraries and the program does not start. The loader message names the library, which is the first thing to check after a deployment.

square.h

#pragma once int square(int value);

math.cpp

#include "square.h" int square(int value) { return value * value; }

main.cpp

#include <iostream> #include "square.h" int main() { std::cout << square(7) << '\n'; }

g++ -std=c++23 -Wall -Wextra -fPIC -shared math.cpp -o libmath.so
g++ -std=c++23 -Wall -Wextra main.cpp -L. -lmath -Wl,-rpath,. -o app_shared
ldd ./app_shared | grep math

libmath.so => ./libmath.so (0x765e35bb9000)

The library the linker cannot find

The linker searches a list of directories that does not include the current one by default. A -lmath without a -L that points at the library fails, even when the file is in the same directory.

The program links with -lmath alone and the linker reports cannot find -lmath. The message names the library, not the file, and the fix is a -L for the directory.

On a system where the library is installed, the path comes from the package and no -L is needed. For a library built in the project, the build system supplies the path, which is what CMake does with target_link_libraries.

square.h

#pragma once int square(int value);

math.cpp

#include "square.h" int square(int value) { return value * value; }

main.cpp

#include <iostream> #include "square.h" int main() { std::cout << square(5) << '\n'; }

g++ -std=c++23 -Wall -Wextra -fPIC -shared math.cpp -o libmath.so
g++ -std=c++23 -Wall -Wextra main.cpp -lmath -o app_broken

/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: cannot find -lmath: No such file or directory
collect2: error: ld returned 1 exit status

Exercises

  1. Build the same library as a static archive and compare the size of the executable with the shared version.
  2. Remove the rpath from the shared link and see what the program prints when it starts.
  3. List the symbols of libmath.a with nm and find the one the program uses.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does ar rcs libmath.a math.o do?

    Answer

    Packs math.o into a static library

    ar builds the archive that the linker reads as a static library.

  2. What does -lmath tell the linker to look for?

    Answer

    A file called libmath.a or libmath.so

    The linker adds the lib prefix and the library suffix to the name.

  3. What does -L. add?

    Answer

    The current directory to the library search path

    -L adds a directory where the linker looks for -l names.

  4. Why is -fPIC needed to build a shared library?

    Answer

    The code must run at any load address

    A shared object is loaded at an address chosen at run time, so the code must be position independent.

  5. What does this command produce? g++ -fPIC -shared math.cpp -o libmath.so

    Answer

    A shared library

    -shared asks for a shared object, and -fPIC makes its code loadable anywhere.

  6. What does ldd ./app show?

    Answer

    The shared libraries the binary needs and where they are found

    ldd asks the loader to resolve the dependencies and prints the paths.

  7. What does -Wl,-rpath,. do?

    Answer

    Adds the current directory to the runtime library search path

    The option is passed to the linker and stored in the binary for the loader.

  8. What error does g++ main.cpp -lmath print when libmath.so is only in the current directory?

    Answer

    cannot find -lmath

    The linker does not search the current directory by default, so it cannot find the library at all.

  9. How does the static library differ from the shared one in the executable?

    Answer

    It is copied into the executable at link time

    The archive members the program uses become part of the binary; the shared form stays a separate file.

  10. What does the program print at run time when a shared library is missing?

    Answer

    error while loading shared libraries and it does not start

    The loader resolves the dependencies before main, and a missing one stops the program there.

The code on this page was built with g++ 15.2.0, GNU ar and ldd and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev