Versioned Symbols in Shared Libraries
FAQ
How do versioned symbols help users?
When software is compiled to a dynamically linked executable, it will contain references to shared libraries and to the symbols in those libraries. If that executable is run on a system where symbols are missing from a shared library (perhaps because the application was installed without also updating installed libraries), execution will fail.
[root v1.1]# Bar 5 10 area Bar: symbol lookup error: Bar: undefined symbol: rectangle_area
The error tells the user what the problem is, but doesn’t tell the user how to solve it. The error doesn’t name the library that’s outdated, and doesn’t indicate the minimum version.
With versioned symbols, the error is much more useful.
[root v1.1]# Bar 5 10 area Bar: lib-versioned/libFoo.so.1: version `+LIBFOO_1.1+' not found (required by Bar)
This error message indicates which shared library has the problem,
and provides a reasonably clear hint as to how to resolve the problem
(which is to install libFoo.so.1, version 1.1 or newer).
RPM can also use this information to generate better dependency information automatically, and prevent these problems from happening in the first place.
What are versioned symbols?
Symbol "versions" are a plain-text label applied both to a dynamic shared object, and to symbols in dynamic shared objects.
Where are versioned symbols used?
Versioned symbols are widely used among the core libraries in GNU/Linux systems, especially among developers who are committed to maintaining stable ABIs. Versioned symbols provide them with a mechanism to deploy changes that would require ABI breaks and soname bumps without symbol versioning.
Demo
libFoo provides a simple demonstration of versioned libraries in C and in C++, with examples of setting up automake, CMake, and Meson build systems.
In that project, you’ll find foo.c, which provides two functions, and libFoo.map, which describes which version of the library each symbol first appeared in:
LIBFOO_1.0 {
global:
rectangle_perimeter;
local:
*;
};
LIBFOO_1.1 {
global:
rectangle_area;
};
That map file and just one extra argument,
-Wl,--version-script=libFoo.map
are all that is needed to add symbol versions to a library.
Upkeep is also straightforward. Whenever a new release includes new symbols that you want to export, add a new set of symbols to the map. Sets within a symbol version map should never be changed after a release.
If you make this project, you’ll get an app named Bar,
which is linked to the unversioned build of the shared library,
and an app named Bar-versioned which is linked to the versioned build of the shared library.
ldd ./Bar will demonstrate that Bar uses the shared library in the lib-unversioned directory by default
(the location is embedded using rpath).
You can run env LD_LIBRARY_PATH=lib-versioned ./Bar to use the library build that includes versioned symbols,
demonstrating that the library remains backward compatible when versioned symbols are added.
If you are developing a library that does not currently include versioned symbols, please don’t let rebuilding the version history be a reason to put off adoption. The simple path forward is simply to add all of your symbols to the version you consider current, and then add new versions for any symbols you introduce in the future.
How-To
Examine the capabilities provided by the binary rpm:
rpm -qp --provides <package>.
A package with shared libraries will list the library as libc.so.6()(64bit)
and if the library provides versioned symbols it will also list the library with versions as libm.so.6(GLIBC_2.41)(64bit).
Adding symbol versions is simple for the majority of libraries, initially requiring only a symbol map and one additional argument to the linker during the build process.
#!/bin/sh
echo "/* Avoid modifying a symbol set after it has been released"
echo " When adding features in a new release, add a new set"
echo " Removing features is a breaking change */"
echo "$2 {"
echo " global:"
objdump -T $1 | \
grep -F .text | \
awk '{print $7;}' | \
c++filt | \
awk '/[() ]/ {print " \"" $0 "\";";} \
!/[() ]/ {print " " $0 ";";}' | \
sort
echo "};"
Run generate_initial_map.sh /path/to/library.so.1 <LIBRARY>_<VERSION> to generate a map file.
Adding version-script to Automake
The GNU Portability Library manual includes examples of using version-script in automake. In Makefile.am:
if HAVE_LD_VERSION_SCRIPT libfoo_la_LDFLAGS += -Wl,--version-script=$(srcdir)/libfoo.map endif
Adding version-script to CMake
The BSD-3-Clause licensed protobuf project includes examples of using version-script in CMake.
In CMakeLists.txt, check the linker for support:
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/cmaketest.map
"{
global:
main;
local:
*;
};")
# CheckLinkerFlag module available in CMake >=3.18.
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL 3.18)
include(CheckLinkerFlag)
check_linker_flag(CXX -Wl,--version-script=${CMAKE_CURRENT_BINARY_DIR}/cmaketest.map project_HAVE_LD_VERSION_SCRIPT)
endif()
file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/cmaketest.map)
And, where the library is defined:
if(project_HAVE_LD_VERSION_SCRIPT)
target_link_options(libfoo PRIVATE -Wl,--version-script=${protobuf_source_dir}/src/libfoo.map)
set_target_properties(libfoo PROPERTIES
LINK_DEPENDS ${project_source_dir}/src/libfoo.map)
endif()
Adding version-script to Meson
Meson’s test cases include examples of using version-script.
# Solaris 11.4 ld supports --version-script only when you also specify
# -z gnu-version-script-compat
if meson.get_compiler('c').get_linker_id() == 'ld.solaris'
add_project_link_arguments('-Wl,-z,gnu-version-script-compat', language: 'C')
endif
# Static map file
mapfile = 'bob.map'
vflag = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), mapfile)
l = shared_library('bob', 'bob.c', link_args : vflag, link_depends : mapfile)
See Also
Want to help? Learn how to contribute to Fedora Docs ›