• No results found

Initialization and Termination Order

In document Linker and Libraries Guide (Page 99-104)

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 withldd(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.

Caution –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

[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 usingldd(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 usingldd(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 withldd(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 Facility” on page 116. 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 bydlopen(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 usingdlopen(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 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 todlclose(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” on page 47. Do not express dependencies that are not required. See“Shared Object Processing” on page 32. 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” on page 128.

Security

Secure processes have some restrictions applied to the evaluation of their dependencies and runpaths to prevent malicious dependency substitution or symbol interposition.

The runtime linker categorizes a process as secure if theissetugid(2)system call returns true for the process.

For 32–bit objects, the default trusted directories that are known to the runtime linker are /lib/secureand /usr/lib/secure. For 64–bit objects, the default trusted directories that are known to the runtime linker are /lib/secure/64 and /usr/lib/secure/64. The utility crle(1)can be used to specify additional trusted directories that are applicable for secure applications. Administrators who use this technique should ensure that the target directories are suitably protected from malicious intrusion.

If an LD_LIBRARY_PATH family environment variable is in effect for a secure process, only the trusted directories specified by this variable are used to augment the runtime linker's search rules. See“Directories Searched by the Runtime Linker” on page 84.

In a secure process, any runpath specifications provided by the application or any of its dependencies are used. However, the runpath must be a full path name, that is, the path name must start with a ‘/'.

In a secure process, the expansion of the $ORIGIN string is allowed only if the string expands to a trusted directory. See“Security” on page 401. However, should a $ORIGIN expansion match a directory that has already provided dependencies, then the directory is implicitly secure. This directory can be used to provide additional dependencies.

In a secure process, LD_CONFIG is ignored. However, a configuration file that is recorded in a secure application is used. See the -c option ofld(1). A recorded configuration file must be a full path name, that is, the path name starts with a ‘/'. A recorded configuration file that employs the $ORIGIN string is restricted to known trusted directories. Developers who record a

configuration file within a secure application should ensure that the configuration file directory is suitably protected from malicious intrusion. In the absence of a recorded configuration file, a secure process uses the default configuration file, if the configuration file exists. Seecrle(1).

In a secure process, LD_SIGNAL is ignored.

Additional objects can be loaded with a secure process using the LD_PRELOAD or LD_AUDIT environment variables. These objects must be specified as full path names or simple file names.

Full path names are restricted to known trusted directories. Simple file names, in which no ‘/' appears in the name, are located subject to the search path restrictions previously described.

Simple file names resolve only to known trusted directories.

In a secure process, any dependencies that consist of simple file names are processed using the path name restrictions previously described. Dependencies expressed as full path names or relative path names are used as is. Therefore, the developer of a secure process should ensure that the target directory referenced as one of these dependencies is suitably protected from malicious intrusion.

When creating a secure process, do not use relative path names to express dependencies or to constructdlopen(3C)path names. This restriction applies to the application and to all dependencies.

In document Linker and Libraries Guide (Page 99-104)