To determine the order of executing initialization and termination code within a process at runtime is a complex issue involving dependency analysis. This process has evolved substantially from the original inception of initialization and termination sections. This process attempts to fulfill the expectations of modern languages and current programming techniques. However, scenarios can exist, where user expectations are hard to meet. Understanding these scenarios, and limiting the content of initialization and termination code can provide both flexible and predictable runtime behavior.
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.
Starting 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.
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 routines are called in the topological order of dependencies and any cyclic dependencies are executed in their load order.
Use ldd(1) with the -i option to display the initialization order of an object's dependencies. For example, the following dynamic executable and its dependencies exhibit a cyclic dependency:
$ dump -Lv B.so.1 | grep NEEDED  NEEDED C.so.1 $ dump -Lv C.so.1 | grep NEEDED  NEEDED B.so.1 $ dump -Lv main | grep NEEDED  NEEDED A.so.1  NEEDED B.so.1  NEEDED libc.so.1 $ ldd -i main A.so.1 => ./A.so.1 B.so.1 => ./B.so.1 libc.so.1 => /usr/lib/libc.so.1 C.so.1 => ./C.so.1 libdl.so.1 => /usr/lib/libdl.so.1 cyclic dependencies detected, group: ./libC.so.1 ./libB.so.1 init object=/usr/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
Prior to Solaris 8 10/00, the environment variable
LD_BREADTH could be set to a non-null value to force the runtime linker to execute
initialization and termination sections in pre-Solaris 2.6 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 processing is repeated for any objects added to the running process with dlopen(3DL). Termination processing is also carried out for any objects unloaded from the process as a result of a call to dlclose(3DL).
Symbol bindings are incorporated as part of dependency analysis because many shared objects exist that do not express their dependencies accurately. Incorporating symbol bindings can therefore help produce a more accurate dependency relationship. However, the addition of symbol binding information to objects that do not express all their dependencies, may still be insufficient to determine an objects complete dependencies. 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 may still be pending, and may extend the dependency relationships so far established.
As the dependency analysis of an object may be incomplete, and as cyclic dependencies often exist, the runtime linker also provides for dynamic initialization. This initialization attempts to execute any initialization sections before any functions in the same object are called. During lazy symbol binding, the runtime linker determines whether the initialization sections of the object being bound to have been called. If not, the runtime linker calls them 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 basic.
See Debugging Library.
Dynamic initialization is only available when processing lazy references. Use of the environment variable
LD_BIND_NOW, objects built with the -z
now option, or objects referenced by dlopen(3DL) with
mode RTLD_NOW, circumvent any dynamic initialization.
Objects that are pending initialization, and are referenced through dlopen(3DL), will be initialized prior to returning control from this function.
The preceding sections describe the various techniques 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 keep initialization and termination processing predictable, and 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. Explicitly 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.