Linker and Libraries Guide

Obtaining New Symbols

A process can obtain the address of a specific symbol using dlsym(3C). This function takes a handle and a symbol name, and returns the address of the symbol to the caller. The handle directs the search for the symbol in the following manner.

In the following example, which is probably the most common, an application adds additional objects to its address space. The application then uses dlsym(3C) to locate function or data symbols. The application then uses these symbols to call upon services that are provided in these new objects. The file main.c contains the following code.


#include    <stdio.h>
#include    <dlfcn.h>
 
int main()
{
        void *handle;
        int  *dptr, (*fptr)();
 
        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {
                (void) printf("dlopen: %s\n", dlerror());
                return (1);
        }
 
        if (((fptr = (int (*)())dlsym(handle, "foo")) == NULL) ||
            ((dptr = (int *)dlsym(handle, "bar")) == NULL)) {
                (void) printf("dlsym: %s\n", dlerror());
                return (1);
        }
 
        return ((*fptr)(*dptr));
}

The symbols foo and bar are searched for in the file foo.so.1, followed by any dependencies that are associated with this file. The function foo is then called with the single argument bar as part of the return() statement.

The application prog, built using the previous file main.c, contains the following dependencies.


$ ldd prog
        libc.so.1 =>     /lib/libc.so.1

If the file name specified in the dlopen(3C) had the value 0, the symbols foo and bar are searched for in prog, followed by /lib/libc.so.1.

The handle indicates the root at which to start a symbol search. From this root, the search mechanism follows the same model as described in Relocation Symbol Lookup.

If the required symbol cannot be located, dlsym(3C) returns a NULL value. In this case, dlerror(3C) can be used to indicate the true reason for the failure. In the following example, the application prog is unable to locate the symbol bar.


$ prog
dlsym: ld.so.1: main: fatal: bar: can't find symbol

Testing for Functionality

The special handles RTLD_DEFAULT, and RTLD_PROBE enable an application to test for the existence of a symbol.

The RTLD_DEFAULT handle employes the same rules used by the runtime linker to resolve any symbol reference from the calling object. See Default Symbol Lookup Model. Two aspects of this model should be noted.

RTLD_PROBE follows a similar model to RTLD_DEFAULT, but differs in the two aspects noted with RTLD_DEFAULT. RTLD_PROBE only binds to explicit symbol definitions, and is not bound to any procedure linkage table entry within the executable. In addition, RTLD_PROBE does not initiate an exhaustive lazy loading fall back. RTLD_PROBE is the most appropriate flag to use to detect the presence of a symbol within an existing process.

RTLD_DEFAULT and RTLD_PROBE can both initiate an explicit lazy load. An object can make reference to a function, and that reference can be established through a lazy loadable dependency. Prior to calling this function, RTLD_DEFAULT or RTLD_PROBE can be used to test for the existence of the function. Because the object makes reference to the function, an attempt is first made to load the associated lazy dependency. The rules for RTLD_DEFAULT and RTLD_PROBE are then followed to bind to the function. In the following example, an RTLD_PROBE call is used both to trigger a lazy load, and to bind to the loaded dependency if the dependency exists.

void foo()
{
    if (dlsym(RTLD_PROBE, "foo1")) {
        foo1(arg1);
        foo2(arg2);
        ....
}

To provide a robust and flexible model for testing for functionally, the associated lazy dependencies should be explicitly tagged as deferred. See Providing an Alternative to dlopen(). This tagging also provides a means of changing the deferred dependency at runtime.

The use of RTLD_DEFAULT or RTLD_PROBE provide a more robust alternative to the use of undefined weak references, as discussed in Weak Symbols.

Using Interposition

The special handle RTLD_NEXT enables an application to locate the next symbol in a symbol scope. For example, the application prog can contain the following code fragment.

        if ((fptr = (int (*)())dlsym(RTLD_NEXT, "foo")) == NULL) {
                (void) printf("dlsym: %s\n", dlerror());
                return (1);
        }
 
        return ((*fptr)());

In this case, foo is searched for in the shared objects associated with prog, which in this case is /lib/libc.so.1. If this code fragment was contained in the file B.so.1 from the example that is shown in Figure 3–1, then foo is searched for in C.so.1 only.

Use of RTLD_NEXT provides a means to exploit symbol interposition. For example, a function within an object can be interposed upon by a preceding object, which can then augment the processing of the original function. For example, the following code fragment can be placed in the shared object malloc.so.1.

#include    <sys/types.h>
#include    <dlfcn.h>
#include    <stdio.h>
 
void *
malloc(size_t size)
{
        static void *(*fptr)() = 0;
        char        buffer[50];
 
        if (fptr == 0) {
                fptr = (void *(*)())dlsym(RTLD_NEXT, "malloc");
                if (fptr == NULL) {
                        (void) printf("dlopen: %s\n", dlerror());
                        return (NULL);
                }
        }
 
        (void) sprintf(buffer, "malloc: %#x bytes\n", size);
        (void) write(1, buffer, strlen(buffer));
        return ((*fptr)(size));
}

malloc.so.1 can be interposed before the system library /lib/libc.so.1 where malloc(3C) usually resides. Any calls to malloc() are now interposed upon before the original function is called to complete the allocation.


$ cc -o malloc.so.1 -G -K pic malloc.c
$ cc -o prog file1.o file2.o ..... -R. malloc.so.1
$ prog
malloc: 0x32 bytes
malloc: 0x14 bytes
..........

Alternatively, the same interposition can be achieved using the following commands.


$ cc -o malloc.so.1 -G -K pic malloc.c
$ cc -o prog main.c
$ LD_PRELOAD=./malloc.so.1 prog
malloc: 0x32 bytes
malloc: 0x14 bytes
..........

Note –

Users of any interposition technique must be careful to handle any possibility of recursion. The previous example formats the diagnostic message using sprintf(3C), instead of using printf(3C) directly, to avoid any recursion caused by printf(3C) possibly using malloc(3C).


The use of RTLD_NEXT within a dynamic executable or preloaded object, provides a predictable interposition technique. Be careful when using this technique in a generic object dependency, as the actual load order of objects is not always predictable.