In a previous posting I described the use of lazy loading. Of course, when we initially played with an implementation of this technology, a couple of applications immediately fell over. It turns out that a fall back was necessary.
Let's say an application developer creates an application with two dependencies. The developer wishes to employ lazy loading for both dependencies.
% ldd main foo.so => ./foo.so bar.so => ./bar.so ...
The application developer has no control over the dependency bar.so, as this dependency is provided by an outside party. In addition, this shared object has its own dependency on foo.so, however it does not express the required dependency information. If we were to inspect this dependency, we would see that it is not ldd(1) clean.
% ldd -r bar.so symbol not found: foo (./bar.so)
The only reason this library has been successfully employed by any application is because the application, or some other shared object within the process, has made the dependency foo.so available. This is probably more by accident than design, but sadly it is an all to common occurrence.
Now, suppose the application main makes reference to a symbol that causes the lazy loading of bar.so before the application makes reference to a symbol that would cause the lazy loading of foo.so to occur.
% LD_DEBUG=bindings,symbols,files main ..... 07683: 1: transferring control: ./main ..... 07683: 1: file=bar.so; lazy loading from file=./main: symbol=bar ..... 07683: 1: binding file=./main to file=./bar.so: symbol `bar'
When control is passed to bar(), the reference it makes to its implicit dependency foo() is not going to be found, because the shared object foo.so is not yet available. Because this scenario is so common, the runtime linker provides a fall back. If a symbol can not be found, and lazy loadable dependencies are still pending, the runtime linker will process these pending dependencies in a final attempt to locate the symbol. This can be observed from the remaining debugging output.
07683: 1: symbol=foo; lookup in file=./main [ ELF ] 07683: 1: symbol=foo; lookup in file=./bar.so [ ELF ] 07683: 1: 07683: 1: rescanning for lazy dependencies for symbol: foo 07683: 1: 07683: 1: file=foo.so; lazy loading from file=./main: symbol=foo ..... 07683: 1: binding file=./bar.so to file=./foo.so: symbol `foo'
Of course, there can be a down-side to this fall back. If main were to have many lazy loadable dependencies, each will be processed until foo() is found. Thus, several dependencies may get loaded that aren't necessary. The use of lazy loading is never going to be more expensive than non-lazy loading, but if this fall back mechanism has to kick in to find implicit dependencies, the advantage of lazy loading is going to be compromised.
To prevent lazy loading from being compromised, always record those dependencies you need (and nothing else).