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.
A handle can be returned from a dlopen(3C) of a named object. The handle enables symbols to be obtained from the named object and the objects that define its dependency tree. A handle returned using the mode RTLD_FIRST, enables symbols to be obtained only from the named object.
A handle can be returned from a dlopen(3C) of a path name whose value is 0. The handle enables symbols to be obtained from the initiating object of the associated link-map and the objects that define its dependency tree. Typically, the initiating object is the dynamic executable. This handle also enables symbols to be obtained from any object obtained by a dlopen(3C) with the RTLD_GLOBAL mode, on the associated link-map. A handle returned using the mode RTLD_FIRST, enables symbols to be obtained only from the initiating object of the associated link-map.
The special handle RTLD_DEFAULT, and RTLD_PROBE enable symbols to be obtained from the initiating object of the associated link-map and objects that define its dependency tree. This handle also enables symbols to be obtained from any object obtained by a dlopen(3C) that belongs to the same group as the caller. Use of RTLD_DEFAULT, or RTLD_PROBE follows the same model as used to resolve a symbolic relocation from the calling object.
The special handle RTLD_NEXT enables symbols to be obtained from the next associated object on the callers link-map list.
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 |
The special handles RTLD_DEFAULT, and RTLD_PROBE enable 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 Default Symbol Lookup Model. For example, the application prog can contain the following code fragment.
if ((fptr = (int (*)())dlsym(RTLD_DEFAULT, "foo")) != NULL) (*fptr)(); |
In this case, foo is searched for in prog, followed by /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 the search for foo continues into B.so.1 and then C.so.1.
This mechanism provides a robust and flexible alternative to the use of undefined weak references, as discussed in Weak Symbols.
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 .......... |
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.