In this post, I will attempt to explain the inner workings of how dynamic loading of shared libraries works in Linux systems. This post is long – for a TL;DR, please read the debugging cheat sheet.
This post is not a how-to guide, although it does show how to compile and debug shared libraries and executables. It’s optimized for understanding of the inner workings of how dynamic loading works. It was written to eliminate my knowledge debt on the subject, in order to become a better programmer. I hope that it will help you become better, too.
- Example Setup
- Compiling and Linking a Dynamic Executable
- ELF – Executable and Linkable Format
- Direct Dependencies
- Runtime Search Path
- Fixing our Executable
- rpath and runpath
- $ORIGIN
- Runtime Search Path: Security
- Debugging Cheat Sheet
- Sources
A library is a file that contains compiled code and data. Libraries in general are useful because they allow for fast compilation times (you don’t have to compile all sources of your dependencies when compiling your application) and modular development process. Static Libraries are linked into a compiled executable (or another library). After the compilation, the new artifact contains the static library’s content. Shared Libraries are loaded by the executable (or other shared library) at runtime. That makes them a little more complicated in that there’s a whole new field of possible hurdles which we will discuss in this post.
To explore the world of shared libraries, we’ll use one example throughout this post. We’ll start with three source files:
main.cpp
will be the main file for our executable. It won’t do much – just call a function from a random
library which we’ll compile:
#include "random.h"
int main() {
return get_random_number();
}
The random
library will define a single function in its header file, random.h
:
int get_random_number();
It will provide a simple implementation in its source file, random.cpp
:
#include "random.h"
int get_random_number(void) {
return 4;
}
Note: I’m running all of my examples on Ubuntu 14.04.
Before compiling the actual library, we’ll create an object file from random.cpp
:
$ clang++ -o random.o -c random.cpp
In general, build tools don’t print to the standard output when everything is okay. Here are all the parameters explained:
-o random.o
: Define the output file name to berandom.o
.-c
: Don’t attempt any linking (only compile).random.cpp
: Select the input file.
Next, we’ll compile the object file into a shared library:
$ clang++ -shared -o librandom.so random.o
The new flag is -shared
which specifies that a shared library should be built. Notice that we called the shared library librandom.so
. This is not arbitrary – shared libraries should be called lib
for them to link properly later on (as we’ll see in the linking section below).
First, we’ll create a shared object for main.cc
:
$ clang++ -o main.o -c main.cpp
This is exactly the same as before with random.o
. Now, we’ll try to create an executable:
$ clang++ -o main main.o
main.o: In function `main':
main.cpp:(.text+0x10): undefined reference to `get_random_number()'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Okay, so we need to tell clang
that we want to use librandom.so
. Let’s do that1:
$ clang++ -o main main.o -lrandom
/usr/bin/ld: cannot find -lrandom
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Hmmmmph. We told our compiler we want to use a librandom
file. Since it’s loaded dynamically, why do we need it in compile time? Well, the reason is that we need to make sure that the libraries we depend on contain all the symbols needed for our executable. Also note that we specified random
as the name of the library, and not librandom.so
. Remember there’s a convention regarding library file naming? This is where it’s used.
So, we need to let clang
know where to search for shared libraries. We do this with the -L
flag. Note that paths specified by -L
only affect the search path when linking – not during runtime. We’ll specify the current directory:
$ clang++ -o main main.o -lrandom -L.
Great. Now let’s run it!
$ ./main
./main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory
This is the error we get when a dependency can’t be located. It will happen before our application even runs one line of code, since shared libraries are loaded before symbols in our executable.
This raises several questions:
- How does
main
know it depends onlibrandom.so
? - Where does
main
look forlibrandom.so
? - How can we tell
main
to look forlibrandom.so
in this directory?
To answer these question, we’ll have to go a little deeper into the structure of these files.
The shared library and executable file format is called ELF (Execut