Yet another post about dynamic lookup of shared libraries
This post is a quick reminder to self regarding specifics of RPATH,
RUNPATH, LD_LIBRARY_PATH, LD_RUN_PATH and the lookup order.
Refresher (a very short one)
RPATH, RUNPATH - are entries in the ELF header, baked into the binary allowing
the dynamic loader to lookup its shared dependencies.
RPATH/RUNPATH can be specified directly, using a linker option (-rpath):
g++ -Wl,-rpath=path/for/rpath
If missing in the command line, RPATH can be set using LD_RUN_PATH variable.
LD_LIBRARY_PATH are environment variables allowing to modify/extend dynamic
loader’s set of search paths for shared dependencies during runtime.
Lookup order
When searching for a shared library, the dynamic loader will look in the following places:
- RPATH
- LD_LIBRARY_PATH
- RUNPATH
- /etc/ld.so.cache
- system paths (i.e.
/lib,/lib64etc)
This specific order differentiates RPATH and RUNPATH in only one way.
RUNPATH can be overridden by LD_LIBRARY_PATH whilst RPATH cannot!
Therefore, RPATH takes the highest precedence.
This became problematic as it wasn’t possible to override RPATH after the
binary has been created without resorting to binary patching with tools like
chrpath or patchelf. For that specific reason RUNPATH was introduced.
New dtags
RUNPATH is available if you specify --enable-new-dtags when linking. This
enables the generation of so called “new tags”. Once enabled, requests to
populate RPATH will in fact populate RUNPATH. Here’s the old way (g++ by
default enables new dtags - they have to be disabled explicitly):
|
|
The default behaviour with new dtags enabled:
|
|
There’s one special case when the binary contains both RPATH and RUNPATH.
If the latter is present, the former is ignored by the dynamic loader.
The implication of --enable-new-dtags is that effectively RPATH is
superseded with RUNPATH. The former one is still supported only because of
backwards compatibility concerns.
Debugging the lookup
It’s as simple as defining an extra environment variable
|
|
Pathname lookups
Aside of the above, there are some nuances to shared library lookup. One of them is described in man 8 ld.so:
When resolving shared object dependencies, the dynamic linker first inspects each dependency string to see if it contains a slash (this can occur if a shared object pathname containing slashes was specified at link time). If a slash is found, then the dependency string is interpreted as a (relative or absolute) pathname, and the shared object is loaded using that pathname.
Which means that exact path to the library can be baked the binary and
RPATH/RUNPATH lookup is ignored for that particular dependency.
Here’s an example (notice the path for libfoo.so):
|
|
$ORIGIN
RPATH or RUNPATH can contain a substitute token. One of the most commonly
used ones is $ORIGIN which, in runtime` is expanded to the directory
containing the program or the shared library.
Meson support
Meson adds RPATH entries allowing for executables to run from the build
directory. There entries are removed during installation. This is mentioned in the documentation.
RPATH can be customised for targets using build_rpath and install_rpath options (refer to e.g. executable documentation).
CMake support
Similarly as meson, CMake adds RPATH to binaries linking with shared libraries.
Meson tends to use ‘$ORIGIN’. CMake likes to specify an absolute path instead.
In a similar fashion as meson, CMake provides two target properties
BUILD_RPATH
and
INSTALL_RPATH.
The former one meant to define paths to be used in the build tree and the
latter defining paths to be used after installation. By default, INSTALL_RPATH is empty.
These can be set like so:
|
|
Conclusion
Dynamic linker/loader contains a lot of customisation mechanisms and it’s a topic on its own. Most of the crux of its behaviour is very well described in
man ld
man 8 ld.so