Chapter 27. Dynamic Loading at Run Time

Loading shared objects at run time can be a useful way to structure your applications. Done right, it can make your applications extensible, and it also forces you to partition your code into logically separate modules, which is a useful coding discipline.

Many Unix applications, particularly large ones, are mostly implemented by separate blocks of code, often called plugins or modules. In some cases, they are implemented as completely different programs, which communicate with the application’s core code via pipes or some other form of interprocess communication (IPC). In other cases, they are implemented as shared objects.

Shared objects are normally built like standard shared libraries (see Chapter 8), but they are used in a completely different way. The linker is never told about the shared objects, and they do not even need to exist when the application is linked. They do not need to be installed on the system in the same way most shared libraries do.

Just like normal shared libraries, a shared object should be linked explicitly against each library it calls. This ensures that the dynamic loader resolves all external references correctly when the shared object is loaded. If this is not done, then the external references are resolved only in the context of the application loading the shared object in that case. Theoretically, shared objects can be standard object files, but this is not recommended because it does not reliably resolve external shared library dependencies, just like a shared library that is not explicitly linked against all the libraries on which it depends.

The symbol names used in shared objects do not need to be unique among different shared objects loaded into the same program; in fact, they usually are not unique. Different shared objects written for the same interface usually use entry points with the same names. With normal shared libraries, this would be a disaster; with shared objects dynamically loaded at run time, it is the obvious thing to do.

Perhaps the most common use of shared objects loaded at run time is to create an interface to some generic type of capability that might have many different implementations. For instance, consider saving a graphics file. An application might have one internal format for managing its graphics, but there are a lot of file formats in which it might want to save graphics, and more are created on an irregular basis [Murray, 1996]. A generic interface for saving a graphics file that is exported to shared objects loaded at run time allows programmers to add new graphics file formats to the application without recompiling the application. If the interface is well documented, it is even possible for third parties who do not have the application’s source code to add new graphics file formats.

A similar use is framework code that provides only an interface, not an implementation. For example, the PAM (Pluggable Authentication Modules) framework provides a general interface to challenge-response authentication methods, such as usernames and passwords. All the authentication is done by modules, and the choice of which authentication modules to use with which application is done at run time, not compile time, by consulting configuration files. The interface is well defined and stable, and new modules can be dropped into place and used at any time without recompiling either the framework or the application. The framework is loaded as a shared library, and code in that shared library loads and unloads the modules that provide the authentication methods.

The dl Interface

Dynamic loading consists of opening a library, looking up any number of symbols, handling any errors that occur, and closing the library. All the dynamic loading functions are declared in one header file, <dlfcn.h>, and are defined in libdl (link the application with -ldl to use the dynamic loading functionality).

The dlerror() function returns a string describing the most recent error that occurred in one of the other three dynamic loading functions:

const char * dlerror (void);

Each time it returns a value, it clears the error condition. Until another error condition is created, it continues to return NULL instead of a string. The reason for this unusual behavior is detailed in the description of the dlsym() function.

The dlopen() function opens a library. This involves finding the library file, opening the file, and doing some preliminary processing. Environment variables and the options passed to dlopen() determine the details:

void * dlopen (const char *filename, int flag);

If filename is an absolute path (that is, it begins with a / character), dlopen() does not need to search for the library. This is the usual way to use dlopen() from within application code. If filename is a simple file name, dlopen() searches for the filename library in these places:

  • A colon-separated set of directories specified in the environment variable LD_ELF_LIBRARY_PATH, or, if LD_ELF_LIBRARY_PATH does not exist, in LD_LIBRARY_PATH.

  • The libraries specified in the file /etc/ld.so.cache. That file is generated by the ldconfig program, which lists every library it finds in a directory listed in /etc/ld.so.conf at the time that it is run.

  • /usr/lib

  • /lib

If filename is NULL, dlopen() opens an instance of the current executable. This is useful only in rare cases. dlopen() returns NULL on failure.

Finding the files is the easy part of dlopen()’s job; resolving the symbols is more complex. There are two fundamentally different types of symbol resolution, immediate and lazy. Immediate resolution causes dlopen() to resolve all the unresolved symbols before it returns; lazy resolution means that symbol resolution occurs on demand.

If most of the symbols will end up being resolved in the end, it is more efficient to perform immediate resolution. However, for libraries with many unresolved symbols, the time spent resolving the symbols may be noticeable; if this significantly affects your user interface, you may prefer lazy resolution. The difference in overall efficiency is not significant.

While developing and debugging, you will almost always want to use immediate resolution. If your shared objects have unresolvable symbols, you want to know about it immediately, not when your program crashes in the middle of seemingly unrelated code. Lazy resolution will be a source of hard-to-reproduce bugs if you do not test your shared objects with immediate resolution first.

This is especially true if you have shared objects that depend on other shared objects to supply some of their symbols. If shared object A depends on a symbol b in shared object B, and B is loaded after A, lazy resolution of b will succeed if it happens after B is loaded, but it will fail before B is loaded. Developing your code with immediate resolution enabled will help you catch this type of bug before it causes problems.

This implies that you should always load modules in reverse order of their dependencies: If A depends on B for some of its symbols, you should load B before you load A, and you should unload A before you unload B. Fortunately, most applications of dynamically loaded shared objects have no such interdependencies.

By default, the symbols in a shared object are not exported and so will not be used to resolve symbols in other shared objects. They will be available only for you to look up and use, as will be described in the next section. However, you may choose to export all the symbols in a shared object to all other shared objects; they will be available to all subsequently loaded shared objects.

All of this is controlled by the flags argument. It must be set to RTLD_LAZY for lazy resolution or RTLD_NOW for immediate resolution. Either of these may be OR’ed with RTLD_GLOBAL in order to export its symbols to other modules.

If the shared object exports a routine named _init, that routine is run before dlopen() returns.

The dlopen() function returns a handle to the shared object it has opened. This is an opaque object handle that you should use only as an argument to subsequent dlsym() and dlclose() function calls. If a shared object is opened multiple times, dlopen() returns the same handle each time, and each call increments a reference count.

The dlsym() function looks up a symbol in a library:

void * dlsym (void *handle, char *symbol);

The handle must be a handle returned by dlopen(), and symbol is a NULL terminated string naming the symbol you wish to look up. dlsym() returns the address of the symbol that you specified, or it returns NULL if a fatal error occurs. In cases in which you know that NULL is not the correct address of the symbol (such as looking up the address of a function), you can test for errors by checking to see if it returned NULL. However, in the more general case, some symbols may be zero-valued and be equal to NULL. In those cases, you need to see if dlerror() returns an error. Since dlerror() returns an error only once and then reverts to returning NULL, you should use code like this:

/* clear any error condition that has not been read yet */
dlerror();
p = dlsym(handle, "this_symbol");
if ((error = dlerror()) != NULL) {
    /* error handling */
}

Since dlsym() returns a void *, you need to use casts to make the C compiler stop complaining. When you store the pointer that dlsym() returns, store it in a variable of the type that you want to use, and make your cast when you call dlsym(). Do not store the result in a void * variable; you would have to cast it every time you use it.

The dlclose() function closes a library.

void * dlclose (void *handle);

dlclose() checks the reference count that was incremented on each duplicate dlopen() call, and if it is zero, it closes the library. This reference count allows libraries to use dlopen() and dlclose() on arbitrary objects without worrying that the code that called it has already opened any of those objects.

Example

In Chapter 8, we presented an example of using a normal shared library. The shared library that we built, libhello.so, can also be loaded at run time. The loadhello program loads libhello.so dynamically and calls the print_hello function it loads from the library.

Here is loadhello.c:

 1: /* loadhello.c */
 2:
 3: #include <dlfcn.h>
 4: #include <stdio.h>
 5: #include <stdlib.h>
 6:
 7: typedef void (*hello_function)(void);
 8:
 9: int main (void) {
10:    void *library;
11:    hello_function hello;
12:    const char *error;
13:
14:    library = dlopen("libhello.so", RTLD_LAZY);
15:    if (library == NULL) {
16:       fprintf(stderr, "Could not open libhello.so: %s
",
17:               dlerror());
18:       exit(1);
19:    }
20:
21:    /* while in this case we know that the print_hello symbol
22:     * should never be null, that is not true for looking up
23:     * arbitrary symbols. So we demonstrate checking dlerror()'s
24:     * return code instead of dlsym()'s.
25:     */
26:    dlerror();
27:    hello = dlsym(library, "print_hello");
28:    error = dlerror();
29:    if (error) {
30:       fprintf(stderr, "Could not find print_hello: %s
", error);
31:       exit(1);
32:    }
33:
34:    (*hello)();
35:    dlclose(library);
36:    return 0;
37: }
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset