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 handle returned from a dlopen(3X) of a named object will allow symbols to be obtained from that objects dependency tree.
The handle returned from a dlopen(3X) of a file whose value is 0 will allow symbols to be obtained from the dynamic executable, from any of its initialization dependencies, or from any object obtained by a dlopen(3X) with the RTLD_GLOBAL mode.
The special handle RTLD_DEFAULT will allow symbols to be obtained from the dynamic executable, from any of its initialization dependencies, or from any object obtained by a dlopen(3X) that belongs to the same group as the caller (see "Testing for Functionality").
The special handle RTLD_NEXT will allow symbols to be obtained from the next associated object (see "Using Interposition").
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.
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".
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 .......... |
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").