Go to main content

Oracle® Solaris 11.4 Linkers and Libraries Guide

Exit Print View

Updated: March 2019
 
 

Initialization and Termination Routines

Dynamic objects can supply code that provides for runtime initialization and termination processing. The initialization code of a dynamic object is executed once each time the dynamic object is loaded in a process. The termination code of a dynamic object is executed once each time the dynamic object is unloaded from a process or at process termination.

Before transferring control to an application, the runtime linker processes any initialization sections found in the application and any loaded dependencies. If new dynamic objects are loaded during process execution, their initialization sections are processed as part of loading the object. The initialization sections .preinit_array, .init_array, and .init are created by the link-editor when a dynamic object is built.

The runtime linker executes functions whose addresses are contained in the .preinit_array and .init_array sections. These functions are executed in the same order in which their addresses appear in the array. The runtime linker executes an .init section as an individual function. If an object contains both .init and .init_array sections, the .init section is processed before the functions defined by the .init_array section for that object.

An executable can provide pre-initialization functions in a .preinit_array section. These functions are executed after the runtime linker has built the process image and performed relocations but before any other initialization functions. Pre-initialization functions are not permitted in shared objects.


Note -  Any .init section within the executable is called from the application by the process startup mechanism supplied by the compiler driver. The .init section within the executable is called last, after all dependency initialization sections are executed.

Dynamic objects can also provide termination sections. The termination sections .fini_array and .fini are created by the link-editor when a dynamic object is built.

Any termination sections are passed to atexit(3C). These termination routines are called when the process calls exit(2). Termination sections are also called when objects are removed from the running process with dlclose(3C).

The runtime linker executes functions whose addresses are contained in the .fini_array section. These functions are executed in the reverse order in which their addresses appear in the array. The runtime linker executes a .fini section as an individual function. If an object contains both .fini and .fini_array sections, the functions defined by the .fini_array section are processed before the .fini section for that object.


Note -  Any .fini section within the executable is called from the application by the process termination mechanism supplied by the compiler driver. The .fini section of the executable is called first, before all dependency termination sections are executed.

For more information on the creation of initialization and termination sections by the link-editor see Initialization and Termination Sections.

Limitations and Pitfalls of Initialization and Termination Code

ELF initialization and termination sections and routines execute at a sensitive point in the life cycle of the object. During initialization, the object has been loaded into memory, but is not fully initialized. During finalization, the object is still loaded in memory, but is no longer safe to use, and may be partially removed from the process state. In either context, the process state is not fully consistent, and there are significant limits on what code can safely do. Common pitfalls include, but are not limited, to the following.

  • Cyclic dependencies resulting in deadlock, where the initialization code for one object triggers the loading of another object, which in turn calls back into the initial object.

  • Thread serialization failures when a shared object is used in a multithreaded application. Two threads may attempt to access a lazily loaded library at the same time. The thread that gets there first will cause the runtime linker to load the object and start to run the initialization code. Programmers are often under the mistaken impression that the runtime linker can prevent more than one thread from accessing a given object when ELF initialization and termination code is running, but this is not the case. The runtime linker cannot prevent other threads from attempting to access the library once the initialization code is running. It is therefore possible for a second thread to access the object in an inconsistent state. It is the responsibility of the object to serialize such access, either by providing the necessary locks, or my requiring the caller to do so.

ELF initialization and termination sections and routines allow for the execution of arbitrary code, giving the illusion that they are capable of doing anything that code running in a normal context might do. In this view, such code seems like nothing more than a convenient way to do initialization or cleanup without explicit function calls. This misconception leads to failures that can be difficult to diagnose.

Programmers should be cautious in their use of ELF initialization and termination code, and limit the scope and complexity of operations. The link-editor and runtime linker are not cognizant of the content or purpose of such code, and cannot diagnose or prevent unsafe code. Small self contained operations are safe. Operations involving access to other objects or process state may not be. Rather than attempt complex operations in initialization and termination code, libraries should provide explicit initialization and termination functions for their callers to run, and document the requirement to do so.

The following section considers these issues in detail.

Initialization and Termination Order

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.

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 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 objects exhibit a cyclic dependency.

$ elfdump -d B.so.1 | grep NEEDED
     [1]     NEEDED      0xa9    C.so.1
$ elfdump -d C.so.1 | grep NEEDED
     [1]     NEEDED      0xc4    B.so.1
$ elfdump -d main | grep NEEDED
     [1]     NEEDED      0xd6    A.so.1
     [2]     NEEDED      0xc8    B.so.1
     [3]     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[1]:
        ./libC.so.1
        ./libB.so.1

    init object=/lib/libc.so.1
    init object=./A.so.1
    init object=./C.so.1 - cyclic group [1], referenced by:
        ./B.so.1
    init object=./B.so.1 - cyclic group [1], 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 –i –d. 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 Runtime Linker Debugging Facility. 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.

Dynamic initialization is only available when processing lazy references. This dynamic initialization is circumvented by the following.

  • 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 facility 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 parallelization 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.