Concluding Remarks and Summary of Best Practices

Part VIII of Libraries, Linking, Initialization, and C++ Series

By Darryl Gove and Stephen Clamage, July 2011

Part I - Introduction to Libraries and Linking
Part II - Resolving Symbols in Libraries


Distribution of Functionality

Delivering an application as an executable together with a set of libraries can involve some complexity. There are a number of issues that need to be given careful consideration. The first issue to consider is the design of the appropriate hierarchy of libraries so that each library either depends on itself for functionality or leverages functionality provided by libraries lower down in the hierarchy. No circular dependencies should be introduced by a low-level library depending on a higher-level library for functionality.

At compile time, the hierarchy can be enforced using the flag -z defs to report any unresolved symbols together with explicitly listing the existing libraries that each new library depends upon. The -L flag should be used together with a path to ensure that the linker can locate the existing libraries.
 

Avoiding Symbolic Clutter

Minimizing the number of symbols exported by a library ensures that only those symbols can be used. This reduces the risk of conflict when multiple libraries define the same symbol, avoids the possibility of someone calling a routine that is internal to the library, and also reduces the amount of work that the run-time linker has to perform when the library is loaded. All of these are important. The other problem to be avoided is multiple definitions of a symbol.

Symbol scoping can be used to avoid exporting symbols that are internal to the library. The best approach to scoping is to set everything that is not otherwise specified to be hidden scoping. This can be done using the compiler flag -xldscope=hidden. Symbols that need to be exported must then be declared with the keyword prefix __symbolic. Symbolic scoping indicates that the local symbol should be used in preference to any other symbols of the same name, and it also indicates that the symbol should be exported so other libraries can use it. Symbols that need to be imported by a library should be declared with the prefix __global. Symbols declared as global are available for anyone to use if they are defined. If they are undefined, global symbols will be resolved by the run-time linker to an external symbol. Global symbols may be satisfied by the local definition, but this is not necessarily the case.

For existing code, the workaround of compiling everything with the scoping flag -xldscope=symbolic ensures that libraries bind to their local version of a symbol in preference to a global version. This might enable applications that have symbol binding problems to function, but it can introduce problems. An application that relies on interposition on calls to a library function will not see those calls that occur within a library. An application that relies on there being only one definition of a particular symbol might find that the same symbol resolves differently in different parts of the same application.

The other area where libraries can accumulate unnecessary symbols is through the definition of inline routines or templates. Member functions declared in header files can appear in any libraries that include the header file. In these cases, appropriate scoping of the member functions or variables is the best way of controlling multiple definitions. For template code, the compiler flag -instlib=<library> causes the compiler to not emit definitions in the current compile unit for template code that is located in the identified library.
 

Locating Libraries at Run Time

At run time, the run-time linker needs to locate the libraries that an application requires. It is never good practice to rely on the setting of LD_LIBRARY_PATH for the run-time linker to find the required libraries. The best approach is to use the compile time flag -R <path> to embed a run-time path into the executable or library. This path can be used at run time to find the required library.

Although it is tempting to provide a hard-coded path to the -R flag, it is recommended that the token $ORIGIN be used to provide a path that is relative to the location of the executable. Doing this ensures that the application and its libraries can be stored anywhere that the relative path is preserved, and they will still work.

Getting Guidance from the Linker

The linker in Oracle Solaris 11 Express provides a new feature under the -zguidance flag. This option provides some guidance on how the link might be improved. An example of this is shown in Listing 1.

Listing 1: Guidance Emitted by the Oracle Solaris 11 Express Linker
$ CC -o main main.cpp -L. -R'$ORIGIN' -ldata -zguidance
ld: guidance: -z lazyload option recommended before first dependency
ld: guidance: -B direct or -z direct option recommended before first dependency
ld: guidance: removal of unused dependency recommended: libCstd.so.1
ld: guidance: removal of unused dependency recommended: libm.so.2
ld: guidance: see ld(1) -z guidance for more information

In the example, the compile-time linker provides recommendations for the use of direct binding and lazy loading of libraries. It also reports that libCstd.so.1 and libm.so.2 are unused. These libraries are automatically included in the link by the C++ compiler, but in this instance, the application makes no use of them.
 

Concluding Remarks

There are many good reasons for distributing an application as a combination of executable and libraries. The rules in this series of articles enables the resulting application to function correctly and efficiently. In the event of symbol scoping or library initialization problems, using debug facilities, such as LD_DEBUG, or tools, such as nm or lari, facilitates rapid diagnosis.

Revision 1, 07/11/2011