Chapter 3. Using Dynamic Shared Objects

A dynamic shared object (DSO) is an object file that is meant to be used simultaneously (or shared) by multiple applications (a.out files) while they are executing.

As you read this chapter, you will learn how to build and use DSOs. This chapter covers the following topics:

You can use DSOs in place of archive libraries (they replace static shared libraries provided with earlier releases of IRIX).

Benefits of Using DSOs

Because DSOs contain shared components, using them provides several substantial benefits:

  • DSOs minimize overall memory use: DSOs minimize overall memory usage because code is shared. Two executables that use the same DSO and that run simultaneously have only one copy of the instruction from the shared component loaded into memory. For example, if executable A and executable B both link with the same DSO C, and if A and B are both running at the same time, the total memory used is what is required for A, B, and C, plus some small overhead. If C is an unshared library, the memory used is what is required for A, B, and two copies of C.

  • Executables linked with DSOs are smaller: Executables linked with DSOs are smaller than those linked with unshared libraries because the shared objects are not part of the executable file image, so disk usage is minimized.

  • DSOs are easier to use, build, and debug: DSOs are much easier to use, build, and debug than the static shared libraries (supplied in IRIX 4 and earlier). Most of the libraries supplied by SGI today are available as DSOs. In IRIX 4 and earlier, only a few static shared libraries were available; most libraries were unshared.

  • Executables using DSOs do not have to be relinked : Executables that use a DSO do not have to be relinked if the DSO changes; when the new DSO is installed, the executable automatically starts using it. This feature makes it easier to update end users with new software versions. It also allows you to create hardware-independent software packages more easily.

    Suppose, for example, you want to build both MIPS IV and a MIPS III version of a shared object. You want your program to use the MIPS IV version when it is running on a Power Challenge (R8000) system, and also run correctly on another 64-bit platform. Suppose you want to do the above with the routines in a library named libchange.so. To do this, build one version of the routines in libchange using the -mips4 option, and place it in /usr/lib64/mips4 on a Power Challenge system. Next, build another version using the -mips3 option, and place it in /usr/lib64. Then, when you build an executable that uses libchange, use the -rpath option to tell the run-time linker to look first for MIPS IV versions of the libraries. For example:

    % cc -mips3 -o prog prog.o -rpath /usr/lib64/mips4 -lchange

    As a result, prog runs on any IRIX 6 (and later) system, and it automatically takes advantage of any MIPS IV libraries whenever it runs on a Power Challenge system.

  • DSOs and executables are mapped into memory: DSOs and the executables that use them are mapped into memory by a run-time loader, rld, which resolves external references between objects and relocates objects at run time. (DSOs contain only position-independent code (PIC), so they can be loaded at any virtual address at run time.) With rld, the binding of symbols can be changed at run time at the request of the executing program. You could use this feature to dynamically change the feature set presented to a user of your application, for example, while minimizing start-up time. The application could be started quickly, with a subset of the features available and then, if the user needs other features, those can be loaded in under programmatic control.

    Costs that are involved with using DSOs are explained in “Using DSOs”. The sections after that explain how to build and optimize DSOs and how rld works. See the rld(1) man page for more information. The dso(5) man page also contains more information about DSOs.

Using DSOs

The linker command-line syntax is the same as for an archive ( .a) library. This section explains how to use DSOs. Specific topics include:

DSOs vs. Archive Libraries

The following compile line creates the executable yourApp by linking with the DSOs libyours.so and with libc.so.1:

% cc yourApp.c -o yourApp -lyours

If libyours.so is not available, but the archive version libyours.a is available, that archive version is used along with libc.so.1.

A significant difference exists between DSOs and archive libraries in terms of what is mapped into the address space when an application is executing. With an archive library, only the text portion of the library that the application actually requires (and the data associated with that text) is mapped, not the entire library. In contrast, the entire DSO that is linked is mapped; in many cases, however, the DSO is shared and already mapped into the address space. Thus, to conserve address space and save time at startup, do not link with DSOs unless your application actually needs them.

Avoid listing any archive libraries on the compile line after you list shared libraries; instead, list the archive libraries first and then the DSOs.

Using QuickStart

You may want to take advantage of the QuickStart optimization that minimizes start-up times for executables. You can use QuickStart when using or building DSOs. At link time, when an executable or a DSO is being created, the linker ld assigns initial addresses to the object and attempts to resolve all references. Since DSOs are relocatable, these initial address assignments are really only guesses about where the object will be really loaded. At run time, rld verifies that the DSO being used is the same one that was linked with and what the real addresses are. If the DSOs are the same and if the addresses match the initial assignments, rld does not have to perform any relocation work, and the application starts up very quickly (or QuickStarts). When an application QuickStarts, memory use is less since rld does not have to read in the information necessary to perform relocations.

To determine whether your application (or DSO) is able to do a QuickStart, use the -quickstart_info flag when building the executable (or DSO). If the application or DSO cannot do a QuickStart, you will be given information about what to do. The next section goes into more detail about why an executable may not be able to use QuickStart.

In summary, when you use DSOs to build an executable, remember the following:

  • Link with only the DSOs that you need.

  • Make sure that archive libraries precede DSOs on the compile line.

  • Use the -quickstart_info flag.

Guidelines for Using Shared Libraries

When you are working with DSOs, you can avoid some common pitfalls if you adhere to the guidelines described in this section:

Choosing DSO Library Members

This section covers some important considerations for choosing library members.

  • Include large, frequently used routines. These routines are prime candidates for sharing. Placing them in a shared library saves code space for individual a.out files and saves memory, too, when several concurrent processes need the same code. printf(3S) and related C library routines are good examples of large, frequently used routines.

  • Exclude infrequently used routines. Putting these routines in a shared library can degrade performance, particularly on paging systems. Traditional a.out files contain all code they need at run time. By definition, the code in an a.out file is (at least distantly) related to the process. Therefore, if a process calls a function, it may already be in memory because of its proximity to other text in the process.

    If the function is in the shared library, a page fault may be more likely to occur, because the surrounding library code may be unrelated to the calling process. Only rarely will any single a.out file use everything in the shared C library. If a shared library has unrelated functions, and unrelated processes make random calls to those functions, the locality of reference may be decreased. The decreased locality may cause more paging activity and, thereby, decrease performance.

  • Exclude routines that use much static data. These modules increase the size of processes. Every process that uses a shared library gets its own private copy of the library's data, regardless of how much of the data is needed.

    Library data is static: it is not shared and cannot be loaded selectively with the provision that unreferenced pages may be removed from the working set.

    For example, getgrent(3C) is not used by many standard UNIX commands. Some versions of the module define over 1400 bytes of unshared, static data. So, do not include it in a shared library. You can import global data, if necessary, but not local, static data.

  • Make libraries self-contained. It is best to make the library self-contained. You can do this by including routines in the shared object. For example, printf(3S) requires much of the standard I/O library. A shared library containing printf (3S), should also contain the rest of the standard I/O routines. This is done with libc.so.1.

    If your shared object calls routines from a different shared object, it is best to build in this dependency by naming the needed shared objects on the link line in the usual way. For example:

    % ld -shared -all mylib.a -o mylib.so -soname mylib.so -lfoo

    This command line specifies that libfoo.so is needed by mylib.so. Thus, when an application is linked against mylib.so, it is not necessary to specify -lfoo.

    This guideline should not take priority over the others in this section. If you exclude some routine that the library itself needs based on a previous guideline, consider leaving the symbol out of the library and importing it.

Tuning Shared Library Code

This section explains things to consider in tuning shared library code:

  • Minimize the number of symbols exported (see “Controlling Symbols to Be Exported or Loaded”) for details.

  • Minimize global data. All external data symbols are, of course, visible to applications. This can make maintenance difficult. Therefore, you should try to reduce global data.

    • Try to use automatic (stack) variables. Do not use permanent storage if automatic variables work. Using automatic variables saves static data space and reduces the number of symbols visible to application processes.

    • Determine whether variables really must be external and exported. Static symbols and hidden symbols are not visible outside the library, so they may change meanings between library versions. Only exported external variables must retain the same meaning.

    • Allocate buffers at run time instead of defining them at compile time. Allocating buffers at run time reduces the size of the library's data region for all processes and, thus, saves memory. Only processes that actually need the buffers get them. It also allows the size of the buffer to change from one release to the next without affecting compatibility. Statically allocated buffers cannot change size without affecting the addresses of other symbols and, perhaps, breaking compatibility.

  • Organize to improve locality. When a function is in a.out files, it typically resides in a page with other code that is used more often (see “Exclude Infrequently Used Routines”). Try to improve locality of reference by grouping dynamically related functions. If every call of funcA generates calls to funcB and funcC, try to put them in the same page.

    The cord(1) command rearranges procedures to reduce paging and achieve better instruction cache mapping. You can use cord to see the number of cycles spent in a procedure and the number of times the procedure was executed. Use profiling to see what is actually called, as opposed to what may be called.

  • Align for paging. The key is to arrange the shared library target's object files so that frequently used functions do not unnecessarily cross page boundaries. When arranging object files within the target library, be sure to keep the text and data files separate. You can reorder text object files without breaking compatibility; the same is not true for object files that define global data.

    For example, the IRIX operating system uses 4Kbyte pages. Using name lists and disassemblies of the shared library target file, the library developers determined where the page boundaries fell.

    After grouping related functions, they broke them into page-sized chunks. Although some object files and functions are larger than a single page, most of them are smaller. Then the developers used the infrequently called functions as glue between the chunks. Because the glue between pages is referenced less frequently than the page contents, the probability of a page fault decreased.

    After determining the branch table, they rearranged the library's object files without breaking compatibility. The developers put frequently used, unrelated functions together, because they would be called randomly enough to keep the pages in memory. System calls went into another page as a group, and so on. For example, the order of the library's object files became:

    Before         After
    
    #objects       #objects
       ...  .         ...
       printf.o       strcmp.o
       fopen.o        malloc.o
       malloc.o       printf.o
       strcmp.o       fopen.o
       ....           ...

Taking Advantage of QuickStart

QuickStart is an optimization designed to reduce start-up times for applications that link with DSOs. Each time ld builds a DSO, it updates a registry of shared objects. The registry contains the preassigned QuickStart addresses of a group of DSOs that typically cooperate by having locations that do not overlap. If you compile your application by linking with registered DSOs, your application takes advantage of QuickStart: all the DSOs are mapped at their QuickStart addresses, and rld wiill not need to move any of them to an unused address and perform a relocation pass to resolve all references.

Suppose you compile your application using the -quickstart_info flag, and QuickStart fails. It may fail because:

  • Your application has directly or indirectly linked with two different versions of the same DSO, as shown in Figure 3-1. In this example, yourApp links with libyours.so , libmotif.so, and libc.so.1 on the compile line. When the DSO libyours.so was built, however, it linked with libmalloc.so, which in turn linked with libc.so.1 when it was created. If the two versions of libc.so.1 were not identical, yourApp will not be able to use QuickStart.

    Figure 3-1. An Application Linked with DSOs

    An Application Linked with DSOs

  • You link with a DSO that cannot use QuickStart. This may occur because the DSO was not registered and therefore was assigned a location that overlaps with the location assigned to another DSO.

  • Your application pulls in incompatible shared objects (in a manner similar to the example shown in Figure 3-1).

  • Your application contains an unresolved reference to a function (where it takes the address of the function).

  • The DSO links with another DSO that cannot use QuickStart.

Even if QuickStart officially succeeds, your application may have name space collisions and therefore may not start up as fast as it should. This is because rld has to bring in more information to resolve the conflicts. In general, you should avoid having conflicts both because of the detrimental effect on start-up time and because conflicts make it difficult to ensure the correctness of an application over time.

In the example shown in Figure 3-1, you may have written your own functions to allocate memory in libmalloc.so for libyours.so to use. If you did not use unique names for those functions (instead of malloc(), for example) the way this particular compile and link hierarchy is set up, the standard malloc() function defined in libc.so.1 is used instead of the one defined in libmalloc.so.


Note: Conflicts are resolved by proceeding through the hierarchy from left to right and then moving to the next level (this is called breadth-first searching). “Searching for DSOs at Run Time”, explains how the run-time linker searches for DSOs.

For example, suppose the diagram in Figure 3-1 corresponds to the following command:

% cc -lyours -lmotif -lc

Because shared objects mentioned on the command line always take precedence over those that are not mentioned, the preceding command uses the standard malloc() defined in libc.so.1.

To get your own version of malloc() defined in libmalloc.so for libyours.so to use, enter:

% cc -lyours -lmotif -lmalloc -lc

However, in both of the above examples, if -lyours contains malloc(), you will get that malloc() . (In the examples above, you do not need to specify -lc ; it was added for clarity).

Thus, it is not a good idea to allow more than one DSO to define the same function. Even if the DSOs are synchronized for their first release, one of them may change the definition of the function in a subsequent release. Of course, you can use conflicts to intentionally override function definitions; however, make sure you control what is overriding what.

If you use the -quickstart_info option, ld tells you if conflicts arise. It also tells you to run elfdump with the -Dc option to find the conflicts. See the elfdump(1) man page for more information about how to read the output produced by elfdump.

Building DSOs

In most cases, you can build DSOs as easily as archive libraries. If your library is written in a high-level language, such as C or Fortran, you will not have to make any changes to the source code. If your code is written in assembly language, you must modify it to produce PIC. This is described in the MIPSpro Assembly Language Programmer's Guide.

This section covers procedures to use when you build DSOs, and includes these topics:

Creating DSOs

To create a DSO from a set of object files, use ld with the -shared option:

% ld -shared stuff.o nonsense.o -soname libdada.so -o libdada.so 

The preceding example creates a DSO, libdada.so, from two object files, stuff.o and nonsense.o . Note that DSO names should begin with lib and end with .so, for ease of use with the compiler driver's -llib argument. If you are already building an archive library (.a file), you can create a DSO from the library by using the -shared and -all arguments to ld:

ld -shared -all libdada.a -soname libdada.so -o libdada.so

The -all argument specifies that all of the object files from the library, libdada.a, should be included in the DSO.


Note: It is best to use the -soname option. For example, if the -o name has an explicit path such as -o ../a/libdada.so , typically you want the -soname to be libdada.so.



Warning: It is essential that the soname of a DSO and its file name be congruent according to the versioning rules. (Congruent means that if the file name is, for example, xxxx.so.1, the soname must be optional-path/xxxx.so or xxxx.so.1 ). Usually, optional-path should be empty. A full path soname is used when a DSO must be accessed (at run time) from that specific location in the file system. A partial-path is used when the DSO must be accessed at that location from the current working directory. As such, a partial path is almost always a mistake.

The consequences of having a DSO file name and soname that are not congruent range from not being able to use the DSO at all to having all of multiple dlopen commands load a fresh copy, even though only one will actually be used.


Making DSOs Self-Contained

When building a DSO, be sure to include any archives required by the DSO on the link line so that the DSO is self-contained (that is, it has no unresolved symbols). If the DSO depends on libraries not explicitly named on the link line, subsequent changes to any of those libraries may result in name space collisions or other incompatibilities that can prevent any applications that use the DSO from doing a QuickStart. Such incompatibilities can also lead to unpredictable results over time as the libraries change asynchronously. Suppose you want to make the archive libmine.a into a DSO, and libmine.a depends on routines in another archive, libutil.a. In this case, include libutil.a on the link line:

% ld -shared -all -no_unresolved libmine.a -soname libmine.so \
-o libmine.so -none libutil.a

This causes the modules in libutil.a that are referenced in libmine.a to be included in the DSO, but these modules will not be exported. (For more information about exported symbols, see “Controlling Symbols to Be Exported or Loaded”.) The -no_unresolved option causes a list of unresolved symbols to be created; generally, this list should be empty to enable using QuickStart.

Similarly, if a DSO relies on another DSO, be sure to include that DSO on the link line. For example:

% ld -shared -all -no_unresolved libbtree.a -soname libtree.so \
-o libtree.so -lyours

This example places libyours.so in the liblist of the new DSO, libtree.so. This ensures that libyours.so is loaded whenever an executable that uses libtree.so is launched. Again, symbols from libyours.so will not be exported for use by other libraries. (You can use the -exports flag to reverse this exporting behavior; the -hides flag specifies the default exporting behavior.)

Controlling Symbols to Be Exported or Loaded

Limiting the number of symbols exported has two effects that are important to DSO performance. These effects apply mainly to DSO startup costs rather than to the time spent executing the code in the application or DSO:

  • Hidden symbols always resolve very quickly, but references to exported symbols can take longer to resolve. If there are n DSOs during execution, name lookup will take more than n/2 times as long. Hidden symbols have an update cost but do not have a real lookup cost so even if n is only 3 or 4, the hidden symbols are handled much faster than exported symbols.

  • Hidden symbols do not conflict with other symbols. Conflicts slow down the startup of DSOs that are quickstarted. Conflicts are irrelevant in any DSO that does not quickstart and after any dlopen() is done, nothing will quickstart in that execution.

By default, to help avoid conflicts, symbols defined in an archive or a DSO that is used to build another DSO are not externally visible. You can explicitly export or hide symbols with the -exported_symbol (or -exports) and -hidden_symbol (or -hides) options :

-exported_symbol name1, name2, name3 
-hidden_symbol name4, name5

By default, if you explicitly export any symbols, all other symbols are hidden. If you both explicitly export and explicitly hide the same symbol on the link line, the first occurrence determines the behavior. You can also create a file of symbol names (delimited by white space) that you want explicitly exported or hidden, and then refer to the file on the link line with either the -exports_file or -hiddens_file option:

-exports_file file 
-hiddens_file file2

These files can be used in addition to explicitly naming symbols on the link line.

The -exports option is used in conjunction with the -shared or -call_shared options. It specifies that symbols from the next object, archive, or DSO be exported by the object being created. Similarly, the hides option specifies that symbols from the next object, archive, or DSO be hidden by the object being created. This is the default behavior for linking in archives or DSOs, but it is not for relocatable objects.

Another useful option, -delay_load, prevents a library from being loaded until it is actually referenced. Suppose, for example, that your DSO contains several functions that are likely to be used in only a few instances. Furthermore, those functions rely on another library (archive or DSO). If you specify -delay_load for this other library when you build your DSO, the run-time linker loads that library only when those few functions that require it are used. Note that if you explicitly export any symbols defined in a library that the run-time linker is supposed to delay loading, the export behavior takes precedence and the library is automatically loaded at run time.

Delay-loaded shared objects are not delay-loaded if direct references to data symbols exist in the delay-loaded object, or if the address of a function in the delay-loaded object is taken. That is, -delay_load is only effective with objects that have a purely functional interface (ld(1) will only set the -delay-load flag in the library list if delay-load will work properly).

Delay-loaded shared objects do not function properly if direct references to data symbols exist in the delay-loaded object, or if the address of the function in the delay-loaded object is used. Therefore, only use -delay_load to load shared objects that have a purely functional interface.


Note: You can build DSOs using cc. However, if you want to export symbols/files or use -delay_load, use ld to build DSOs.


Building DSOs with C++

It is recommended that you use the CC command rather than the ld command to build DSOs from C++ programs. The driver generates a lot of C++ specific arguments to ld, without which the DSO does not work. If you use templates, using CC to build your DSO also guarantees that templates get instantiated properly. For example:

% CC -shared -o libmylib.so  object file list

For example:

% CC -shared -o libmylib.so a.o b.o c.o

CC recognizes many of the ld options such as -l and -L; hence these options to ld work. However, most ld options do not work. If you want to specify other options, refer to the CC(1) and the ld(1) man pages. If the option is not described in the CC page, you may need to use the -Wl,ld _option syntax to tell the CC driver to pass ld_option to ld. See the CC(1) man page for details.

Run-Time Linking

This section explains the search path followed by the run-time linker and how you can cause symbols to be resolved at run time rather than link time. Specifically, this section describes:

Searching for DSOs at Run Time

When you run a dynamically linked executable, the run-time linker, rld(1), identifies the DSOs required by the executable and loads the required DSOs. If necessary the IRIX kernel relocates DSOs within the process' virtual address space, so that no two DSOs occupy the same location. The program header of a dynamically linked executable contains a field, the liblist, which lists the DSOs required by the executable.

When looking for a DSO, rld searches directories in a specific sequence. This section covers run-time searching for the o32-bit, n32-bit, and 64-bit ABIs.

This section also describes environment variables that let you customize the search on your system. Each ABI has its own environment variable set. The o32 set usually applies to the other ABIs unless the other ABI environment variable is set. Only the _RLD_ARGS environment variable, which is not often used, is shared by all three ABIs.

Searching for DSOs at Run Time under the o32-Bit ABI

The (old) o32-bit ABI rules use the following sequence when searching for DSOs at run time:

  1. /usr/lib

  2. /usr/lib/internal

  3. /lib

  4. /lib/cmplrs/cc

  5. /usr/lib/cmplrs/cc

  6. /opt/lib

RPATH is a colon-separated list of directories stored in the main executable. You can set RPATH by using the -rpath argument to ld:

% ld -o myprog myprog.c -rpath /d/src/mylib -soname libmylib.so \
libmylib.so -lc

This example links the program against libmylib.so in the current directory and configures the executable such that rld searches the directory /d/src/mylib when searching for DSOs.

The LD_LIBRARY_PATH environment variable is a colon-separated list of directories to search for DSOs. This can be very useful for testing new versions of DSOs before installing them in their final location.

You can set the environment variable, _RLD_ROOT for the old 32-bit ABI, to a colon-separated list of directories. The run-time linker prepends these to the paths in RPATH and the paths in the default search path.

In all of the colon-separated directory lists, an empty field is interpreted as the current directory. A leading or trailing colon counts as an empty field. For example, if an application using the old 32-bit ABI sets LD_LIBRARY_PATH to the following:

/d/src/lib1:/d/src/lib2:

In this example, the run-time linker searches the directory /d/src/lib1, then the directory /d/src/lib2, and then the current directory.


Note: The security policy is implemented in the IRIX kernel in IRIX 6.5 and later versions; for earlier versions of IRIX, it is implemented in rld. The current policy for honoring rld environment variables is as follows:

Most rld environment variables are ignored for executables with no capabilities set (see the capabilities(4) man page) if both of the following are true:

  • The real user ID is not 0 (root).

  • One of the following is true:

    • The real and effective (those active for the process) user IDs do not match.

    • The real and effective group IDs do not match.



If the environment or an executable has capabilities set, that executable will be treated as if it were a setuid(2) application. To check if your shell has capabilities set, use id -P. Use su -C all= to get a shell with no capabilities.


Searching for DSOs at Run Time under the n32-Bit ABI

The (new) n32-bit ABI rules use the following sequence when searching for DSOs at run time:

  1. /usr/lib32

  2. /usr/lib32/internal

  3. /lib32

  4. /opt/lib32

Setting the _RLD32_ROOT or the LD_LIBRARYN32_PATH environment variable overrides the default settings.

If LD_LIBRARYN32_PATH is not specified, rld honors LD_LIBRARY_PATH, if specified. As a result, if LD_LIBRARY_PATH is set for an old 32-bit program, it is recommended that you also set LD_LIBRARYN32_PATH to something ("", for example) to avoid having LD_LIBRARY_PATH apply accidentally to new 32-bit applications in that environment.

Searching for DSOs at Run Time under the 64-Bit ABI

The 64-bit ABI rules use the following sequence when searching for DSOs at run time:

  1. /usr/lib64

  2. /usr/lib64/internal

  3. /lib64

  4. /opt/lib64

Setting the _RLD64_ROOT or the LD_LIBRARY64_PATH environment variable overrides the default settings.

If LD_LIBRARY64_PATH is not specified, rld honors LD_LIBRARY_PATH, if specified. As a result, if LD_LIBRARY_PATH is set for an old 32-bit program, it is recommended that you also set LD_LIBRARY64_PATH to something ("", for example) to avoid having LD_LIBRARY_PATH apply accidentally to 64-bit applications in that environment.

Run-Time Symbol Resolution

Dynamically linked executables can contain symbol references that are not resolved before run time. Any symbol references in your main program or in an archive must be resolved at link time, unless you specify the -ignore_unresolved argument to cc.

DSOs may contain references that are not resolved at link time. All data symbols must be resolved at run time. If rld finds an unresolvable data symbol at run time, the executable exits with an error. Text symbols are resolved only when they are used, so a program can run with unresolved text symbols, as long as the unresolved symbols are not used.

You can force rld to resolve text symbols at run time by setting the environment variable LD_BIND_NOW. If unresolvable text symbols exist in your executable and you set LD_BIND_NOW , the executable exits with an error, as if there were unresolvable data symbols.

Building a DSO with -Bsymbolic

When you build a DSO with -Bsymbolic, the dynamic linker resolves referenced symbols from itself first. If the shared object fails to supply the referenced symbol, then the dynamic linker searches the executable file and other shared objects. For example:

main--defines x

x.so--defines and uses x

If you build x.so with -Bsymbolic on, the linker tries to resolve the use of x by looking first for the definition in x.so and then by looking in main.

In FORTRAN programs, the linker allocates space for COMMON symbols and the compiler allocates space for BLOCK DATA. The first kind of symbol (with COMMON blocks present) appears in the symbol table as SHN_MIPS_ACOMMON (uninitialized DATA ) whereas the second kind of symbol (with BLOCK DATA present) appears as SHN_DATA (initialized DATA ). In general, initialized data takes precedence when the dynamic linker tries to resolve a symbol. However, with -Bsymbolic, whatever is defined in the current object takes precedence, whether it is initialized or uninitialized.

Variables that are declared at file scope in C with -cckr are also treated this way. For example:

int foo[100];

is COMMON if -cckr is used and DATA if -xansi or -ansi is used.

For example:

In main:

COMMON i, j /* definition of i, j with initial values */
DATA i/1/, j/1/
CALL junk
END

In x.so:

SUBROUTINE junk
COMMON i, j
/* definition of i, j with NO initial values */
/* initialized by kernel to all zeros */
PRINT *, i, j
END

When you build x.so using -Bsymbolic, this program prints 0 0. When you build x.so without -Bsymbolic, the program prints 1 1.

Converting Archive Libraries to DSOs

When you link a program with a DSO, all of the symbols in the DSO become associated with the executable. This can cause unexpected results if archives that contain unresolved externals are converted to DSOs. When linking with a PIC archive, the linker links in only those object files that satisfy unresolved references.

If an object file in an archive contains an unresolved external reference, the linker tries to resolve the reference only when that object file is linked in to your program. In contrast, a DSO containing an external data reference that cannot be resolved at run time causes the program to fail. Therefore, use caution when converting archives with external data references to DSOs.

For example, suppose you have an archive, mylib.a, and one of the object files in the archive, has_extern.o, references an external variable, foo. As long as your program does not reference any symbols in has_extern.o, the program will link and run properly. If your program references a symbol in has_extern.o and does not define foo , then the link will fail. However, if you convert mylib.a to a DSO, then any program that uses the DSO and does not define foo will fail at run time, regardless of whether the program references any symbols from has_extern.o.

Two possible solutions exist for this problem.

  • Add a “dummy” definition of the data to the DSO. A data definition appearing in the main executable preempts one appearing in the DSO itself. This may, however, be misleading for executables that use the portion of the DSO that needs the data, but that failed to define it in the main program.

  • Separate the routines that use the data definition into a second DSO, and place dummy functions for them in the first DSO. The second DSO can then be loaded dynamically the first time any of the dummy functions is accessed. Each of the dummy functions must verify that the second DSO was loaded before calling the real function (which must have a unique name). This way, programs run whether or not they supply the missing external data, as long as they do not call any of the functions that require the data. The first time one of the dummy functions is called, it tries to dynamically load the second DSO. Programs that do not supply the missing data fail at this point.

For more information on dynamic loading, see “Dynamic Loading Under Program Control”.

Dynamic Loading Under Program Control

IRIX provides a library interface to the run-time linker that allows programs to load and unload DSOs dynamically. The functions in this interface are part of libc (see Table 3-1).

Table 3-1. Functions to Load and Unload DSOs

Function

Action

dlopen()

Loads a DSO

dlsym()

Finds a symbol in a loaded DSO

dlclose()

Unloads a DSO

dlerror()

Reports errors

sgidlopen_version()

Loads a DSO

sgidladd_version()

Loads a DSO

You can dynamically load shared objects by using sgidladd(), which is similar to dlopen(...,RTLD_LOCAL|...) . However, unlike dlopen(), all the names in the shared object become available to satisfy references in shared objects during lazy text resolution. Furthermore, it is not necessary to use dlsym() to gain access to the symbols in the shared object. sgidladd() is available as part of libc. For more information, see the sgidladd(3) man page.

To load a DSO, call dlopen():

include <dlfcn.h>
void *dlhandle;
     ..
dlhandle = dlopen("/usr/lib/mylib.so", RTLD_LAZY | RTLD_LOCAL);
if (dlhandle == NULL) {
     /* couldn't open DSO */
     printf("Error: %s\n", dlerror());
}

The first argument to dlopen() is the pathname of the DSO to be loaded. This may be either an absolute or a relative pathname. When you call this routine, the run-time linker tries to load the specified DSO. If any unresolved references exist in the executable that are defined in the DSO, the run-time linker resolves these references on demand. You can also use dlsym() to access symbols in the DSO, whether or not the symbols are referenced in your executable.

When a DSO is brought into the address space of a process, it may contain references to symbols whose addresses are not known until the object is loaded. These references must be relocated before the symbols can be accessed. The second argument to dlopen() governs when these relocations take place.

This argument can have the following values:

  • RTLD_LAZY: Under this mode, only references to data symbols are relocated when the object is loaded. References to functions are not relocated until a given function is invoked for the first time. This mode may result in better performance, since a process may not reference all of the functions in any given shared object.

  • RTLD_NOW: Under this mode, all necessary relocations are performed when the object is first loaded. This may result in some wasted effort if relocations are performed for functions that are never referenced. However, this option is useful for applications that need to know as soon as an object is loaded that all symbols referenced during execution will be available.

  • RTLD_GLOBAL: This mode modifies the treatment of the symbols in the DSO being opened to be identical to those of sgidladd(). RTLD_GLOBAL may be ORed with either RTLD_NOW or RTLD_LAZY (RTLD_GLOBAL cannot be the mode value on its own). See dlopen (3c) for details.

  • RTLD_LOCAL: With this mode, the symbols in the dlopen DSO can only be referenced by dlsym ; they cannot be accessed by symbol name. This is the default.

To access symbols that are not referenced in your program, use dlsym():

#include <dlfcn.h>
void *dlhandle;
int (*funcptr)(int);
int i,j;
     .. load DSO ... 
funcptr = (int (*)(int)) dlsym(dlhandle, "factorial");
if (funcptr == NULL) {
     /* couldn't locate the symbol */
     exit();
}
i = (*funcptr)(j);


Note: The cast to (int (*) (int)) may produce a compiler warning about converting data pointers to function pointers. The warning is honoring the ANSI/ISO C standard; the cast and subsequent call work fine.

This example looks up the address of the function factorial() and assigns it to the function pointer funcptr.

If you encounter an error (dlopen() or dlsym() returns NULL), you can get diagnostic information by calling dlerror(). The dlerror() function returns a string describing the cause of the latest error. You should call dlerror() only after an error has occurred; at other times, its return value is undefined.

An application with multiple threads that calls these functions must provide its own locking because dlerror() is not thread specific.

To unload a DSO, call dlclose():

#include <dlfcn.h>
void *dlhandle;
... load DSO, use DSO symbols ...
dlclose(dlhandle);

The dlclose function frees up the virtual address space that has been mmaped by the dlopen call of that file (similar to a munmap call). The difference, however, is that a dlclose on a file that has been opened multiple times (either through dlopen or program startup) does not cause the file to be munmaped until the file is no longer needed by the process.

Versioning of DSOs

This section describes the DSO versioning mechanism of SGI and includes the following topics:

The Versioning Mechanism

SGI uses a mechanism for the versioning of shared objects and executables. Note that this mechanism is outside the scope of the MIPS ABI, and, thus, must not be relied on for code that must be MIPS ABI-compliant and run on other vendors' platforms. Currently, all executables produced on SGI systems have a bit set that marks them as SGI_ONLY to allow use of the versioning mechanism.

Versioning is of interest mainly to developers of shared objects. It may not be of interest to you if you simply use shared objects. Versioning allows a developer to update a shared object in a way that may be incompatible with executables previously linked against the shared object. You can accomplish this by renaming the original shared object and providing it along with the (incompatible) new version.

What Is a Version?

A version is part or all of an identifying version_string that can be associated with a shared object by using the -set_version version_string option to ld when the shared object is created.

A version_string consists of one or more versions separated by colons (:). A single version has the form:

[comment#]sgimajor .minor

where:

  • comment: Specifies a comment string, which is ignored by the versioning mechanism. It consists of any sequence of characters followed by a pound sign (#). The comment is optional.

  • sgi: Specifies the literal string sgi.

  • major: Specifies the major version number, which is a string of digits [0-9].

  • .: Specifies a literal period.

  • minor: Specifies the minor version number, which is a string of digits [0-9].

Building a Shared Library Using Versioning

Follow these instructions when building your shared library:

When you first build your shared library, give it an initial version, for example, sgi1.0. Add the option -set_version sgi1.0 to the command to build your shared library (cc -shared, ld -shared ).

Whenever you make a compatible change to the shared object, create another version by changing the minor version number (for example, sgi1.1 ) and add it to the end of the version_string. The command to set the version of the shared library now looks like -set_version “sgi1.0:sgi1.1”.

When you make an incompatible change to the shared object:

  1. Change the filename of the old shared object by adding a dot followed by the major number of one of the versions to the filename of the shared object. Do not change the soname of the shared object or its contents. Simply rename the file.

  2. Update the major version number and set the version_string of the shared object (when you create it) to this new version; for example, -set_version sgi2.0.

This versioning mechanism affects executables in the following ways:

  • When an executable is linked against a shared object, the last version in the shared object's version_string is recorded in the executable as part of the liblist. You can examine this using elfdump -Dl.

  • When you run an executable, rld looks for the proper filename in its usual search routine.

  • If a file is found with the correct name, the version specified in the executable for this shared object is compared to each of the versions in the version_string in the shared object. If one of the versions in the version_string matches the executable's version exactly (ignoring comments), then that library is used.

  • If no proper match is found, a new filename for the shared object is built by combining the soname specified in the executable for this shared object and the major number found in the version specified in the executable for this shared object (soname.major). Remember that you did not change the soname of the object, only the filename. The new file is searched for using rld's usual search procedure.

Example of Versioning

For example, suppose you have a shared object foo.so with initial version sgi10.0. Over time, you make two compatible changes for foo.so that result in the following final version_string for foo.so:

initial_version#sgi10.0:upgrade#sgi10.1:new_devices#sgi10.2

You then link an executable that uses this shared object, useoldfoo. This executable specifies version sgi10.2 for soname foo.so. (Remember that the executable inherits the last version in the version_string of the shared object.)

The time comes to upgrade foo.so in an incompatible way. Note that the major version of foo.so is 10, so you move the existing foo.so to the filename foo.so.10 and create a new foo.so with the version_string:

efficient_interfaces#sgi11.0

New executables linked with foo.so use it directly. Older executables, like useoldfoo, attempt to use foo.so, but find that its version (sgi11.0) is not the version they need (sgi10.2). They then attempt to find a foo.so in the file name foo.so.10 with version sgi10.2.


Note: When a needed DSO has its interface changed, then a new version is created. If the interface change is not compatible with older versions, then a consuming shared object needs incompatible versions in order to use the new version, even if it does not use that part of the interface that is changed.