Here’s a little introduction to Linux/glibc symbol versioning. The sources, linker version scripts, and makefile from this post can be found here in github.
The definitive reference for this topic is Ulrich Drepper’s dsohowto document.
Linux symbol versioning is a very logical way to construct a definitive API for your product. If you are careful, you can have a single version of your shared library that is binary compatible with all previous versions of code that have linked it, and additionally hide all symbols that you don’t want your API consumer to be able to use (as if all non-exported symbols have a “static” like scope within the library itself).
That visibility hiding can be done in other ways (such as using static, or special compiler options), but utilizing a symbol version linker script to do so also has all the additional binary compatibility related benefits.
Suppose that we have a “v1.1” API with the following implementation:
#include <stdio.h> void internal_function_is_not_visible() { printf( "You can't call me directly.\n" ); } void foo( int x ) { internal_function_is_not_visible(); printf( "foo@@V1: %d\n", x ); }
and we build this into a shared library like so:
$ make libfoo.1.so rm -f libfoo.1.so cc foo.c -g -Wl,--version-script=symver.map -fpic -o libfoo.1.so -Wl,-soname,libfoo.so -shared rm -f libfoo.so ln -s libfoo.1.so libfoo.so
Everything here is standard for building a shared library except for the –version-script option that is passed into the linker with -Wl,. That version script file has the following contents:
This defines a “V1.1” API where all the symbols that are exported with a symbol version @@MYSTUFF_1.1. Note that internal_function_is_not_visible is not in that list. It’s covered in the local: catch-all portion of the symbol version file. Code that calls foo does not look out of the ordinary:
Compiling and linking that code is also business as usual:
However, look at the foo symbol reference that we have for this program:
If we run this, we get:
If you add in a call to internal_function_is_not_visible() you’ll see that compilation fails:
void foo(int); void internal_function_is_not_visible(); int main() { foo(1); internal_function_is_not_visible(); return 0; }
$ make caller1 cc caller.c -g -Wl,-rpath,`pwd` -o caller1 -lfoo -L. /run/user/1002/ccqEPYcu.o: In function `main': /home/pjoot/symbolversioning/caller.c:7: undefined reference to `internal_function_is_not_visible' collect2: error: ld returned 1 exit status make: *** [caller1] Error 1
This is because internal_function_is_not_visible is not a visible symbol. Cool. We now have versioned symbols and symbol hiding. Suppose that we now want to introduce a new binary incompatible change too our foo API, but want all existing binaries to still work unchanged. We can do so by introducing a symbol alias, and implementations for both the new and the OLD API.
#include <stdio.h> void internal_function_is_not_visible() { printf( "You can't call me directly.\n" ); } void foo2( int x, int y ) { if ( y < 2 ) { internal_function_is_not_visible(); } printf( "foo@@V2: %d %d\n", x, y ); } void foo1( int x ) { internal_function_is_not_visible(); printf( "foo@@V1: %d\n", x ); }
This is all standard C up to this point, but we now add in a little bit of platform specific assembler directives (using gcc specific compiler sneaks) :
#define V_1_2 "MYSTUFF_1.2" #define V_1_1 "MYSTUFF_1.1" #define SYMVER( s ) \ __asm__(".symver " s ) SYMVER( "foo1,foo@" V_1_1 ); SYMVER( "foo2,foo@@" V_1_2 );
We’ve added a symbol versioning alias for foo@@MYSTUFF_1.2 and foo@MYSTUFF_1.1. The @@ one means that it applies to new code, whereas the @MYSTUFF_1.1 is a load only function, and no new code can use that symbol. In the symbol version script we now introduce a new version stanza:
$ cat symver.2.map MYSTUFF_1.2 { global: foo; }; MYSTUFF_1.1 { global: foo; local: *; }; $ make libfoo.2.so rm -f libfoo.2.so cc foo.2.c -g -Wl,--version-script=symver.2.map -fpic -o libfoo.2.so -Wl,-soname,libfoo.so -shared rm -f libfoo.so ln -s libfoo.2.so libfoo.so
If we call the new V1.2 API program, like so:
our output is now:
Our “binary incompatible changes” are two fold. We don’t call internal_function_is_not_visible if our new parameter is >= 2, and we print a different message.
If you look at the symbols that are referenced by the new binary, you’ll see that it now explicitly has a v1.2 dependency: