For each object added by a basic dlopen(3C), the runtime linker first looks for the symbol in the dynamic executable. The runtime linker then looks in each of the objects provided during the initialization of the process. If the symbol is still not found, the runtime linker continues the search. The runtime linker next looks in the object acquired through the dlopen(3C) and in any of its dependencies.
The default symbol lookup model provides for transitioning into a lazy loading environment. If a symbol can not be found in the presently loaded objects, any pending lazy loaded objects are processed in an attempt to locate the symbol. This loading compensates for objects that have not fully defined their dependencies. However, this compensation can undermine the advantages of a lazy loading.
In the following example, the dynamic executable prog and the shared object B.so.1 have the following 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(3C), 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, think of the shared objects acquired through the dlopen(3C) 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 as shown in the following figure.
Any symbol lookup required by the objects acquired from the dlopen(3C), that is shown as shaded blocks, proceeds 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 the executable 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 the applications initial dependencies. For example, if A.so.1 requires a function relocation after the previous dlopen(3C) has occurred, the runtime linker's normal search for the relocation symbol is to look in prog and then A.so.1. The runtime linker does not 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 is assigned to the dynamic executable and all the dependencies loaded with it. This scope 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 maintain associations between objects. These associations are based on their introduction into the process address space, and on any dependency relationship between the objects. Assigning the objects associated with a given dlopen(3C) to a unique group ensures that only objects associated with the same dlopen(3C) 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(3C). For example, suppose the shared object D.so.1 has the following dependency.
$ ldd D.so.1 E.so.1 => ./E.so.1
and the prog application used dlopen(3C) to load this shared object in addition to the shared object B.so.1. The following figure illustrates the symbol lookup releationship between the objects.
Suppose that 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. Because of the association of objects to a unique group, C.so.1 is bound to the definition in B.so.1, and E.so.1 is bound to the definition in D.so.1. This mechanism is intended to provide the most intuitive binding of objects that are obtained from multiple calls to dlopen(3C).
When objects are used in the scenarios that have so far been described, the order in which each dlopen(3C) 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(3C) calls are made.
In the following example, the shared objects O.so.1 and P.so.1 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(3C) each of these shared objects. Because the shared object Z.so.1 is a common dependency of both O.so.1 and P.so.1, Z.so.1 is assigned to both of the groups that are associated with the two dlopen(3C) calls. This relationship is shown in the following figure.
Z.so.1 is available for both O.so.1 and P.so.1 to look up symbols. More importantly, as far as dlopen(3C) ordering is concerned, Z.so.1 is 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 is affected by the order of the dlopen(3C) 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(3C) ordering.