Linker and Libraries Guide

Obtaining New Symbols

A process can obtain the address of a specific symbol using dlsym(3X). 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:

The first example is probably the most common. Here an application will add additional objects to its address space and use dlsym(3X) to locate function or data symbols. The application then uses these symbols to call upon services provided in these new objects. For example, let's take the file main.c that contains the following code:


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

Here the symbols foo and bar will be 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.

If the application prog is built using the above file main.c, and its initial dependencies are:


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

then if the filename specified in the dlopen(3X) had the value 0, the symbols foo and bar will be searched for in prog, followed by /usr/lib/libdl.so.1, and finally /usr/lib/libc.so.1.

Once the handle has indicated the root at which to start a symbol search, the search mechanism follows the same model as was described in "Symbol Lookup".

If the required symbol cannot be located, dlsym(3X) will return a NULL value. In this case dlerror(3X) can be used to indicate the true reason for the failure. For example;


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

Here the application prog was unable to locate the symbol bar.

Testing for Functionality

The special handle RTLD_DEFAULT allows an application to test for the existence of another symbol. The symbol search follows the same model as used to relocate the calling object (see "Relocation Processing"). For example, if the application prog contained the following code fragment:


if ((fptr = (int (*)())dlsym(RTLD_DEFAULT, "foo")) != NULL)
        (*fptr)();

then foo will be searched for in prog, followed by /usr/lib/libdl.so.1, and then /usr/lib/libc.so.1. If this code fragment was contained in the file B.so.1 from the example shown in Figure 3-2, then the search for foo will continue into B.so.1 and then C.so.1.

This mechanism provides a robust and flexible alternative to the use of undefined weak references discussed in "Weak Symbols".

Using Interposition

The special handle RTLD_NEXT allows an application to locate the next symbol in a symbol scope. For example, if the application prog contained the following code fragment:


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

return ((*fptr)());

then foo will be searched for in the shared objects associated with prog, in this case, /usr/lib/libdl.so.1 and then /usr/lib/libc.so.1. If this code fragment was contained in the file B.so.1 from the example shown in Figure 3-2, then foo will be searched for in the associated shared object C.so.1 only.

Using RTLD_NEXT provides a means to exploit symbol interposition. For example, an object function can be interposed upon by a preceding object, which can then augment the processing of the original function. If the following code fragment is 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 (0);
        }
    }
 
    (void) sprintf(buffer, "malloc: %#x bytes\n", size);
    (void) write(1, buffer, strlen(buffer));
    return ((*fptr)(size));
}

Then by interposing this shared object between the system library /usr/lib/libc.so.1 where malloc(3C) usually resides, any calls to this function will be interposed on 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, this same interposition can be achieved by:


$ 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(3S), instead of using printf(3S) directly, to avoid any recursion caused by printf(3S)'s use of malloc(3C).


The use of RTLD_NEXT within a dynamic executable or preloaded object provides a predictable and useful interpositioning technique. However, care should be taken when using this technique in a generic object dependency, as the actual load order of objects is not always predictable (see "Dependency Ordering").