Linker and Libraries Guide

Binding to a Version Definition

When a dynamic executable or shared object is built against other shared objects, these dependencies are recorded in the resulting object (see "Shared Object Processing" and "Recording a Shared Object Name" for more details). If these shared object dependencies also contain version definitions, then an associated version dependency will be recorded in the resulting object.

The following example takes the data files from the previous section and generates a shared object suitable for a compile time environment. This shared object, libfoo.so.1, will be used in following binding examples:


$ cc -o libfoo.so.1 -h libfoo.so.1 -M mapfile -G foo.o bar.o \
data.o
$ ln -s libfoo.so.1 libfoo.so
$ pvs -dsv libfoo.so.1
        libfoo.so.1:
                _end;
                _GLOBAL_OFFSET_TABLE_;
                _DYNAMIC;
                _edata;
                _PROCEDURE_LINKAGE_TABLE_;
                _etext;
        SUNW_1.1:
                foo1;
                SUNW_1.1;
        SUNW_1.2:                {SUNW_1.1}:
                foo2;
                SUNW_1.2;
        SUNW_1.2.1 [WEAK]:       {SUNW_1.2}:
                SUNW_1.2.1;
        SUNW_1.3a:               {SUNW_1.2}:
                bar1;
                SUNW_1.3a;
        SUNW_1.3b:               {SUNW_1.2}:
                bar2;
                SUNW_1.3b

In effect, there are six public interfaces being offered by this shared object. Four of these interfaces (SUNW_1.1, SUNW_1.2, SUNW_1.3a and SUNW_1.3b) define a set of functions, one interface (SUNW_1.2.1) describes an internal implementation change to the shared object, and one interface (libfoo.so.1) defines several reserved labels. Dynamic objects that are created with this object will record which of these interfaces they bind to.

The following example creates an application that references both symbols foo1 and foo2. The versioning dependency information recorded in the application can be examined using pvs(1) with the -r option:


$ cat prog.c
extern void foo1();
extern void foo2();

main()
{
        foo1();
        foo2();
}
$ cc -o prog prog.c -L. -R. -lfoo
$ pvs -r prog
        libfoo.so.1 (SUNW_1.2, SUNW_1.2.1);

In this example, the application prog has really bound to the two interfaces SUNW_1.1 and SUNW_1.2, as these interfaces have provided the global symbols foo1 and foo2 respectively.

However, since version definition SUNW_1.1 is defined within libfoo.so.1 as being inherited by the version definition SUNW_1.2, it is only necessary to record the latter version dependency. This normalization of version definition dependencies reduces the amount of version information that must be maintained within an object and processed at runtime.

Since the application prog was built against the shared object's implementation containing the weak version definition SUNW_1.2.1, this dependency is also recorded. Even though this version definition is defined to inherit the version definition SUNW_1.2, the version's weak nature precludes its normalization with SUNW_1.1, and results in a separate dependency recording.

Had there been multiple weak version definitions that inherit from each other, then these definitions will be normalized in the same manner as non-weak version definitions are.


Note -

The recording of a version dependency can be suppressed by the link-editor's -znoversion option.


Having recorded these version definition dependencies, the runtime linker validates the existence of the required version definitions in the objects bound to when the application is executed. This validation can be displayed using ldd(1) with the -v option. For example, by running ldd(1) on the application prog, the version definition dependencies are shown to be found correctly in the shared object libfoo.so.1:


$ ldd -v prog

   find object=libfoo.so.1; required by prog
        libfoo.so.1 =>   ./libfoo.so.1
   find version=libfoo.so.1;
        libfoo.so.1 (SUNW_1.2) =>            ./libfoo.so.1
        libfoo.so.1 (SUNW_1.2.1) =>          ./libfoo.so.1
   ....

Note -

ldd(1) with the -v option implies verbose output, in that a recursive list of all dependencies, together with all versioning requirements, will be generated.


If a non-weak version definition dependency cannot be found, a fatal error will occur during application initialization. Any weak version definition dependency that cannot be found is silently ignored. For example, if the application prog was run in an environment in which libfoo.so.1 only contained the version definition SUNW_1.1, then the following fatal error will occur:


$ pvs -dv libfoo.so.1
        libfoo.so.1;
        SUNW_1.1;
$ prog
ld.so.1: prog: fatal: libfoo.so.1: version `SUNW_1.2' not \
found (required by file prog)

Had the application prog not recorded any version definition dependencies, the nonexistence of the required interface symbol foo2 will have manifested itself some time during the execution of the application as a fatal relocation error (see "Relocation Errors"). This relocation error might occur at process initialization, during process execution, or might not occur at all if the execution path of the application did not call the function foo2.

Recording version definition dependencies provides an alternative and immediate indication of the availability of the interfaces required by the application.

If the application prog was run in an environment in which libfoo.so.1 only contained the version definitions SUNW_1.1 and SUNW_1.2, then all non-weak version definition requirements will be satisfied. The absence of the weak version definition SUNW_1.2.1 is deemed nonfatal, and so no runtime error condition will be generated. However, ldd(1) can be used to display all version definitions that cannot be found:


$ pvs -dv libfoo.so.1
        libfoo.so.1;
        SUNW_1.1;
        SUNW_1.2:                {SUNW_1.1};
$ prog
string used by foo1()
string used by foo2()
$ ldd prog
        libfoo.so.1 =>   ./libfoo.so.1
        libfoo.so.1 (SUNW_1.2.1) =>          (version not found)
        ...........

Note -

If an object requires a version definition from a given dependency, and at runtime an implementation of that dependency is found that contains no version definition information, the version verification of the dependency will be silently ignored. This policy provides a level of backward compatibility as the transition from non-versioned to versioned shared objects is taken. ldd(1), however, can still be used to display any version requirement discrepancies.


Verifying Versions in Additional Objects

Version definition symbols also provide a mechanism for verifying the version requirements of an object obtained by dlopen(3DL). Any object added to the process's address space using this function will have no automatic version dependency verification carried out by the runtime linker. Thus, it is the responsibility of the caller of this function to verify that any versioning requirements are met.

The presence of a required version definition can be verified by looking up the associated version definition symbol using dlsym(3DL). The following example shows the shared object libfoo.so.1 being added to a process by dlopen(3DL) and verified to ensure that the interface SUNW_1.2 is available:


#include        <stdio.h>
#include        <dlfcn.h>
 
main()
{
    void *       handle;
    const char * file = "libfoo.so.1";
    const char * vers = "SUNW_1.2";
    ....
 
    if ((handle = dlopen(file, RTLD_LAZY)) == NULL) {
            (void) printf("dlopen: %s\n", dlerror());
            exit (1);
    }
 
    if (dlsym(handle, vers) == NULL) {
            (void) printf("fatal: %s: version `%s' not found\n",
                 file, vers);
            exit (1);
    }
    ....