12
Oct 09
Versioning Symbols for Shared Libraries (glibc)


Sometime back I got a request for running a piece of Java software on an older development system (2.4 kernel/2.2 glibc). The software makes use of a nifty little native compiled JVM launcher, Java Service Wrapper, which provides several features related to JVM configuration and life-cycle process management. Unfortunately, this software has a requirement on >= glibc 2.3 which isn’t supported by that system. Initially this did not surprise me, considering how old it was, but it did spark my curiosity as to how the dynamic loader enforces prerequisite versions on symbols even if an older version exists. Clearly shared library compatibility doesn’t end at versioning of the soname.

To get a basic idea of what shared libraries and symbols were listed in the wrapper binary I ran the following command:

objdump -x wrapper-linux-x86-32

Which gives quite a bit of useful info about the executable:

...
Dynamic Section:
NEEDED               libm.so.6
NEEDED               libpthread.so.0
NEEDED               libc.so.6
...
Version References:
required from libpthread.so.0:
0x0d696912 0x00 05 GLIBC_2.2
0x0d696911 0x00 04 GLIBC_2.1
0x0d696910 0x00 03 GLIBC_2.0
required from libc.so.6:
0x0d696913 0x00 08 GLIBC_2.3
0x0d696911 0x00 07 GLIBC_2.1
0x0d696912 0x00 06 GLIBC_2.2
0x0d696910 0x00 02 GLIBC_2.0
...
SYMBOL TABLE:
...
00000000    F *UND*  00000167      strchr@@GLIBC_2.0
00000000    F *UND*  00000078      nanosleep@@GLIBC_2.0
...
00000000    F *UND*  00000496      realpath@@GLIBC_2.3
...

In the output above I’ve omitted the uninteresting parts and left the important tidbits for the purposes of this article. (1) We can see that this binary requires the shared libraries: libm.so.6, libpthread.so.0, libc.so.6, (2) it has various glibc version references: 2.0, 2.1, 2.2, 2.3, and (3) has some interesting symbols appended with a unique version suffix.

So what? Well, after grepping through the large output from objdump I found that the only requirement on glibc 2.3 was the realpath function. Hmm…that’s interesting, if it wasn’t for this one function the executable would be binary compatible with the glibc installed on this old 2.4 linux distribution. Also, after browsing through the git tree I found that realpath has been available since at least glibc 2.1 (i.e. stdlib/canonicalize.c@202). So why is the dynamic linker requiring a newer version of glibc even though the function has been around for so long? The answer has to do with the versioned symbol scheme introduced in glibc 2.1 (http://people.redhat.com/drepper/symbol-versioning) which is an “extension” of Sun’s own symbol versioning scheme (http://docs.sun.com/app/docs/doc/817-1984/appendixb-45356?a=view).

As I mentioned above realpath has been around since glibc 2.1, yet this executable requires 2.3. The mechanism behind this is that the linker can be used to create global versioned symbol aliases to local symbols generated at compiled time. The internal linker interface provides two mechanisms for defining the aliases and requires the source to be inlined with a simple assembly pseduo-op .symver.

.symver actual, alias@version

.symver actual, alias@@version

The single @ op can be used to define any number of versioned symbols with the same base name. The double @@ can only be defined once for a given symbol since it denotes the default version to use. The linker also requires the use of a map file for defining if a symbol is global or local. A global symbol can be exported, while a local symbol is kept private. Each entry within the map file should correspond to a given version within the source file’s .symver definition.

VER_1.0 {
   global: alias
   local: *
};

To illustrate the full mechanics of the versioned symbol linking mechanism I’ve provided a better example. Say for example we release a library version 1.0 with a function foo. We could version it by adding the simple assembly to the function definition in the source foo.c:

__asm__(".symver foo, foo@@FOO_1.0");
int foo() {
   return 0;
}

Then we would need to create a map file (foo.map):

FOO_1.0 {
   foo;
};

After which we would compile our simple shared library.

gcc -shared -fPIC -Wl,--version-script foo.map foo.c -o libfoo.so.1

At this point we could distribute the library with the foo.h and let others compile against it to their hearts content. But what happens when we want to release another version of the library with potential behavioral changes that may affect the use of foo in other programs? We could introduce a new foo with with the updated behavior and still keep the old fucntion around for legacy programs. This can be done by renaming the original definition of foo to foo_1_0 and adding a new definition of foo called foo_1_1, like so:

/* old foo */
__asm__(".symver foo_1_0, foo@FOO_1.0");
int foo_1_0() {
   return 0;
}
 
/* new foo */
__asm__(".symver foo_1_1, foo@@FOO_1.1");
int foo_1_1() {
   return -1;
}

Then we update our existing map file (foo.map) to the following:

FOO_1.0 {
   foo;
};
FOO_1.1 {
   foo;
} FOO_1.0;

After which we would compile our simple shared library again and redistribute to our customers noting the change in behavior. Programs compiled against the old version and new version can now operate concurrently utilizing the updated library.

1 comment

  1. Hi,

    Very useful post! Btw, would it be possible to “retro-compile” your code to an older glibc version without actually having that version installed in your machine? I mean, compiling this software for using glibc 2.1 realpath from a system bundled with glibc 2.3 or newer?

Leave a comment

*