As described in "Relocation Processing", after locating and mapping any objects, the runtime linker must process each object and perform any necessary relocations. Any objects brought into the process's address space with dlopen(3X) must also be relocated in the same manner.
For simple applications this process might be quite uninteresting. However, for users who have more complex applications with many dlopen(3X) calls involving many objects, possibly with common dependencies, this topic can be quite important.
Relocations can be categorized according to when they occur. The default behavior of the runtime linker is to process all data reference relocations at initialization and all function references during process execution, a mechanism commonly referred to as lazy binding.
This same mechanism is applied to any objects added with dlopen(3X) when the mode is defined as RTLD_LAZY. An alternative is to require all relocations of an object to be performed immediately when the object is added. This can be achieved by using a mode of RTLD_NOW, or by recording this requirement in the object when it was built using the link-editors' -z now option. This relocation requirement is propagated to any dependencies of the object being opened.
Relocations can also be categorized into non-symbolic and symbolic. The remainder of this section covers issues regarding symbolic relocations, regardless of when these relocations occur, with a focus on some of the subtleties of symbol lookup.
If an object acquired by dlopen(3X) refers to a global symbol, the runtime linker must locate this symbol from the pool of objects that makeup the process. A default symbol search model is applied to objects obtained by dlopen(3X), and this is described in the following sections. However, the mode of a dlopen(3X), combined with the attributes of the objects that makeup the process, provide for alternative symbol search models.
Two attributes of an object effect symbol lookup. The first is the requesting objects symbol search scope, and the second is the symbol visibility offered by each object within the process. An objects' search scope can be:
The object can look in any other global object within the process.
The object can only look in an object of the same group. The dependency tree created from an object obtained with dlopen(3X), or from an object built using the link-editors' -B group option, forms a unique group.
The visibility of a symbol from an object can be:
By default, objects obtained with dlopen(3X) are assigned world symbol search scope, and local symbol visibility. The following section "Default Symbol Lookup Model", uses this default model to illustrate typical object group interactions. Sections "Defining a Global Object", "Isolating a Group" and "Object Hierarchies" show examples of using dlopen(3X) modes and file attributes to extend the default symbol lookup model.
For each object added by dlopen(3X) the runtime linker will first look for the symbol in the dynamic executable, and then look in each of the objects provided during the initialization of the process. However, if the symbol is still not found, the runtime linker will continue the search, looking in the object acquired through the dlopen(3X) and in any of its dependencies.
For example, let's take the dynamic executable prog, and the shared object B.so.1, each of which has the following (simplified) dependencies:
$ ldd prog A.so.1 => ./A.so.1 $ ldd B.so.1 C.so.1 => ./C.so.1 |
If prog acquires the shared object B.so.1 by dlopen(3X), then any symbol required to relocate the shared objects B.so.1 and C.so.1 will first be looked for in prog, followed by A.so.1, followed by B.so.1, and finally in C.so.1. In this simple case, it might be easier to think of the shared objects acquired through the dlopen(3X) as if they had been added to the end of the original link-edit of the application. For example, the objects referenced in the previous listing can be expressed diagrammatically:
Any symbol lookup required by the objects acquired from the dlopen(3X), shown as shaded blocks, will proceed from the dynamic executable prog through to the final shared object C.so.1.
This symbol lookup is established by the attributes assigned to the objects as they were loaded. Recall that the dynamic executable and all the dependencies loaded with it, are assigned global symbol visibility, and that the new objects are assigned world symbol search scope. Therefore, the new objects are able to look for symbols in the original objects. The new objects also form a unique group in which each object has local symbol visibility. Therefore, each object within the group can look for symbols within the other group members.
These new objects do not affect the normal symbol lookup required by either the application or its initial object dependencies. For example, if A.so.1 requires a function relocation after the above dlopen(3X) has occurred, the runtime linker's normal search for the relocation symbol will be to look in prog and then A.so.1, but not to follow through and look in B.so.1 or C.so.1.
This symbol lookup is again a result of the attributes assigned to the objects as they were loaded. The world symbol search scope assigned the dynamic executable and all the dependencies loaded with it, does not allow them to look for symbols in the new objects that only offer local symbol visibility.
These symbol search and symbol visibility attributes thus maintain associations between objects based on their introduction into the process address space, and on any dependency relationships between the objects. Assigning the objects associated with a given dlopen(3X) a unique group insures that only objects associated with the same dlopen(3X) are allowed to look up symbols within themselves and their related dependencies.
This concept of defining associations between objects becomes more clear in applications that carry out more than one dlopen(3X). For example, if the shared object D.so.1 has the following dependency:
$ ldd D.so.1 E.so.1 => ./E.so.1 |
and the prog application was to dlopen(3X) this shared object in addition to the shared object B.so.1, then diagrammatically the symbol lookup relationship between the objects can be represented as:
If both B.so.1 and D.so.1 contain a definition for the symbol foo, and both C.so.1 and E.so.1 contain a relocation that requires this symbol, then because of the association of objects to a unique group, C.so.1 will be bound to the definition in B.so.1, and E.so.1 will be bound to the definition in D.so.1. This mechanism is intended to provide the most intuitive binding of objects obtained from multiple calls to dlopen(3X).
When objects are used in the scenarios that have so far been described, the order in which each dlopen(3X) occurs has no effect on the resulting symbol binding. However, when objects have common dependencies the resultant bindings can be affected by the order in which the dlopen(3X) calls are made.
Take for example the shared objects O.so.1 and P.so.1, which have the same common dependency:
$ ldd O.so.1 Z.so.1 => ./Z.so.1 $ ldd P.so.1 Z.so.1 => ./Z.so.1 |
In this example, the prog application will dlopen(3X) each of these shared objects. Because the shared object Z.so.1 is a common dependency of both O.so.1and P.so.1, it will be assigned to both of the groups that are associated with the two dlopen(3X) calls. Diagrammatically this can be represented as:
The result is that Z.so.1 will be available for both O.so.1 and P.so.1 to look up symbols, but more importantly, as far as dlopen(3X) ordering is concerned, Z.so.1 will also be able to look up symbols in both O.so.1 and P.so.1.
Therefore, if both O.so.1 and P.so.1 contain a definition for the symbol foo which is required for a Z.so.1 relocation, the actual binding that occurs is unpredictable because it will be affected by the order of the dlopen(3X) calls. If the functionality of symbol foo differs between the two shared objects in which it is defined, the overall outcome of executing code within Z.so.1 might vary depending on the application's dlopen(3X) ordering.
The default assignment of local symbol visibility to the objects obtained by a dlopen(3X) can be promoted to global by augmenting the mode argument with the RTLD_GLOBAL flag. Under this mode, any objects obtained through a dlopen(3X) can be used by any other objects with world symbol search scope to locate symbols.
In addition, any object obtained by dlopen(3X) with the RTLD_GLOBAL flag will also be available for symbol lookup using dlopen(0) (see "Loading Additional Objects").
If a member of a group, having local symbol visibility, is referenced by another group requiring global symbol visibility, the objects visibility will become a concatenation of both local and global. This promotion of attributes will remain, even if the global group reference is later removed.
The default assignment of world symbol search scope to the objects obtained by a dlopen(3X) can be reduced to group by augmenting the mode argument with the RTLD_GROUP flag. Under this mode, any objects obtained through a dlopen(3X) will only be allowed to look for symbols within their own group.
Objects can have the group symbol search scope assigned to them when they are built by using the link-editors' -B group option.
If a member of a group, having group search capability, is referenced by another group requiring world search capability, the objects search capability will become a concatenation of both group and world. This promotion of attributes will remain, even if the world group reference is later removed.
If an initial object, obtained from a dlopen(3X), was to dlopen(3X) a secondary object, both objects would be assigned to a unique group. This situation can prevent either object from locating symbols from one-another.
In some implementations it is necessary for the initial object to export symbols for the relocation of the secondary object. This requirement can be satisfied by one of two mechanisms:
Making the initial object an explicit dependency of the second object.
Use of the RTLD_PARENT mode flag to dlopen(3X) the secondary object.
If the initial object is an explicit dependency of the secondary object, it will also be assigned to the secondary objects' group, and thus will be able to provide symbols for the secondary objects' relocation.
However, if many objects can dlopen(3X) the secondary object, and each of these initial objects must export the same symbols to satisfy the secondary objects' relocation, then the secondary object cannot be assigned an explicit dependency. In this case, the dlopen(3X) mode of the secondary object can be augmented with the RTLD_PARENT flag. This flag causes the propagation of the secondary objects' group to the initial object in the same manner as an explicit dependency would do.
There is one small difference between these two techniques. In the case of specifying an explicit dependency, the dependency itself becomes part of the secondary objects' dlopen(3X) dependency tree, and thus becomes available for symbol lookup with dlopen(3X). In the case of obtaining the secondary object with RTLD_PARENT, the initial object does not become available for symbol lookup with dlopen(3X).
In the case where a secondary object is obtained by dlopen(3X) from an initial object with global symbol visibility, the RTLD_PARENT mode is both redundant and harmless. This case commonly occurs when dlopen(3X) is called from an application or from one of the dependencies of the application.