Linker and Libraries Guide

Chapter 3 Runtime Linker

As part of the initialization and execution of a dynamic executable, an interpreter is called to complete the binding of the application to its dependencies. In the Solaris operating environment, this interpreter is referred to as the runtime linker.

During the link-editing of a dynamic executable, a special .interp section, together with an associated program header, are created. This section contains a path name specifying the program's interpreter. The default name supplied by the link-editor is that of the runtime linker: /usr/lib/ld.so.1 for a 32–bit executable and /usr/lib/64/ld.so.1 for a 64–bit executable.


Note –

ld.so.1 is a special case of a shared object. Here, a version number of 1 is used. However, later Solaris releases might provide higher version numbers.


During the process of executing a dynamic object the kernel loads the file and reads the program header information. See Program Header. From this information the kernel locates the name of the required interpreter. The kernel loads this interpreter and transfers control to it, passing sufficient information to enable the interpreter to continue binding the application and run it.

In addition to initializing an application, the runtime linker provides services that enable the application to extend its address space. This process involves loading additional objects and binding to symbols within them.

The runtime linker:

Shared Object Dependencies

When the runtime linker creates the memory segments for a program, the dependencies tell what shared objects are needed to supply the program's services. By repeatedly connecting referenced shared objects and their dependencies, the runtime linker generates a complete process image.


Note –

Even when a shared object is referenced multiple times in the dependency list, the runtime linker connects the object only once to the process.


Locating Shared Object Dependencies

During the link-edit of a dynamic executable, one or more shared objects are explicitly referenced. These objects are recorded as dependencies within the dynamic executable.

The runtime linker uses this dependency information to locate, and load, the associated objects. These dependencies are processed in the same order as they were referenced during the link-edit of the executable.

Once all the dynamic executable's dependencies are loaded, they too are inspected, in the order they are loaded, to locate any additional dependencies. This process continues until all dependencies are located and loaded. This technique results in a breadth-first ordering of all dependencies.

Directories Searched by the Runtime Linker

The runtime linker looks in one default location for dependencies. This location is /usr/lib when processing 32–bit objects, and /usr/lib/64 when processing 64–bit objects. Any dependency specified as a simple file name is prefixed with this default directory name. The resulting path name is used to locate the actual file.

The dependencies of a dynamic executable or shared object can be displayed using ldd(1). For example, the file /usr/bin/cat has the following dependencies:


$ ldd /usr/bin/cat
        libc.so.1 =>     /usr/lib/libc.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1

The file /usr/bin/cat has a dependency, or needs, the files libc.so.1 and libdl.so.1.

The dependencies recorded in an object can be inspected using dump(1). Use this command to display the file's .dynamic section, and look for entries that have a NEEDED tag. In the following example, the dependency libdl.so.1, displayed in the previous ldd(1) example, is not recorded in the file /usr/bin/cat. ldd(1) shows the total dependencies of the specified file, and libdl.so.1 is actually a dependency of /usr/lib/libc.so.1.


$ dump -Lvp /usr/bin/cat
 
/usr/bin/cat:
[INDEX] Tag      Value
[1]     NEEDED   libc.so.1
.........

In the previous dump(1) example, the dependencies are expressed as simple file names. In other words, there is no `/' in the name. The use of a simple file name requires the runtime linker to generate the path name from a set of rules. File names that contain an embedded `/', are used as provided.

The simple file name recording is the standard, most flexible mechanism of recording dependencies. The -h option of the link-editor records a simple name within the dependency. See Naming Conventions and Recording a Shared Object Name.

Frequently, dependencies are distributed in directories other than /usr/lib or /usr/lib/64. If a dynamic executable or shared object needs to locate dependencies in another directory, the runtime linker must explicitly be told to search this directory.

You can specify additional search path, on a per-object basis, by recording a runpath during the link-edit of an object. See Directories Searched by the Runtime Linker for details on recording this information.

Any runpath recording can be displayed using dump(1). Reference the .dynamic entry that has the RUNPATH tag. In the following example, prog has a dependency on libfoo.so.1. The runtime linker must search directories /home/me/lib and /home/you/lib before it looks in the default location.


$ dump -Lvp prog
 
prog:
[INDEX] Tag      Value
[1]     NEEDED   libfoo.so.1
[2]     NEEDED   libc.so.1
[3]     RUNPATH  /home/me/lib:/home/you/lib
.........

Another way to add to the runtime linker's search path is to set the environment variable LD_LIBRARY_PATH. This environment variable, which is analyzed once at process startup, can be set to a colon-separated list of directories. These directories are searched by the runtime linker before any runpath specification or default directory.

These environment variables are well suited to debugging purposes, such as forcing an application to bind to a local dependency. In the following example, the file prog from the previous example is bound to libfoo.so.1, found in the present working directory.


$ LD_LIBRARY_PATH=. prog

Although useful as a temporary mechanism of influencing the runtime linker's search path, the use of LD_LIBRARY_PATH is strongly discouraged in production software. Any dynamic executables that can reference this environment variable will have their search paths augmented. This augmentation can result in an overall degradation in performance. Also, as pointed out in Using an Environment Variable and Directories Searched by the Runtime Linker, LD_LIBRARY_PATH affects the link-editor.

Environmental search paths can result in a 64–bit executable searching a path that contains a 32–bit library matching the name being looked for. Or, vice versa. The runtime linker rejects the mismatched 32–bit library and continues down its search path looking for a valid 64–bit match. If no match is found, an error message is generated. This can be observed in detail by setting the LD_DEBUG environment variable to include the files token. See Debugging Library.


$ LD_LIBRARY_PATH=/usr/bin/64 LD_DEBUG=files /usr/bin/ls
...
00283: file=libc.so.1;  needed by /usr/bin/ls
00283: 
00283: file=/usr/lib/64/libc.so.1  rejected: ELF class mismatch: \
00283:                                  32-bit/64-bit
00283: 
00283: file=/usr/lib/libc.so.1  [ ELF ]; generating link map
00283:     dynamic:  0xef631180  base:  0xef580000  size:      0xb8000
00283:     entry:    0xef5a1240  phdr:  0xef580034  phnum:           3
00283:      lmid:           0x0
00283: 
00283: file=/usr/lib/libc.so.1;  analyzing  [ RTLD_GLOBAL  RTLD_LAZY ]
...

If a dependency cannot be located, ldd(1) indicates that the object cannot be found. Any attempt to execute the application results in an appropriate error message from the runtime linker:


$ ldd prog
        libfoo.so.1 =>   (file not found)
        libc.so.1 =>     /usr/lib/libc.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1
$ prog
ld.so.1: prog: fatal: libfoo.so.1: open failed: No such file or directory

Configuring the Default Search Paths

The default search paths used by the runtime linker are /usr/lib or /usr/lib/64. These search paths can be administered using a runtime configuration file created by the crle(1) utility. This file is often a useful aid for establishing search paths for applications that have not been built with the correct runpaths.

A configuration file can be constructed in the default location /var/ld/ld.config, for 32–bit applications, or /var/ld/64/ld.config, for 64–bit applications. This file affects all applications of the respective type on a system. Configuration files can also be created in other locations, and the runtime linker's LD_CONFIG environment variable used to select these files. This latter method is useful for testing a configuration file before installing it in the default location.

Dynamic String Tokens

The runtime linker allows for the expansion of various dynamic string tokens. These tokens may be applicable for filter, runpath and dependency definitions.

Relocation Processing

After the runtime linker has loaded all the dependencies required by an application, the linker processes each object and performs all necessary relocations.

During the link-editing of an object, any relocation information supplied with the input relocatable objects is applied to the output file. However, when creating a dynamic executable or shared object, many of the relocations cannot be completed at link-edit time because they require logical addresses that are known only when the objects are loaded into memory. In these cases the link-editor generates new relocation records as part of the output file image. The runtime linker must then process these new relocation records.

For a more detailed description of the many relocation types, see Relocation Types (Processor-Specific). There are two basic types of relocations:

The relocation records for an object can be displayed by using dump(1). In the following example, the file libbar.so.1 contains two relocation records that indicate that the global offset table (the .got section) must be updated.


$ dump -rvp libbar.so.1

libbar.so.1:

.rela.got:
Offset      Symndx                Type              Addend

0x10438     0                     R_SPARC_RELATIVE  0
0x1043c     foo                   R_SPARC_GLOB_DAT  0

The first relocation is a simple relative relocation that can be seen from its relocation type and the symbol index (Symndx) field being zero. This relocation needs to use the base address at which the object was loaded into memory to update the associated .got offset.

The second relocation requires the address of the symbol foo. To complete this relocation, the runtime linker must locate this symbol from either the dynamic executable or one of its dependencies.

Symbol Lookup

When an object requires a symbol, the runtime linker searches for that symbol based upon the requesting object's symbol search scope, and the symbol visibility offered by each object within the process. These attributes are applied as defaults to an object at the time the object is loaded, as specific modes to dlopen(3DL), and in some cases can be recorded within the object at the time it is built.

Typically, an average user becomes familiar with the default symbol search models that are applied to a dynamic executable and its dependencies, and to objects obtained through dlopen(3DL). The former is outlined in the next section Default Lookup, and the latter, which is also able to exploit the various symbol lookup attributes, is discussed in Symbol Lookup.

An alternative model for symbol lookup is provided when a dynamic object is created with the link-editors -B direct option. This model directs the runtime linker to search for a symbol directly in the object that provided the symbol at link-edit time. This model is discussed in more detail in Direct Binding.

Default Lookup

A dynamic executable and all the dependencies loaded with it are assigned world search scope, and global symbol visibility. See Symbol Lookup. When the runtime linker looks up a symbol for a dynamic executable or for any of the dependencies loaded with the executable, it does so by searching each object. The runtime linker starts with the dynamic executable, and progresses through each dependency in the same order in which the objects were loaded.

As discussed in previous sections, ldd(1) lists the dependencies of a dynamic executable in the order in which they are loaded. Therefore, if the shared object libbar.so.1 requires the address of symbol foo to complete its relocation, and this shared object is a dependency of the dynamic executable prog:


$ ldd prog
        libfoo.so.1 =>   /home/me/lib/libfoo.so.1
        libbar.so.1 =>   /home/me/lib/libbar.so.1

The runtime linker first looks for foo in the dynamic executable prog, then in the shared object /home/me/lib/libfoo.so.1, and finally in the shared object /home/me/lib/libbar.so.1.


Note –

Symbol lookup can be an expensive operation, especially when the size of symbol names increases and the number of dependencies increases. This aspect of performance is discussed in more detail in Performance Considerations. See Direct Binding for an alternative lookup model.


Interposition

The runtime linker's default mechanism of searching for a symbol first in the dynamic executable and then in each of the dependencies means that the first occurrence of the required symbol will satisfy the search. Therefore, if more than one instance of the same symbol exists, the first instance interposes on all others. See also Shared Object Processing.

Direct Binding

When creating an object using the link-editor's -B direct option, the relationship between the referenced symbol and the dependency that provided the definition is recorded in the object. The runtime linker uses this information to search directly for the symbol in the associated object, rather than carry out the default symbol search model.


Note –

The use of -B direct also enables lazy loading, which is equivalent to adding the option -z lazyload to the front of the link-edit command line. See Lazy Loading of Dynamic Dependencies.


The direct binding model can significantly reduce the symbol lookup overhead within a dynamic process that has many symbolic relocations and many dependencies. This model also enables multiple symbols of the same name to be located from different objects that have been bound to directly.

Direct binding can circumvent the traditional use of interposition symbols because it bypasses the default search model. The default model ensures that all references to a symbol bind to one definition.

Interposition can still be achieved in a direct binding environment, on a per-object basis, if an object is identified as an interposer. Any object loaded using the environment variable LD_PRELOAD or created with the link-editor's -z interpose option, is identified as an interposer. When the runtime linker searches for a directly bound symbol, it first looks in any object identified as an interposer before it looks in the object that supplies the symbol definition.


Note –

Direct bindings can be disabled at runtime by setting the environment variable LD_NODIRECT to a non-null value.


When Relocations Are Performed

Relocations can be distinguish by when they are performed. This distinction arises due to the type of reference being made to the relocated offset, and is either:

An immediate reference refers to a relocation that must be determined immediately when an object is loaded. These references are typically to data items used by the object code, pointers to functions, and even calls to functions made from position-dependent shared objects. These relocations cannot provide the runtime linker with knowledge of when the relocated item is referenced. Therefore, all immediate relocations must be carried out when an object is loaded, and before the application gains, or regains, control.

A lazy reference refers to a relocation that can be determined as an object executes. These references are typically calls to global functions made from position-independent shared objects, or calls to external functions made from a dynamic executable. During the compilation and link-editing of any dynamic module that provide these references, the associated function calls become calls to a procedure linkage table entry. These entries make up the .plt section. Each procedure linkage table entry becomes a lazy reference with a relocation associated with it.

Procedure linkage table entries are constructed so that when they are first called, control is passed to the runtime linker. The runtime linker looks up the required symbol and rewrites information in the associated object so that any future calls to this procedure linkage table entry go directly to the function. This mechanism enables relocations of this type to be deferred until the first instance of a function is called. This process is sometimes referred to as lazy binding.

The runtime linker's default mode is to perform lazy binding whenever procedure linkage table relocations are provided. This default can be overridden by setting the environment variable LD_BIND_NOW to any non-null value. This environment variable setting causes the runtime linker to perform both immediate and lazy reference relocations when an object is loaded, and before the application gains, or regains, control. For example, setting the environment variable as follows means that all relocations within the file prog and within its dependencies, will be processed before control is transferred to the application.


$ LD_BIND_NOW=1 prog

Objects can also be accessed with dlopen(3DL) with the mode defined as RTLD_NOW. Objects can also be built using the link-editor's -z now option to indicate that they require complete relocation processing at the time they are loaded. This relocation requirement is also propagated to any dependencies of the marked object at runtime.


Note –

Although the preceding examples of immediate and lazy references are typical, the creation of procedure linkage table entries is ultimately controlled by the relocation information provided by the relocatable object files used as input to a link-edit. Relocation records such as R_SPARC_WPLT30 and R_386_PLT32 instruct the link-editor to create a procedure linkage table entry are common for position-independent code. However, as a dynamic executable has a fixed location, external function references that can be determined at link-edit time can be converted to procedure linkage table entries regardless of the original relocation records.


Relocation Errors

The most common relocation error occurs when a symbol cannot be found. This condition results in an appropriate runtime linker error message and the termination of the application. For example:


$ ldd prog
        libfoo.so.1 =>   ./libfoo.so.1
        libc.so.1 =>     /usr/lib/libc.so.1
        libbar.so.1 =>   ./libbar.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1
$ prog
ld.so.1: prog: fatal: relocation error: file ./libfoo.so.1: \
symbol bar: referenced symbol not found

The symbol bar, which is referenced in the file libfoo.so.1, cannot be located.

During the link-edit of a dynamic executable, any potential relocation errors of this sort are flagged as fatal undefined symbols. See Generating an Executable Output File for examples. This runtime relocation error can occur if the link-edit of main used a different version of the shared object libbar.so.1 that contained a symbol definition for bar, or if the -z nodefs option was used as part of the link-edit.

If a relocation error of this type occurs because a symbol used as an immediate reference cannot be located, the error condition will occur immediately during process initialization. Because of the default mode of lazy binding, if a symbol used as a lazy reference cannot be found, the error condition will occur after the application has gained control. This latter case can take minutes or months, or might never occur, depending on the execution paths exercised throughout the code.

To guard against errors of this kind, the relocation requirements of any dynamic executable or shared object can be validated using ldd(1).

When the -d option is specified with ldd(1), all dependencies will be printed and all immediate reference relocations will be processed. If a reference cannot be resolved, a diagnostic message is produced. From the previous example this option would result in:


$ ldd -d prog
        libfoo.so.1 =>   ./libfoo.so.1
        libc.so.1 =>     /usr/lib/libc.so.1
        libbar.so.1 =>   ./libbar.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1
        symbol not found: bar           (./libfoo.so.1)

When the -r option is specified with ldd(1), all immediate and lazy reference relocations are processed. If either type of relocation cannot be resolved, a diagnostic message is produced.

Loading Additional Objects

The runtime linker provides an additional level of flexibility by enabling you to introduce new objects during process initialization.

The environment variable LD_PRELOAD can be initialized to a shared object or relocatable object file name, or a string of file names separated by white space. These objects are loaded after the dynamic executable and before any dependencies. These objects are assigned world search scope, and global symbol visibility.


$ LD_PRELOAD=./newstuff.so.1 prog

The dynamic executable prog is loaded, followed by the shared object newstuff.so.1, and then by the dependencies defined within prog.

The order in which these objects are processed can be displayed using ldd(1):


$ LD_PRELOAD=./newstuff.so.1 ldd prog
        ./newstuff.so.1 => ./newstuff.so
        libc.so.1 =>     /usr/lib/libc.so.1

In another example the preloading is a little more complex and time consuming.


$ LD_PRELOAD="./foo.o ./bar.o" prog

The runtime linker first link-edits the relocatable objects foo.o and bar.o to generate a shared object that is maintained in memory. This memory image is then inserted between the dynamic executable and its dependencies in the same manner as the shared object newstuff.so.1 was preloaded in the previous example. Again, the order in which these objects are processed can be displayed with ldd(1):


$ LD_PRELOAD="./foo.o ./bar.o" ldd prog
        ./foo.o =>       ./foo.o
        ./bar.o =>       ./bar.o
        libc.so.1 =>     /usr/lib/libc.so.1

These mechanisms of inserting an object after a dynamic executable take the concept of interposition to another level. You can use these mechanisms to experiment with a new implementation of a function that resides in a standard shared object. If you preload an object containing this function, the object interposes on the original. Thus the old functionality can be completely hidden with the new preloaded version.

Another use of preloading is to augment a function that resides in a standard shared object. The intention is to interpose the new symbol on the original, enabling the new function to carry out some additional processing while calling through to the original function. This mechanism requires either a symbol alias that is to be associated with the original function or the ability to look up the original symbol's address.

Lazy Loading of Dynamic Dependencies

When a dynamic object is loaded into memory, it is examined for any additional dependencies. By default, if any dependencies exist they are immediately loaded. This cycle continues until the full dependency tree is exhausted. At which point all inter-object references, specified by relocations, are resolved.

Under this default model, all the dependencies of an application are loaded into memory, and all data relocations are performed. These operations are performed regardless of whether the code in these dependencies is referenced by the application during its execution.

Under a lazy loading model, any dependencies that are labeled for lazy loading are loaded only when explicitly referenced. By taking advantage of a function call's lazy binding, the loading of a dependency is delayed until it is first referenced. In fact, objects that are never referenced are never loaded.

A relocation reference can be immediate or lazy. Because immediate references must be resolved when an object is initialized, any dependency that satisfies this reference must be immediately loaded. Therefore, identifying such a dependency as lazy loadable has little effect. See When Relocations Are Performed. Immediate references between dynamic objects are generally discouraged.

Lazy loading is used by the link-editor itself, which references a debugging library, liblddbg. Because debugging is only called upon infrequently, loading this library every time the link-editor is invoked is unnecessary and expensive. By indicating that this library can be lazily loaded, the expense of processing it can be moved to those invocations that ask for debugging output.

The alternate method of achieving a lazy loading model is to use dlopen() and dlsym() to load and bind to a dependency when needed. This is ideal if the number of dlsym() references is small, or the dependency name or location is not known at link-edit time. For more complex interactions with known dependencies, coding to normal symbol references and designating the dependency to be lazily loaded is simpler.

An object is designated as lazily or normally loaded through the link-editor options -z lazyload and -z nolazyload respectfully. These options are position-dependent on the link-edit command line. Any dependency found following the option takes on the loading attribute specified by the option. By default, the -z nolazyload option is in effect.

The following simple program has a dependency on libdebug.so.1. The dynamic section (.dynamic), shows libdebug.so.1 is marked for lazy loading. The symbol information section (.SUNW_syminfo), shows the symbol reference that triggers libdebug.so.1 loading.


$ cc -o prog prog.c -L. -zlazyload -ldebug -znolazyload -R'$ORIGIN'
$ elfdump -d prog
 
Dynamic Section:  .dynamic
     index  tag           value
       [0]  POSFLAG_1     0x1           [ LAZY ]
       [1]  NEEDED        0x123         libdebug.so.1
       [2]  NEEDED        0x131         libc.so.1
       [3]  RUNPATH       0x13b         $ORIGIN
       ...
$ elfdump -y prog
 
Syminfo section: .SUNW_syminfo
     index flgs  boundto                symbol
	      ...
      [52] DL       [1] libdebug.so.1   debug

The POSFLAG_1 with the value of LAZY designates that the following NEEDED entry, libdebug.so.1, should be lazily loaded. Because libc.so.1 has no preceding LAZY flag it is loaded at the initial startup of the program.

The use of lazy loading can require a precise declaration of dependencies and runpaths through out the objects used by an application. For example, suppose two objects, libA.so and libB.so, both make reference to symbols in libX.so. libA.so declares it has a dependency on libX.so, but libB.so does not. Typically, when libA.so and libB.so are used together, libB.so can reference libX.so because libA.so made it available. But, if libA.so declares libX.so to be lazy loaded, it is possible that libX.so may not be loaded when libB.so makes reference to it. A similar failure can occur if libB.so declares libX.so as a dependency but fails to provide a runpath necessary to locate it.

Regardless of lazy loading, it is recommended that dynamic objects declare all their dependencies and how to locate them. With lazy loading, this dependency information becomes even more important.


Note –

Lazy loading can be disabled at runtime by setting the environment variable LD_NOLAZYLOAD to a non-null value.


Initialization and Termination Routines

Before transferring control to an application, the runtime linker processes any initialization sections found in the application and any loaded dependencies. 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.

A dynamic executable may 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 dynamic executable is called from the application itself by the process startup mechanism supplied by the compiler driver. The .init section within the dynamic 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 organized such that they can be recorded by atexit(3C). These routines are called when the process calls exit(2), or when objects are removed from the running process with dlclose(3DL).

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 dynamic executable is called from the application itself by the process termination mechanism supplied by the compiler driver. The .fini section of the dynamic executable is called first, before all dependency termination sections are executed.


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

Initialization and Termination Order

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
[1]     NEEDED      C.so.1
$ dump -Lv C.so.1 | grep NEEDED
[1]     NEEDED      B.so.1
$ dump -Lv main | grep NEEDED
[1]     NEEDED      A.so.1
[2]     NEEDED      B.so.1
[3]     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[1]:
        ./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 [1], referenced by:
        ./B.so.1
   init object=./B.so.1 - cyclic group [1], referenced by:
        ./C.so.1

Caution – Caution –

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.


Note –

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

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 the user is not a super-user, and the real user and effective user identifiers are not equal. Similarly, if the user is not a super-user and the real group and effective group identifiers are not equal, the process is deemed secure. See the getuid(2), geteuid(2), getgid(2) and getegid(2) man pages.

The default trusted directory known to the runtime linker is /usr/lib/secure for 32–bit objects or /usr/lib/secure/64 for 64–bit objects. The utility crle(1) may be used to specify additional trusted directories 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.

In a secure process, any runpath specifications provided by the application or any of its dependencies is used, provided it is a full pathname, that is, the pathname starts with a `/').

In a secure process, the expansion of the $ORIGIN string is allowed only if it expands to a trusted directory. See Security.

In a secure process, LD_CONFIG is ignored. A secure process uses the default configuration file, if it exists. See crle(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 or relative path names are used as is. Therefore, the developer of a secure process should ensure that the target directory referenced as a full or relative path name dependency is suitably protected from malicious intrusion.

When creating a secure process, do not use relative path names to express dependencies or to construct dlopen(3DL) path names. This restriction should be applied to the application and to all dependencies.

Runtime Linking Programming Interface

Dependencies specified during the link-edit of an application are processed by the runtime linker during process initialization. In addition to this mechanism, the application can extend its address space during its execution by binding to additional objects. The application can request the same services of the runtime linker that are used to process the dependencies specified during the link-edit of the application.

This delayed object binding has several advantages:

An application can use the following typical scenario to access an additional shared object.

The services of the runtime linker are defined in the header file dlfcn.h and are made available to an application by the shared object libdl.so.1. In the following example, the file main.c can make reference to any of the dlopen(3DL) family of routines, and the application prog can bind to these routines at runtime.


$ cc -o prog main.c -ldl

Loading Additional Objects

Additional objects can be added to a running process's address space using dlopen(3DL). This function takes a path name and a binding mode as arguments, and returns a handle to the application. This handle can be used to locate symbols for use by the application using dlsym(3DL).

If the path name is specified as a simple file name, one with no `/' in the name, then the runtime linker will use a set of rules to generate an appropriate path name. Path names that contain a `/' will be used as provided.

These search path rules are exactly the same as are used to locate any initial dependencies. See Directories Searched by the Runtime Linker. For example, if the file main.c contains the following code fragment:


#include        <stdio.h>
#include        <dlfcn.h>
 
main(int argc, char ** argv)
{
        void *  handle;
        .....
 
        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {
                (void) printf("dlopen: %s\n", dlerror());
                exit (1);
        }
        .....

then to locate the shared object foo.so.1, the runtime linker uses any LD_LIBRARY_PATH definition present at process initialization, followed by any runpath specified during the link-edit of prog. Finally, the runtime linker uses the default location /usr/lib for 32–bit objects, and /usr/lib/64 for 64–bit objects.

If the path name is specified as:


        if ((handle = dlopen("./foo.so.1", RTLD_LAZY)) == NULL) {

then the runtime linker searches for the file only in the current working directory of the process.


Note –

Any shared object specified using dlopen(3DL) should be referenced by its versioned file name. For more information on versioning, see Coordination of Versioned Filenames.


If the required object cannot be located, dlopen(3DL) returns a NULL handle. In this case dlerror(3DL) can be used to display the true reason for the failure. For example:


$ cc -o prog main.c -ldl
$ prog
dlopen: ld.so.1: prog: fatal: foo.so.1: open failed: No such \
file or directory

If the object being added by dlopen(3DL) has dependencies on other objects, they too are brought into the process's address space. This process continues until all the dependencies of the specified object are loaded. This dependency tree is referred to as a group.

If the object specified by dlopen(3DL), or any of its dependencies, are already part of the process image, then the objects are not processed any further. A valid handle is returned to the application. This mechanism prevents the same object from being loaded more than once, and enables an application to obtain a handle to itself. For example, if the previous main.c example contained the following dlopen() call:


        if ((handle = dlopen((const char *)0, RTLD_LAZY)) == NULL) {

then the handle returned from dlopen(3DL) can be used to locate symbols within the application itself, within any of the dependencies loaded as part of the process's initialization, or within any objects added to the process's address space, using a dlopen(3DL) that specified the RTLD_GLOBAL flag.

Relocation Processing

As described in Chapter 3, Runtime Linker, after locating and loading any objects, the runtime linker must process each object and perform any necessary relocations. Any objects brought into the process's address space with dlopen(3DL) must also be relocated in the same manner.

For simple applications this process is straightforward. However, for users who have more complex applications with many dlopen(3DL) calls involving many objects, possibly with common dependencies, this process can be quite important.

Relocations can be categorized according to when they occur. The default behavior of the runtime linker is to process all immediate reference relocations at initialization and all lazy references during process execution, a mechanism commonly referred to as lazy binding.

This same mechanism is applied to any objects added with dlopen(3DL) when the mode is defined as RTLD_LAZY. An alternative is to require all relocations of an object to be performed immediately when the object is added. You can use a mode of RTLD_NOW, or record this requirement in the object when it is built using the link-editor's -z now option. This relocation requirement is propagated to any dependencies of the object being opened.

Relocations can also be categorized into non-symbolic and symbolic. The remainder of this section covers issues regarding symbolic relocations, regardless of when these relocations occur, with a focus on some of the subtleties of symbol lookup.

Symbol Lookup

If an object acquired by dlopen(3DL) refers to a global symbol, the runtime linker must locate this symbol from the pool of objects that make up the process. In the absence of direct binding, a default symbol search model is applied to objects obtained by dlopen(3DL). However, the mode of a dlopen(3DL), combined with the attributes of the objects that make up the process, provide for alternative symbol search models.

Objects that required direct binding, although maintaining all the attributes described later, search for symbols directly in the associated dependency. See Direct Binding.

Two attributes of an object affect symbol lookup. The first is the requesting object's symbol search scope, and the second is the symbol visibility offered by each object within the process. An object's search scope can be:

world

The object can look in any other global object within the process.

group

The object can look only in an object of the same group. The dependency tree created from an object obtained with dlopen(3DL), or from an object built using the link-editor's -B group option, forms a unique group.

The visibility of a symbol from an object can be:

global

The object's symbols can be referenced from any object having world search scope.

local

The object's symbols can be referenced only from other objects that make up the same group.

By default, objects obtained with dlopen(3DL) are assigned world symbol search scope, and local symbol visibility. The section, Default Symbol Lookup Model, uses this default model to illustrate typical object group interactions. The sections Defining a Global Object, Isolating a Group, and Object Hierarchies show examples of using dlopen(3DL) modes and file attributes to extend the default symbol lookup model.

Default Symbol Lookup Model

For each object added by dlopen(3DL) 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, looking in the object acquired through the dlopen(3DL) and in any of its dependencies.

In the following example, the dynamic executable prog and the shared object B.so.1 each have the following (simplified) 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(3DL), 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(3DL) 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.

Figure 3–1 A Single dlopen() Request

A single dlopen() request.

Any symbol lookup required by the objects acquired from the dlopen(3DL), 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 it 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 its initial object dependencies. For example, if A.so.1 requires a function relocation after the above dlopen(3DL) 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 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(3DL) to a unique group ensures that only objects associated with the same dlopen(3DL) 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(3DL). 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(3DL) 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.

Figure 3–2 Multiple dlopen() Requests

Multiple dlopen() requests.

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 obtained from multiple calls to dlopen(3DL).

When objects are used in the scenarios that have so far been described, the order in which each dlopen(3DL) 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(3DL) 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(3DL) 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(3DL) calls. This relationship is shown in the following figure.

Figure 3–3 Multiple dlopen() Requests With A Common Dependency

Multiple dlopen() requests with a common dependency.

Z.so.1 is available for both O.so.1 and P.so.1 to look up symbols. More importantly, as far as dlopen(3DL) 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(3DL) 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(3DL) ordering.

Defining a Global Object

The default assignment of local symbol visibility to the objects obtained by a dlopen(3DL) can be promoted to global by augmenting the mode argument with the RTLD_GLOBAL flag. Under this mode, any objects obtained through a dlopen(3DL) can be used by any other objects with world symbol search scope to locate symbols.

In addition, any object obtained by dlopen(3DL) with the RTLD_GLOBAL flag is available for symbol lookup using dlopen() with a path name whose value is 0.


Note –

If a member of a group having local symbol visibility is referenced by another group requiring global symbol visibility, the object's visibility will become a concatenation of both local and global. This promotion of attributes remains even if the global group reference is later removed.


Isolating a Group

The default assignment of world symbol search scope to the objects obtained by a dlopen(3DL) can be reduced to group by augmenting the mode argument with the RTLD_GROUP flag. Under this mode, any objects obtained through a dlopen(3DL) will only be allowed to look for symbols within their own group.

Using the link-editor's -B group option, you can assign the group symbol search scope to objects when they are built.


Note –

If a member of a group, having group search capability, is referenced by another group requiring world search capability, the object's search capability will become a concatenation of both group and world. This promotion of attributes remains even if the world group reference is later removed.


Object Hierarchies

If an initial object, obtained from a dlopen(3DL), was to use dlopen(3DL) to open a secondary object, both objects would be assigned to a unique group. This situation can prevent either object from locating symbols from one another.

In some implementations the initial object has to export symbols for the relocation of the secondary object. This requirement can be satisfied by one of two mechanisms:

If the initial object is an explicit dependency of the secondary object, the initial object is assigned to the secondary objects' group. The initial object is therefore able to provide symbols for the secondary objects' relocation.

If many objects can use dlopen(3DL) to open the secondary object, and each of these initial objects must export the same symbols to satisfy the secondary objects' relocation, then the secondary object cannot be assigned an explicit dependency. In this case, the dlopen(3DL) mode of the secondary object can be augmented with the RTLD_PARENT flag. This flag causes the propagation of the secondary objects' group to the initial object in the same manner as an explicit dependency would do.

There is one small difference between these two techniques. If you specify an explicit dependency, the dependency itself becomes part of the secondary objects' dlopen(3DL) dependency tree, and thus becomes available for symbol lookup with dlsym(3DL). If you obtain the secondary object with RTLD_PARENT, the initial object does not become available for symbol lookup with dlsym(3DL).

When a secondary object is obtained by dlopen(3DL) from an initial object with global symbol visibility, the RTLD_PARENT mode is both redundant and harmless. This case commonly occurs when dlopen(3DL) is called from an application or from one of the dependencies of the application.

Obtaining New Symbols

A process can obtain the address of a specific symbol using dlsym(3DL). This function takes a handle and a symbol name, and returns the address of the symbol to the caller. The handle directs the search for the symbol in the following manner:

In the following example, which is probably the most common, an application adds additional objects to its address space. The application then uses dlsym(3DL) to locate function or data symbols. The application then uses these symbols to call upon services provided in these new objects. The file main.c contains the following code:


#include    <stdio.h>
#include    <dlfcn.h>
 
main()
{
        void *  handle;
        int *   dptr, (* fptr)();
 
        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {
                (void) printf("dlopen: %s\n", dlerror());
                exit (1);
        }
 
        if (((fptr = (int (*)())dlsym(handle, "foo")) == NULL) ||
            ((dptr = (int *)dlsym(handle, "bar")) == NULL)) {
                (void) printf("dlsym: %s\n", dlerror());
                exit (1);
        }
 
        return ((*fptr)(*dptr));
}

The symbols foo and bar are searched for in the file foo.so.1, followed by any dependencies that are associated with this file. The function foo is then called with the single argument bar as part of the return() statement.

The application prog, built using the above file main.c, contains the following dependencies:


$ ldd prog
        libdl.so.1 =>    /usr/lib/libdl.so.1
        libc.so.1 =>     /usr/lib/libc.so.1

If the file name specified in the dlopen(3DL) had the value 0, the symbols foo and bar are searched for in prog, followed by /usr/lib/libdl.so.1, and finally /usr/lib/libc.so.1.

Once the handle has indicated the root at which to start a symbol search, the search mechanism follows the same model as described in Symbol Lookup.

If the required symbol cannot be located, dlsym(3DL) returns a NULL value. In this case, dlerror(3DL) can be used to indicate the true reason for the failure. In the following example, the application prog is unable to locate the symbol bar.


$ prog
dlsym: ld.so.1: main: fatal: bar: can't find symbol

Testing for Functionality

The special handle RTLD_DEFAULT enables an application to test for the existence of another symbol. The symbol search follows the same model as used to relocate the calling object. See Default Symbol Lookup Model. For example, if the application prog contained the following code fragment:


        if ((fptr = (int (*)())dlsym(RTLD_DEFAULT, "foo")) != NULL)
                (*fptr)();

then foo is searched for in prog, followed by /usr/lib/libdl.so.1, and then /usr/lib/libc.so.1. If this code fragment was contained in the file B.so.1 from the example shown in Figure 3–1, then the search for foo continues into B.so.1 and then C.so.1.

This mechanism provides a robust and flexible alternative to the use of undefined weak references, discussed in Weak Symbols.

Using Interposition

The special handle RTLD_NEXT enables an application to locate the next symbol in a symbol scope. For example, if the application prog contained the following code fragment:


        if ((fptr = (int (*)())dlsym(RTLD_NEXT, "foo")) == NULL) {
                (void) printf("dlsym: %s\n", dlerror());
                exit (1);
        }
 
        return ((*fptr)());

then foo is searched for in the shared objects associated with prog, which in this case are /usr/lib/libdl.so.1 and then /usr/lib/libc.so.1. If this code fragment was contained in the file B.so.1 from the example shown in Figure 3–1, then foo is searched for in the associated shared object C.so.1 only.

Use of RTLD_NEXT provides a means to exploit symbol interposition. For example, a function within an object can be interposed upon by a preceding object, which can then augment the processing of the original function. For example, the following code fragment is placed in the shared object malloc.so.1:


#include    <sys/types.h>
#include    <dlfcn.h>
#include    <stdio.h>
 
void *
malloc(size_t size)
{
        static void * (* fptr)() = 0;
        char             buffer[50];
 
        if (fptr == 0) {
                fptr = (void * (*)())dlsym(RTLD_NEXT, "malloc");
                if (fptr == NULL) {
                        (void) printf("dlopen: %s\n", dlerror());
                        return (0);
                }
        }
 
        (void) sprintf(buffer, "malloc: %#x bytes\n", size);
        (void) write(1, buffer, strlen(buffer));
        return ((*fptr)(size));
}

This shared object can be interposed before the system library /usr/lib/libc.so.1 where malloc(3C) usually resides. Any calls to malloc() are now interposed upon before the original function is called to complete the allocation:


$ cc -o malloc.so.1 -G -K pic malloc.c
$ cc -o prog file1.o file2.o ..... -R. malloc.so.1
$ prog
malloc: 0x32 bytes
malloc: 0x14 bytes
..........

Alternatively, this same interposition can be achieved using the following:


$ cc -o malloc.so.1 -G -K pic malloc.c
$ cc -o prog main.c
$ LD_PRELOAD=./malloc.so.1 prog
malloc: 0x32 bytes
malloc: 0x14 bytes
..........

Note –

Users of any interposition technique must be careful to handle any possibility of recursion. The previous example formats the diagnostic message using sprintf(3C), instead of using printf(3C) directly, to avoid any recursion caused by printf(3C)'s possible use of malloc(3C).


The use of RTLD_NEXT within a dynamic executable or preloaded object, provides a predictable and useful interposition technique. Be careful when using this technique in a generic object dependency, as the actual load order of objects is not always predictable.

Feature Checking

Dynamic objects built by the link-editor sometimes require new runtime linker features. The function _check_rtld_feature() can be used to check if the runtime features required for execution are supported by the running runtime linker. The runtime features currently identified are listed in Table 7–47.

Debugging Aids

A debugging library and mdb(1) module are provided with the Solaris linkers. The debugging library enables you to trace the runtime linking process in more detail. The mdb(1) module enables interactive process debugging.

Debugging Library

This debugging library helps you understand, or debug, the execution of applications and dependencies. Although the type of information displayed using this library is expected to remain constant, the exact format of the information might change slightly from release to release.

Some of the debugging output might be unfamiliar to those who do not have an intimate knowledge of the runtime linker. However, many aspects may be of general interest to you.

Debugging is enabled by using the environment variable LD_DEBUG. All debugging output is prefixed with the process identifier and by default is directed to the standard error. This environment variable must be augmented with one or more tokens to indicate the type of debugging required.

The tokens available with this debugging option can be displayed by using LD_DEBUG=help. Any dynamic executable can be used to solicit this information, as the process itself terminates following the display of the information. For example:


$ LD_DEBUG=help prog
11693:
11693:           For debugging the runtime linking of an application:
11693:                  LD_DEBUG=token1,token2  prog
11693:           enables diagnostics to the stderr.  The additional
11693:           option:
11693:                  LD_DEBUG_OUTPUT=file
11693:           redirects the diagnostics to an output file created
11593:           using the specified name and the process id as a
11693:           suffix.  All diagnostics are prepended with the
11693:           process id.
11693:
11693:
11693: audit     display runtime link-audit processing
11693: basic     provide basic trace information/warnings
11693: bindings  display symbol binding; detail flag shows
11693:             absolute:relative addresses
11693: detail    provide more information in conjunction with other
11693:             options
11693: files     display input file processing (files and libraries)
11693: help      display this help message
11693: libs      display library search paths
11693: move      display move section processing
11693: reloc     display relocation processing
11693: symbols   display symbol table processing
11693: tls       display TLS processing info
11693: unused    display unused/unreferenced files
11693: versions  display version processing

This example shows the options meaningful to the runtime linker. The exact options might differ from release to release.

The environment variable LD_DEBUG_OUTPUT can be used to specify an output file for use instead of the standard error. The process identifier is added as a suffix to the output file.

Debugging of secure applications is not allowed.

One of the most useful debugging options is to display the symbol bindings that occur at runtime. The following example uses a very trivial dynamic executable that has a dependency on two local shared objects.


$ cat bar.c
int bar = 10;
$ cc -o bar.so.1 -K pic -G bar.c
 
$ cat foo.c
foo(int data)
{
        return (data);
}
$ cc -o foo.so.1 -K pic -G foo.c
 
$ cat main.c
extern  int     foo();
extern  int     bar;
 
main()
{
        return (foo(bar));
}
$ cc -o prog main.c -R/tmp:. foo.so.1 bar.so.1

The runtime symbol bindings can be displayed by setting LD_DEBUG=bindings:


$ LD_DEBUG=bindings prog
11753: .......
11753: binding file=prog to file=./bar.so.1: symbol bar
11753: .......
11753: transferring control: prog
11753: .......
11753: binding file=prog to file=./foo.so.1: symbol foo
11753: .......

The symbol bar, which is required by an immediate relocation, is bound before the application gains control. Whereas the symbol foo, which is required by a lazy relocation, is bound after the application gains control when the function is first called. This demonstrates the default mode of lazy binding. If the environment variable LD_BIND_NOW is set, all symbol bindings occur before the application gains control.

Setting LD_DEBUG=bindings,detail, provides additional information regarding the real and relative addresses of the actual binding locations.

When the runtime linker performs a function relocation, it rewrites data associated with the functions .plt so that any subsequent calls will go directly to the function. The environment variable LD_BIND_NOT can be set to any value to prevent this data update. By using this variable together with the debugging request for detailed bindings, you can get a complete runtime account of all function binding. The output from this combination can be excessive, in which case the performance of the application is degraded.

You can use LD_DEBUG to display the various search paths used. For example, the search path mechanism used to locate any dependencies can be displayed by setting LD_DEBUG=libs.


$ LD_DEBUG=libs prog
11775:
11775: find object=foo.so.1; searching
11775:  search path=/tmp:.  (RPATH from file prog)
11775:  trying path=/tmp/foo.so.1
11775:  trying path=./foo.so.1
11775:
11775: find object=bar.so.1; searching
11775:  search path=/tmp:.  (RPATH from file prog)
11775:  trying path=/tmp/bar.so.1
11775:  trying path=./bar.so.1
11775: .......

The runpath recorded in the application prog affects the search for the two dependencies foo.so.1 and bar.so.1.

In a similar manner, the search paths of each symbol lookup can be displayed by setting LD_DEBUG=symbols. If this is combined with a bindings request, you can obtain a complete picture of the symbol relocation process.


$ LD_DEBUG=bindings,symbols
11782: .......
11782: symbol=bar;  lookup in file=./foo.so.1  [ ELF ]
11782: symbol=bar;  lookup in file=./bar.so.1  [ ELF ]
11782: binding file=prog to file=./bar.so.1: symbol bar
11782: .......
11782: transferring control: prog
11782: .......
11782: symbol=foo;  lookup in file=prog  [ ELF ]
11782: symbol=foo;  lookup in file=./foo.so.1  [ ELF ]
11782: binding file=prog to file=./foo.so.1: symbol foo
11782: .......

In the previous example, the symbol bar is not searched for in the application prog. This is due to an optimization used when processing copy relocations. See Copy Relocations for more details of this relocation type.

Debugger Module

The debugger module provides a set of dcmds and walkers that can be loaded under mdb(1). This module can be used to inspect various internal data structures of the runtime linker. Much of this information requires familiarity with the internals of the runtime linker, and may change from release to release. However, some elements of these data structures reveal the basic components of a dynamically linked process and may aid general debugging.

The following examples show some simple scenarios of using mdb(1) with the runtime linker debugger module.


$ cat main.c
#include  <dlfnc.h>

int main()
{
        void *  handle;
        void (* fptr)();

        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL)
                return (1);

        if ((fptr = (void (*)())dlsym(handle, "foo")) == NULL)
                return (1);

        (*fptr)();
        return (0);
}
$ cc -o main main.c -R. -ldl

If mdb(1) has not automatically loaded the debugger module, ld.so, explicitly do so. The capabilities of the debugger module can then be inspected.


$ mdb main
> ::load ld.so
> ::dmods -l ld.so

ld.so
-----------------------------------------------------------------
  dcmd Bind                 - Display a Binding descriptor
  dcmd Callers              - Display Rt_map CALLERS binding descriptors
  dcmd Depends              - Display Rt_map DEPENDS binding descriptors
  dcmd ElfDyn               - Display Elf_Dyn entry
  dcmd ElfEhdr              - Display Elf_Ehdr entry
  dcmd ElfPhdr              - Display Elf_Phdr entry
  dcmd Groups               - Display Rt_map GROUPS group handles
  dcmd GrpDesc              - Display a Group Descriptor
  dcmd GrpHdl               - Display a Group Handle
  dcmd Handles              - Display Rt_map HANDLES group descriptors
  dcmd List                 - Display entries in a List
  dcmd ListRtmap            - Display a List of Rt_Map's
  dcmd Lm_list              - Display ld.so.1 Lm_list structure
  dcmd Rt_map               - Display ld.so.1 Rt_map structure
  dcmd Rt_maps              - Display list of Rt_map structures
  walk List                 - Walk a List
  walk Rt_maps              - Walk a List of Rt_maps
> ::bp main
> :r

Each dynamic object within a process is expressed as a link-map, Rt_map, which is maintained on a link-map list. All link-maps for the process can be displayed with Rt_maps.


> ::Rt_maps
Link-map lists (dynlm_list): 0xffbfe0d0
----------------------------------------------
  Lm_list: 0xff3f6f60  (LM_ID_BASE)
  ----------------------------------------------
    Link-map*  ADDR()     NAME()
    ----------------------------------------------
    0xff3f9040 0x00010000 main
    0xff3f9460 0xff3b0000 /lib/libdl.so.1
    0xff3f977c 0xff280000 /lib/libc.so.1
  ----------------------------------------------
  Lm_list: 0xff3f6f88  (LM_ID_LDSO)
  ----------------------------------------------
    0xff3f8cc0 0xff3c0000 /lib/ld.so.1

An individual link-map can be displayed with Rt_map.


> 0xff3f9040::Rt_map
Rt_map located at: 0xff3f9040
     NAME: main
     ADDR: 0x00010000         DYN: 0x000207bc
     NEXT: 0xff3f9460        PREV: 0x00000000
      FCT: 0xff3f6f18    TLSMODID:          0
     INIT: 0x00010710        FINI: 0x0001071c
   GROUPS: 0x00000000     HANDLES: 0x00000000
  DEPENDS: 0xff3f96e8     CALLERS: 0x00000000
    .....

The object's .dynamic section can be displayed with the ElfDyn dcmd. The following example shows the first 4 entries.


> 0x000207bc,4::ElfDyn
Elf_Dyn located at: 0x207bc
    0x207bc  NEEDED       0x0000010f
Elf_Dyn located at: 0x207c4
    0x207c4  NEEDED       0x00000124
Elf_Dyn located at: 0x207cc
    0x207cc  INIT         0x00010710
Elf_Dyn located at: 0x207d4
    0x207d4  FINI         0x0001071c

mdb(1) is also very useful for setting deferred break points. In this example, it might be useful to put a break point on the function foo(). However, until the dlopen(3DL) of foo.so.1 occurs, this symbol isn't known to the debugger. A deferred break point instructs the debugger to set a real breakpoint when the dynamic object is loaded.


> ::bp foo.so.1`foo
> :c
> mdb: You've got symbols!
> mdb: stop at foo.so.1`foo
mdb: target stopped at:
foo.so.1`foo:   save      %sp, -0x68, %sp

At this point, new objects have been loaded:


> *ld.so`lml_main::Rt_maps
Link-map*  ADDR()     NAME()
----------------------------------------------
0xff3f9040 0x00010000 main
0xff3f9460 0xff3b0000 /lib/libdl.so.1
0xff3f977c 0xff280000 /lib/libc.so.1
0xff3f9ca4 0xff380000 ./foo.so.1
0xff37006c 0xff260000 ./bar.so.1

The link-map for foo.so.1 shows the handle returned by dlopen(3DL). You can expand this structure using Handles.


> 0xff3f9ca4::Handles -v
HANDLES for ./foo.so.1
----------------------------------------------
  HANDLE: 0xff3f9f60 Alist[used 1: total 1]
    ----------------------------------------------
    Group Handle located at: 0xff3f9f28
    ----------------------------------------------
        owner:               ./foo.so.1
        flags: 0x00000000    [ 0 ]
       refcnt:          1    depends: 0xff3f9fa0 Alist[used 2: total 4]
        ----------------------------------------------
        Group Descriptor located at: 0xff3f9fac
           depend: 0xff3f9ca4    ./foo.so.1
            flags: 0x00000003    [ AVAIL-TO-DLSYM,ADD-DEPENDENCIES ]
        ----------------------------------------------
        Group Descriptor located at: 0xff3f9fd8
           depend: 0xff37006c    ./bar.so.1
            flags: 0x00000003    [ AVAIL-TO-DLSYM,ADD-DEPENDENCIES ]

The dependencies of a handle are a list of link-maps that represent the objects of the handle that can satisfy a dlsym(3DL) request. In this case, the dependencies are foo.so.1 and bar.so.1.


Note –

The above examples provide a basic guide to the debugger module capabilities, but the exact commands, usage, and output may change from release to release. Refer to usage and help information for the exact capabilities available on your system.