Author: Mugabi Siro

Category: ELF Support


This page presents a tutorial on the development and handling of shared libraries on a GNU/Linux system. This is entry-level to intermediate-level material. For more advanced reading e.g. using version scripts, GCC visibility attributes for exported symbols and certain C++ specific issues, check out the links in the "Further Reading" section. Development platform used was Ubuntu 12.04 AMD64.

Tags: gnu/linux toolchain gnu linux s/w development elf support libraries


Shared libraries are shared object modules that get dynamically linked with a program in memory courtesy of the dynamic linker (i.e.,, etc) at application load-time or during application run-time. These libraries are "shared" since a single copy of the physical pages belonging to their code segment and the read-only parts of their data segment, can be mapped into the virtual address space of different and unrelated processes that use them.

ABI Versioning

Generally, successive versions of a shared library contain modifications and/or additions to it's exported interfaces. These Application Binary Interface (ABI) changes could either be compatible or incompatible with existing applications. For compatibility, no definition (i.e. interface) used by existing applications should have disappeared or have changed in such a way as to affect application run-time functionality. Loosely put, ABI versioning is the process of associating an ABI with a specific shared library version.

Shared libraries are used both during the static linking phase with ld(1) and during program execution with the dynamic linker. ld(1) tries to satisfy undefined references in its input object files from the definitions in the development libraries of the toolchain environment. These references are marked in the output object file. During the execution of the program, the dynamic linker associates these references with shared libraries in the run-time host environment. Now, in typical embedded development scenarios, the build and run-time host machines/environments will be different and ABI versioning, therefore, becomes crucial. Under Linux, there exist a number of methods used for ABI versioning.

Filename Versioning

The oldest and coarsest-grained approach is filename versioning. Conventionally, library names take the form, where:

  • MAJOR: The major version number is used to identify complete library ABI incompatiblity. MAJOR changes when interfaces to functions in the shared library change to the extent that existing applications can no longer use the library e.g. and For example, refer to the Linux Documentation Project: Program-Library-HOWTO (Section "Incompatible Libraries") for a discussion on what makes libraries incompatible in C/C++.

  • MINOR and RELVER: These are used to identify compatible changes between library releases. Essentially, compatible changes consist of extended or added interfaces in newer releases. Generally, this translates to fowards compatibility: programs compiled in a build environment with older shared library release versions are guaranteed to execute on the run-time host environment with newer library versions. However, backwards compatibility is not guaranteed since, for example, newer releases may include added interfaces and if a program employs any one of these new interfaces, then their relocations won't be satisfied by outdated libraries versions in the runtime host.

    The minor version number, MINOR, changes when new functions are added to a shared library but existing functions still provide the same interfaces. MINOR is reset when MAJOR changes. RELVER, the release version number, indicates patch levels with respect to bug fixes in a previous version e.g. and When MINOR changes, RELVER is reset.

The version numbering scheme for shared libraries makes it possible to maintain several compatible and incompatible versions of the shared library on the system. However, this situation presents a potential harzard: If several incompatible versions of a library are loaded (at execution time) by the different modules in an application, then the overall behaviour of the application is likely to be affected since only one version of the set of library functions in question will be available at run-time.

One solution to this problem is to ensure that a consistent set of development libraries are used in the build machine for all binaries of the run-time target. Alternatively, some shared libraries on GNU/Linux systems include symbol versioning as an extra versioning technique that mitigates such application run-time conflicts in execution environments that have incompatible versions of a library present.

Symbol Versioning

Symbol versioning is a finer-grained method of ABI versioning originally developed by Sun for Solaris OS. Basically, an added or extended interface is associated with a version name (typically, based on the version number) of the library that first exported it. So, in the run-time environment, even if incompatible versions of a given shared library get pulled in by an application, references will be satisfied correctly courtesy of the unique versioning scheme for each incompatible interface exported by the libraries.

With (e)glibc, for instance, version names are generally of the form GLIBC_${VER} where ${VER} corresponds to the full number of the library release e.g. GLIBC_2.0, GLIBC_2.3.4, GLIBC_2.15 etc. This also means that the shared library can support multiple interfaces, without changing the MAJOR number e.g:

$ readelf --dyn-syms /lib/x86_64-linux-gnu/ | grep sched_setaffinity
     743: 0000000000131720    13 FUNC    GLOBAL DEFAULT   12 sched_setaffinity@GLIBC_2.3.3
     744: 00000000000cabd0   348 FUNC    GLOBAL DEFAULT   12 sched_setaffinity@@GLIBC_2.3.4

where the symbol marked with @@ is the default definition that ld(1) considers at compile-time. Applications can still specify a particular symbol version to bind to via, say, a symbol alias or the .symver assembler psuedo-op.

Symbol versioning is also used for performance reasons: By default, exported shared library function symbols are bound on an "as-needed" basis by the dynamic linker i.e. not all relocations are performed at program load. Thus if the program uses a symbol that is abscent in an outdated shared library version on the runtime host, the application will suddenly fail during execution at the point in time when the symbol is referenced. On the other hand, setting the LD_BIND_NOW environment variable to a non-empty value before starting the application, or compiling with the -z now static linker option, will force the dynamic linker to perform all relocations at load time. Unfortunately, this is relatively expensive and a performance penalty might be incurred. Shared libraries that employ symbol versioning make it possible for the dynamic linker to detect outdated shared library versions in the runtime environment much faster at load time (i.e. before it starts the application), yet without having to perform all relocations.

Shared library build

This section covers the essential build steps for a shared library. Issues such as using GCC visibility attributes for symbol export control, using version scripts for symbol versioning, etc will not be covered here. Refer to the links in Section "Resources and Further Reading".

Consider the following trivial library sources:

$ cat foo.h

#ifndef _FOO_H_
#define _FOO_H_

void lfunc(void);

#endif /* _FOO_H_ */

See Section "Appendix" for comments on writing header files for mixed C/C++ code.

$ cat libfoo.c
#include <stdio.h>

void lfunc(void)

Now, build the shared library:

$ gcc -fPIC -c libfoo.c -o libfoo.o

$ gcc -Wall -O2 -shared -Wl,-soname, -o libfoo.o -lc

$ rm libfoo.o

$ ls
libfoo.c  foo.h
  • -fPIC/-fpic Instruct gcc to generate Position Independent Code (PIC) so that the final shared object module can be loaded at any address. This is characteristic of shared libraries since multiple running processes will share the same library code. The -fpic switch may result in the generation of smaller and faster code, especially for certain RISC architectures - but probably with platform-dependent limitations. Generally, -fPIC always works. See gcc(1).

  • The -shared option is passed on to ld(1) and instructs it to create a shared object file.

  • Since libfoo makes a call to puts(3), the -lc option is included in gcc's command line. But this is not really necessary since gcc automatically includes -lc unless something like -nostdlibs or -nodefaultlibs was specified in the command line.

  • The -Wl,-soname, linker option (i.e. from ld(1)'s perspective) is used to specify the shared library's soname.

  • Of course, the compile and build steps could be achieved with a one-liner:

    gcc -Wall -O2 -fPIC -shared -Wl, libfoo.c -o

    Also note that in real world library development, optimization (e.g. -O2) may have to wait until the final build phases; inclusion of debugging info (e.g. via gcc(1)'s -g) may also come in handy during the test phases.

The Shared Object NAME (soname)

The Shared Object NAME (soname) conventionally takes the form and is required by the dynamic linker/loader. The soname is specified during the shared library build via the -soname option which sets the DT_SONAME entry in the shared object's dynamic section to the specified value. In our case:

$ readelf -d | grep SONAME
 0x000000000000000e (SONAME)             Library soname: []

In the filesystem, the soname gets installed as a symbolic link to the shared library object file. The soname can be created by hand, for example:

$ ln -s

or (preferably) via ldconfig(8)1. For example, to request ldconfig(8) to process library files that reside only in the current working directory:

$ /sbin/ldconfig -nv .
.: -> (changed)

The separate soname-libname scheme allows for the replacement of a library object file without having to recompile existing applications that need the library. Nevertheless, executing instances of these applications (depending on the manner in which they dynamically link with the library) may have to be stopped and restarted in order to use the new library. Naturally, this mechanism only applies if library compatibility is still mantained (same MAJOR).

Linking with Shared Libraries

Dynamic linking with a given shared library can be done either at application load time or run time. Object files linked against the shared library during program build employ the load time mechanism. On the other hand, programs that use the dlopen(3) API exploit run-time dynamic linking. During the build of applications that will employ load-time dynamic linking, the presence of the needed shared libraries is required both in the build machine environment (for the static linking phase with ld(1)) and in the run-time host environment. On the other hand, for applications that exploit run-time dynamic linking, the shared libraries used via dl_open(3) are only required in the run-time host environment.

Load time dynamic linking

Consider the following helloworld application that makes use of the shared library's routines:

$ cat fooprog.c
#include "foo.h"

int main(void)
    return 0;

Building applications that make use of non-standard libraries requires explicit specification of the the respective library linker names on the command line. A shared library linker name takes the form - which is essentially the soname without any version number.

To use the linker name, specify -lNAME on the gcc(1) command line. The linker name is just a symbolic link to the shared library soname and needs to be created explicity - otherwise an attempt to compile against the library will fail:

$ gcc -Wall -O2 fooprog.c -L. -lfoo -o fooprog
/usr/bin/ld: cannot find -lfoo
collect2: ld returned 1 exit status

$ ln -s

$ gcc -Wall -O2 fooprog.c -L. -lfoo -o fooprog

Note that the linker name could as well point to the shared library object file itself i.e. However, it is recommended to set the linker name to point to the soname instead so that an update of the library will only require a corresponding update of the soname symbolic link. This way, the build of dynamically linked applications will continue to work "transparently". Of course, this is only true if library ABI compatibility is still in effect.

Alternatively, for simple setups such as these, linking may also be done directly against the shared library object file, say:

$ gcc -Wall -O2 fooprog.c -o fooprog

Note that the linker name is not required/used during application load.

Run-time dynamic linking

Perhaps the less commonly used approach (on GNU/Linux systems), this mechanism allows running applications to request the dynamic linker to load and link arbitrary shared libraries "on-the-fly" i.e. at the point in a program when the loaded and executing application actually requires the shared library.

This mode of dynamic linking has its merits. For example, existing library functions can be updated and new functions can even be added and used by a "live" instance of an application i.e. without a restart. In addition, application compile and build can proceed in the abscence of the shared library or even its development headers.

There are four functions available for a program to interface to the dynamic linker for run-time dynamic linking: dlopen(3), dlsym(3), dlclose(3) and dlerror(3). Using this API requires the program to include <dlfcn.h> and link with -ldl.

void *dlopen(const char *filename, int flag);
                         Returns: ptr to handle if OK, NULL on error


  • filename is (typically) the shared library name. According to dlopen(3), if filename is NULL, then the returned handle is for the main program. If filename contains a slash (/), then it is interpreted as a (relative or absolute) pathname. Otherwise, the dynamic linker searches for the library in the manner described in Section Library Search.

    If filename is to be set to the shared library name, then it is recommended to specify its soname instead of the actually name of the shared object file. Using the soname allows for "transparent" library updates even while the application is still running i.e. without the need for editing the sources, recompiling and restarting the application - provided, of course, that the new library is still ABI comptible (maintains the same MAJOR).

  • flag Either RTLD_LAZY or RTLD_NOW must be specified. With the RTLD_LAZY, symbols (function references only) are only resolved as the code that references them is executed. References to variables are always immediately bound. RTLD_NOW will cause all undefined symbols in the library to get resolved before dlopen returns. If this cannot be done, then an error is returned.

    There are other optional RTLD_*values that may be ORed in flag. These include RTLD_GLOBAL which makes symbols defined in the library available for symbol resolution by subsequently loaded libraries - generally a bad idea2. Consult dlopen(3) for the full story.

The rest of the API is declared as:

void *dlsym(void *handle, char *symbol);
                    Returns: ptr to symbol if OK, NULL on error

void dlclose(void *handle);
                    Returns: 0 if OK, -1 on error

const char *dlerror(void);
                    Returns: error msg if previous call to dl* failed, NULL if previous call was OK

Consider the following program, dl_fooprog.c (a modified version of fooprog.c), that interfaces with by way of run-time dynamic linking:

#include <stdio.h>  /* fprintf */
#include <stdlib.h> /* exit(3) */
#include <dlfcn.h>  /* dlopen */

int main(void)
    void *handle;
    char *errstr;
    void (*lfunc)(void);

    /* load "" */
    if(!(handle = dlopen("./", RTLD_LAZY))){
        fprintf(stderr, "%d: %s\n", __LINE__, dlerror());

    /* get pointer to "lfunc" */
    lfunc = dlsym(handle, "lfunc");
    if((errstr = dlerror())){
        fprintf(stderr, "%d: %s\n", __LINE__, errstr);


    /* unload "" */
    if(dlclose(handle) < 0){
        fprintf(stderr, "%d: %s\n", __LINE__, dlerror());


and build:

$ gcc -Wall -O2 -rdynamic dl_fooprog.c -o dl_fooprog -ldl

where the -rdynamic switch by gcc(1) translates to the -export-dynamic option for ld(1). This is really only required if the object loaded via dlopen(3) needs to refer back to symbols defined by the program that loaded it. It isn't necessary here. This page includes a simple case study.

During the static linking process, ld(1) records the sonames of the development shared libraries in the DT_NEEDED entries of the dynamic section of the output object file:

$ readelf -d | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: []

$ readelf -d fooprog | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: []
 0x0000000000000001 (NEEDED)             Shared library: []

$ readelf -d dl_fooprog | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: []
 0x0000000000000001 (NEEDED)             Shared library: []

Notice that:

  • The values of these entries are just a filename with no pathname info, i.e. no run path. The dynamic linker in the runtime host environment is charged with the responsibility of locating these files.

  • The order in which the libraries were listed on ld(1)s command line (i.e. from left to right) controls the order that they will be loaded by the dynamic linker. In other words, the left-to-right positioning of needed shared libraries on ld(1)'s command line translates to the top-to-bottom order of DT_NEEDED entries in the final executable. The topmost entry will be loaded first.

(e)glibc's searches for the libraries as follows (see for further details):

  • (ELF only) If the executable file for the calling program contains a DT_RPATH tag, and does not contain a DT_RUNPATH tag, then the directories listed in the DT_RPATH tag are searched. DT_RPATH and DT_RUNPATH are discussed in Section -rpath below

  • If, at the time that the program was started, the environment variable LD_LIBRARY_PATH was defined to contain a colon-separated list of directories, then these are searched. (As a security measure this variable is ignored for set-user-ID and set-group-ID programs.)

  • (ELF only) If the executable file for the calling program contains a DT_RUNPATH tag, then the directories listed in that tag are searched.

  • The cache file /etc/ (maintained by ldconfig(8)) is checked to see whether it contains an entry for filename.

  • The directories /lib and /usr/lib are searched (in that order).

If the library has dependencies on other shared libraries, then these are also automatically loaded by the dynamic linker using the same rules. This process may occur recursively, if those libraries in turn have dependencies, and so on.

Now, running:

$ ./fooprog 
./fooprog: error while loading shared libraries: cannot open shared object file: No such file or directory

fails because the dynamic linker cannot find ./ since it neither resides in either /lib or /usr/lib, nor has /etc/ been updated to point to it. To fix this according to the rules above:


The LD_LIBRARY_PATH environment variable passes to the dynamic linker a colon-separated list of directories in which to search for libraries at execution-time. In this case:

$ LD_LIBRARY_PATH=$PWD ./fooprog


ld(1) makes it is possible to record, in its output object file, the explicit run paths for the needed runtime shared libraries that reside in non-standard system directories. At load time, the dynamic linker will search these explicit run paths along with the standard system settings: locations specified against LD_LIBRARY_PATH, entries in /etc/ and then standard system directories, /lib/ and /usr/lib.

Explicit run paths can be specified via the -rpath or -R static linker command line switches. Values specified against these options get recorded in the DT_RPATH and DT_RUNPATH entries of the output file's dynamic section:

  • The path in DT_RPATH is searched by the dynamic linker prior to any other run path setting including LD_LIBRARY_PATH.

  • The path in DT_RUNPATH is searched after the run paths in LD_LIBRARY_PATH. This allows the user to override the compile-time -rpath settings.

Note that, for backward compatibility, DT_RPATH entries are created by default. Check out Using -rpath, An Example for a case study on the matter.

However, keep it in mind that since the run paths are hard-coded in the object file, moving the object files to the runtime host may present portability issues if the expected (absolute or relative) run paths do not exist in the target. There exist a number of workarounds, but those won't be discussed here. Check out the links in Section "Resources and Further Reading".

Standard Library Installation

As noted, the dynamic linker will automatically search directories listed in /etc/ and then the directories /lib and /usr/lib for the library names listed against the object file's DT_NEEDED tags. Since these tags are conventionally set to the soname, the searched directories should at least contain the soname symbolic link to the actual library file, e.g:

$ ls -l /lib/x86_64-linux-gnu/ | egrep '(|'
lrwxrwxrwx 1 root root ->
lrwxrwxrwx 1 root root ->

By default, ldconfig(8) reads /etc/* to update soname symlinks for libraries in the directories listed there, and then updates /etc/ See ldconfig(8) for info about editing /etc/

LD_LIBRARY_PATH and -rpath are quite useful during the hacking/prototyping/development phase. For a production-level embedded system, it is probably best to perform a standard library installation.

Overriding/Interposing library functions

Interposition of library functions allows a private implementation of an interface (in a private library) to be used without replacing the general code of the application.

Execution time symbol interposition is made possible courtesy of the dynamic linker's symbol lookup policy (in conjuction with the GOT/PLT mechanism): For each symbol relocation, the lookup process is repeated from the start of a lookup scope3. Loosely put, a lookup scope is an ordered list of loaded objects needed by the application. The global lookup scope is in this fashion: the executable itself (internally defined global symbols that are also members of its dynamic linking symbol table, .dynsym)4, then the DT_NEEDED entries in the executable (in the order that they were listed in its dynamic section, .dynamic)5, then the DT_NEEDED entries of the executable's first library dependency, second library dependency, and so forth, recursively, for all of the application's library dependencies6. If the lookup scope contains more than one definition of the same symbol, the lookup algorithm simply picks up the first definition it encounters.

A few approaches to effecting library function interposition are described below:


With respect to library function interposition, a restriction with using the LD_LIBRARY_PATH mechanism is that the private library's soname must much an entry in the DT_NEEDED tags of the executable. This means that the execution time load of the private library replaces use of the original library - and therefore the private library must support all respective (versions of) functions referenced by the application i.e. non-selective library function interposition. It also means that the private library's soname must also reside in a directory separate from that of the soname of the library to be overriden.

See Overriding Default Library via LD_LIBRARY_PATH for an example of using this environment variable to override default library search path, thereby, overriding a default library load.


The LD_PRELOAD environment variable can be used to selectively interpose functions in other shared libraries. With this mechanism, the private libraries are only required at execution time i.e. their sonames need not be recorded in the DT_NEEDED entries of, say, the dynamically linked executable of the application. See Interposing Library Functions via LD_PRELOAD for an example of using LD_PRELOAD.

ld(1) command line

Unlike most gcc(1) command line options, where the order of their placement on the command line doesn't matter, the position of the -l and -L options remains significant. While ld(1) accepts an intermixed list of object files and libraries on the command line, it processes each in the specfied order i.e. from left to right:

  • Generally, the object file which contains definitions of exported symbols should be specified after any other files which reference those symbols.

  • If more than one of the listed libraries exports a given symbol, a DT_NEEDED tag will be emitted only for the first library that satisfies the undefined symbol reference from a regular object file. Check out this example.

  • The order in which libraries get listed on the command line controls the order of the DT_NEEDED tags in the static linker's output object file, thereby influencing the dynamic linker's lookup scope policy.

In short, the gcc(1) command line provides the developer with a facility to interpose private versions of library functions by listing the private libraries before the standard libraries on the command line. The Interposing Library Functions with ld(1) entry presents a few examples on this subject.


Probably not the most practical approach but it can serve as an interesting academic or hacking exercise. See Editing DT_NEEDED, An Example for a case study on editing an ELF executable object file.



LD_DEBUG can be used to view the manner in which searches and loads the required shared library dependencies. The examples in this page use LD_DEBUG to illustrate the different manner in which libraries get loaded during the execution of fooprog (i.e. at program load time) and dl_fooprog (i.e. at program run-time).


The dynamic linker's LD_TRACE_LOADED_OBJECTS environment variable can be used to list all external dependencies of a dynamically linked object file. For instance:

$ LD_TRACE_LOADED_OBJECTS=1 ./fooprog =>  (0x00007fff689ff000) => not found => /lib/x86_64-linux-gnu/ (0x00007f54a5ddc000)
    /lib64/ (0x00007f54a619c000)

$ LD_TRACE_LOADED_OBJECTS=1 ./dl_fooprog =>  (0x00007fff6f1ff000) => /lib/x86_64-linux-gnu/ (0x00007f3220b87000) => /lib/x86_64-linux-gnu/ (0x00007f32207c7000)
    /lib64/ (0x00007f3220d8b000)

The ldd(1) script internally sets and uses this dynamic linker environment variable.

Resources and Further Reading

The (e)glibc dynamic linker,, supports several other useful environment variables and options. Consult

This entry just scratched the surface of developing and using shared libraries on a GNU/Linux system. If you intend to pursue serious or production level shared library development, then there exist plenty of authoritative (online) material on the subject including:

  • How To Write Shared Libraries, Ulrich Drepper, (Available Online - Please read it.)

  • gcc(1), ld(1),, readelf(1), objdump(1), elf(5) manpages


  • Using ld, The GNU linker (Available Online)


1. When creating the sonames, ldconfig(8) consults the object file's DT_SONAME entry. See elf/readelflib.c:process_elf_file() of eglibc-2_X [go back]

2. See How to Write Shared Libraries, Ulrich Drepper (Available Online) [go back]

3. A lookup scope can consist of upto three parts: Global (default), local (for objects opened via dlopen(3); searched after global), and a special lookup scope (pertinent to object file modules marked with the DF_SYMBOLIC flag) that is searched before the global lookup scope. See How To Write Shared Libraries, Ulrich Drepper (Available Online) [go back]

4. Using gcc(1)'s -rdynamic (or ld(1)'s --export-dynamic) when creating a dynamically linked executable causes ld(1) to add all global symbols to executable's dynamic linking symbol table, dynsym. This table is the set of symbols which are visible from dynamic objects at run-time. Also see --dynamic-list in ld(1). [go back]

5. The left-to-right order which shared objects get listed on ld(1)'s command line is the top-to-bottom order which their DT_NEEDED tags will appear in the static linker's output object file. [go back]

6. Loaded objects in a lookup scope are unique and do not appear more than once. [go back]


C vs C++

To prevent the C++ compiler from mangling the names of C functions in mixed C/C++ code, the extern C directive is required in the .h header files of the development libraries e.g:

#ifndef FOOBAR_H
#define FOOBAR_H

#ifdef __cplusplus
extern "C" {

/* header file contents */

#ifdef __cpluscplus
#endif /* FOOBAR_H */