Posts Tagged: symbol versioning


19
Oct 09

Linking to Older Versioned Symbols (glibc)

In my last post I gave a brief overview of the mechanism used internally by glibc for versioning symbols within shared libraries. As an addendum to my previous article I would like to discuss a simple way to force linking against older glibc symbols. Why would you do this you may ask? Well, suppose you have several GNU/Linux systems with varying glibc installs across them but you want to deliver a binary that would be compatible across them. One option is to statically link your binary, my vote :) , the other is to link to an older symbol within the shared library. I would like to mention that using an older symbol has the obvious drawback, possible advantage, of using something that was deprecated for a reason in the past (i.e. broken, behavior changes, performed poorly, new arch support, etc.). The reason I say it is possibly advantageous to link against an older symbol may be because it is known to behave in a desired way, broken or not! I present the following merely for education purposes, so use with care.

To force linking against a particular symbol you need to use the same .symver pseudo-op that is used for defining versioned symbols in the first place. In the following example I make use of glibc’s realpath, but want to make sure it is linked against an older 2.2.5 version.

#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
 
__asm__(".symver realpath,realpath@GLIBC_2.2.5");
int main()
{
   char* unresolved = "/lib64";
   char  resolved[PATH_MAX+1];
 
   if(!realpath(unresolved, resolved))
      { return 1; }
 
   printf("%s\n", resolved);
 
   return 0;
}

If you were to use objdump on the resulting binary you would see that it is indeed using realpath@GLIBC_2.2.5! Also note that other symbols have been resolved to their defaults so you need to make sure you add a .symver pseudo-op for each symbol you want to force to an older version.

0000000000000000      F *UND*  0000000000000000         realpath@GLIBC_2.2.5
...
0000000000000000      F *UND*  0000000000000000         __stack_chk_fail@@GLIBC_2.4

The .symver pseudo-op can be used this way to force any symbol to be linked against an older one so long as it is valid. To ease linking against older glibc versions I’ve provided a simple header which can be used to force linking against the minimum glibc version for a give x86 architecture. The minimum versions I am using were taken from shlib-versions file of the glibc git tree.

#ifndef __GLIBC_COMPAT_SYMBOL_H__
#define __GLIBC_COMPAT_SYMBOL_H__ 1
 
/**
 * add other architecures below
 */
#ifdef __amd64__
   #define GLIBC_COMPAT_SYMBOL(FFF) __asm__(".symver " #FFF "," #FFF "@GLIBC_2.2.5");
#else
   #define GLIBC_COMPAT_SYMBOL(FFF) __asm__(".symver " #FFF "," #FFF "@GLIBC_2.0");
#endif /*__amd64__*/
 
#endif /*__GLIBC_COMPAT_SYMBOL_H__*/

To use the glibc compatible header with the realpath example above, we merely use the GLIBC_COMPAT_SYMBOL macro with the appropriate symbol:

GLIBC_COMPAT_SYMBOL(realpath)

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.