Dynamic Object Versioning

Rod Evans — Sunday August 22, 2004

Surfing with the Linker-Aliens

For some time now, we've been versioning core system libraries. You can display version definitions, and version requirements with pvs(1). For example, the latest version of libelf.so.1 from Solaris 10, provides the following versions:

    % pvs -d /lib/libelf.so.1

So, what do these versions provide? Shared object versioning has often been established with various conventions of renaming the file itself with different major or minor (or micro) version numbers. However, as applications have become more complex, specifically because they are constructed from objects that are asynchronously delivered from external partners, this file naming convention can be problematic.

In developing the core Solaris libraries, we've been rather obsessed with compatibility, and rather than expect customers to rebuild against different shared object file names (i.e., libfoo.so.1, and later libfoo.so.2), we've maintained compatibility by defining fixed interface sets within the same library file. And, the only changes we've made to the library is to add new interface sets. These interface sets are described by version names.

Now you could maintain compatibility by retaining all existing public interfaces, and only adding new interfaces, without the versioning scheme. However, the version scheme has a couple of advantages:

When a consumer references a versioned shared object, the version name representing the interfaces the consumer references are recorded. For example, an application that references the elf_getshnum(3elf) interface from libelf.so.1, will record a dependency on the SUNW_1.4 version:

    % cc -o main main.c -lelf
    % pvs -r main
        libelf.so.1 (SUNW_1.4);

This version name requirement is verified at runtime. Therefore, should this application be executed in an environment consisting of an older libelf.so.1, one that perhaps only offers version names up to SUNW_1.3, then a fatal error will result when libelf.so.1 is processed:

    % pvs -dn /lib/libelf.so.1
    % main
    ld.so.1: ./main: fatal: libelf.so.1: version `SUNW_1.4' not found \\
        (required by file ./main)

This verification might seem simplistic, and won't the application be terminated anyway if a required interface can't be located? Well yes, but function binding normally occurs at the time the function is first called. And this call can be some time after an application is started (think scientific applications that can run for days or weeks). It is far better to be informed that an interface can't be located when a library is first loaded, that to be killed some time later when a specific interface can't be found.

Defining a version typically results in the demotion of many other global symbols to local scope. This localization can prevent unintended symbol collisions. For example, most shared objects are built from many relocatable objects, each referencing one another. The interface that the developer wishes to export from the shared object is normally a subset of the number of global symbols that would normally remain visible.

Version definitions can be defined using a mapfile. For example, the following mapfile defines a version containing two interfaces. Any other global symbols that would normally be made available by the objects that contribute to the shared object are demoted, and hence hidden as locals:

    % cat mapfile
    ISV_1.1 {
    % cc -o libfoo.so.1 -G -Kpic -Mmapfile foo.c bar.c ...
    % pvs -dos libfoo.so.1
    libfoo.so.1 -       ISV_1.1: foo1;
    libfoo.so.1 -       ISV_1.1: foo2;

The demotion of unnecessary global symbols to locals greatly reduces the relocation requirements of the object at runtime, and can significantly reduce the runtime startup cost of loading the object.

Of course, interface compatibility requires a disciplined approach to maintaining interfaces. In the previous example, should the signature of foo1() be changed, or foo2() be deleted, then the use of a version name is meaningless. Any application that had built against the original interfaces, will fail at runtime when the new library is delivered, even though the version name verification will have been satisfied.

With the core Solaris libraries we maintain compatibility as we evolve through new releases by maintaining existing public interfaces and only adding new version sets. Auditing of the version sets help catch any mistaken interface deletions or additions. Yeah, we fall foul of cut-and-paste errors too :-)

For more information on versioning refer to Interfaces and Versioning.

Surfing with the Linker-Aliens

Published Elsewhere


Surfing with the Linker-Aliens

[6] Lazy Loading fall back
Blog Index (rie)
[8] Relocations and debugging flags