To determine the order of executing initialization and termination code within a process at runtime is a complex procedure that involves dependency analysis. This procedure has evolved substantially from the original inception of initialization and termination sections. This procedure attempts to fulfill the expectations of modern languages and current programming techniques. However, scenarios can exist, where user expectations are hard to meet. Flexible, predictable runtime behavior can be achieved by understanding these scenarios together with limiting the content of initialization code and termination code.
The goal of an initialization section is to execute a small piece of code before any other code within the same object is referenced. The goal of a termination section is to execute a small piece of code after an object has finished executing. Self contained initialization sections and termination sections can easily satisfy these requirements.
However, initialization sections are typically more complex and make reference to external interfaces that are provided by other objects. Therefore, a dependency is established where the initialization section of one object must be executed before references are made from other objects. Applications can establish an extensive dependency hierarchy. In addition, dependencies can creating cycles within their hierarchies. The situation can be further complicated by initialization sections that load additional objects, or change the relocation mode of objects that are already loaded. These issues have resulted in various sorting and execution techniques that attempt to satisfy the original goal of these sections.
Prior to the Solaris 2.6 release, dependency initialization routines were called in reverse load order, which is the reverse order of the dependencies displayed with ldd(1). Similarly, dependency termination routines were called in load order. However, as dependency hierarchies became more complex, this simple ordering approach became inadequate.
With the Solaris 2.6 release, the runtime linker constructs a topologically sorted list of objects that have been loaded. This list is built from the dependency relationship expressed by each object, together with any symbol bindings that occur outside of the expressed dependencies.
Prior to the Solaris 8 10/00 release, the environment variable LD_BREADTH could be set to a non-null value. This setting forced the runtime linker to execute initialization and termination sections in pre-Solaris 2.6 release order. This functionality has since been disabled, as the initialization dependencies of many applications have become complex and mandate topological sorting. Any LD_BREADTH setting is now silently ignored.
Initialization sections are executed in the reverse topological order of the dependencies. If cyclic dependencies are found, the objects that form the cycle cannot be topologically sorted. The initialization sections of any cyclic dependencies are executed in their reverse load order. Similarly, termination sections are called in the topological order of the dependencies. The termination sections of any cyclic dependencies are executed in their load order.
A static analysis of the initialization order of an object's dependencies can be obtained by using ldd(1) with the -i option. For example, the following dynamic executable and its dependencies exhibit a cyclic dependency.
$ elfdump -d B.so.1 | grep NEEDED  NEEDED 0xa9 C.so.1 $ elfdump -d C.so.1 | grep NEEDED  NEEDED 0xc4 B.so.1 $ elfdump -d main | grep NEEDED  NEEDED 0xd6 A.so.1  NEEDED 0xc8 B.so.1  NEEDED 0xe4 libc.so.1 $ ldd -i main A.so.1 => ./A.so.1 B.so.1 => ./B.so.1 libc.so.1 => /lib/libc.so.1 C.so.1 => ./C.so.1 libm.so.2 => /lib/libm.so.2 cyclic dependencies detected, group: ./libC.so.1 ./libB.so.1 init object=/lib/libc.so.1 init object=./A.so.1 init object=./C.so.1 - cyclic group , referenced by: ./B.so.1 init object=./B.so.1 - cyclic group , referenced by: ./C.so.1
The previous analysis resulted solely from the topological sorting of the explicit dependency relationships. However, objects are frequently created that do not define their required dependencies. For this reason, symbol bindings are also incorporated as part of dependency analysis. The incorporation of symbol bindings with explicit dependencies can help produce a more accurate dependency relationship. A more accurate static analysis of initialization order can be obtained by using ldd(1) with the -i and -d options.
The most common model of loading objects uses lazy binding. With this model, only immediate reference symbol bindings are processed before initialization processing. Symbol bindings from lazy references might still be pending. These bindings can extend the dependency relationships so far established. A static analysis of the initialization order that incorporates all symbol binding can be obtained by using ldd(1) with the -i and -r options.
In practice, most applications use lazy binding. Therefore, the dependency analysis achieved before computing the initialization order follows the static analysis using ldd -id. However, because this dependency analysis can be incomplete, and because cyclic dependencies can exist, the runtime linker provides for dynamic initialization.
Dynamic initialization attempts to execute the initialization section of an object before any functions in the same object are called. During lazy symbol binding, the runtime linker determines whether the initialization section of the object being bound to has been called. If not, the runtime linker executes the initialization section before returning from the symbol binding procedure.
Dynamic initialization can not be revealed with ldd(1). However, the exact sequence of initialization calls can be observed at runtime by setting the LD_DEBUG environment variable to include the token init. See Debugging Library. Extensive runtime initialization information and termination information can be captured by adding the debugging token detail. This information includes dependency listings, topological processing, and the identification of cyclic dependencies.
Use of the environment variable LD_BIND_NOW.
Objects that have been built with the -z now option.
Objects that are loaded by dlopen(3C) with the mode RTLD_NOW.
The initialization techniques that have been described so far might still be insufficient to cope with some dynamic activities. Initialization sections can load additional objects, either explicitly using dlopen(3C), or implicitly through lazy loading and the use of filters. Initialization sections can also promote the relocations of existing objects. Objects that have been loaded to employ lazy binding have these bindings resolved if the same object is referenced using dlopen(3C) with the mode RTLD_NOW. This relocation promotion effectively suppresses the dynamic initialization capability that is available when resolving a function call dynamically.
Whenever new objects are loaded, or existing objects have their relocations promoted, a topological sort of these objects is initiated. Effectively, the original initialization execution is suspended while the new initialization requirements are established and the associated initialization sections executed. This model attempts to insure that the newly referenced objects are suitably initialized for the original initialization section to use. However, this parallization can be the cause of unwanted recursion.
While processing objects that employ lazy binding, the runtime linker can detect certain levels of recursion. This recursion can be displayed by setting LD_DEBUG=init. For example, the execution of the initialization section of foo.so.1 might result in calling another object. If this object then references an interface in foo.so.1 then a cycle is created. The runtime linker can detect this recursion as part of binding the lazy function reference to foo.so.1.
$ LD_DEBUG=init prog 00905: ....... 00905: warning: calling foo.so.1 whose init has not completed 00905: .......
Recursion that occurs through references that have already been relocated can not be detected by the runtime linker.
Recursion can be expensive and problematic. Reduce the number of external references and dynamic loading activities that can be triggered by an initialization section so as to eliminate recursion.
Initialization processing is repeated for any objects that are added to the running process with dlopen(3C). Termination processing is also carried out for any objects that are unloaded from the process as a result of a call to dlclose(3C).
The preceding sections describe the various techniques that are employed to execute initialization and termination sections in a manner that attempts to meet user expectations. However, coding style and link-editing practices should also be employed to simplify the initialization and termination relationships between dependencies. This simplification helps make initialization processing and termination processing that is predictable, while less prone to any side affects of unexpected dependency ordering.
Keep the content of initialization and termination sections to a minimum. Avoid global constructors by initializing objects at runtime. Reduce the dependency of initialization and termination code on other dependencies. Define the dependency requirements of all dynamic objects. See Generating a Shared Object Output File. Do not express dependencies that are not required. See Shared Object Processing. Avoid cyclic dependencies. Do not depend on the order of an initialization or termination sequence. The ordering of objects can be affected by both shared object and application development. See Dependency Ordering.