Lazy binding

Lazy binding (also known as lazy linking or on-demand symbol resolution) is the process by which symbol resolution isn't done until a symbol is actually used. Functions can be bound on-demand, but data references can't.

All dynamically resolved functions are called via a Procedure Linkage Table (PLT) stub. A PLT stub uses relative addressing, using the Global Offset Table (GOT) to retrieve the offset. The PLT knows where the GOT is, and uses the offset to this table (determined at program linking time) to read the destination function's address and make a jump to it.

To be able to do that, the GOT must be populated with the appropriate addresses. Lazy binding is implemented by providing some stub code that gets called the first time a function call to a lazy-resolved symbol is made. This stub is responsible for setting up the necessary information for a binding function that the runtime linker provides. The stub code then jumps to it.

The binding function sets up the arguments for the resolving function, calls it, and then jumps to the address returned from resolving function. The next time that user code calls this function, the PLT stub jumps directly to the resolved address, since the resolved value is now in the GOT. (GOT is initially populated with the address of this special stub; the runtime linker does only a simple relocation for the load base.)

The semantics of lazy-bound (on-demand) and now-bound (at load time) programs are the same:

  • In the bind-now case, the application fails to load if a symbol couldn't be resolved.
  • In the lazy-bound case, it doesn't fail right away (since it didn't check to see if it could resolve all the symbols) but will still fail on the first call to an unresolved symbol. This doesn't change even if the application later calls dlopen() to load an object that defines that symbol, because the application can't change the resolution scope. The only exceptions to this rule are objects loaded using dlopen() with the RTLD_LAZY flag (see below).

Lazy binding is controlled by the -z option to the linker, ld . This option takes keywords as an argument; the keywords include (among others):

lazy
When generating an executable or shared library, mark it to tell the dynamic linker to defer function-call resolution to the point when the function is called (lazy binding), rather than at load time.
now
When generating an executable or shared library, mark it to tell the dynamic linker to resolve all symbols when the program is started, or when the shared library is linked to using dlopen() , instead of deferring function-call resolution to the point when the function is first called.

Lazy binding is the default. If you're using qcc (as we recommend), use the -W option to pass the -z option to ld. For example, specify -Wl,-zlazy or -Wl,-znow.

There are cases where the default lazy binding isn't desired. For example:

  • While the system is under development, you might want to fully resolve all symbols right away, to catch library mismatches; your application would fail to load if a referenced function couldn't be resolved.
  • You might want to fully resolve the symbols for a particular object at load time.
  • You might want only a given program to be always bound right away.

There's a way to do each of these:

  • To change the default lazy binding to the bind now behavior for all processes started from a given shell, set the LD_BIND_NOW environment variable to a non-null value. For example:
    LD_BIND_NOW=1 ./foobar
      
    By default, pdebug sets LD_BIND_NOW to 1.

    Without LD_BIND_NOW, you'd see a different backtrace for the first function call into the shared object as the runtime linker resolves the symbol. On subsequent calls to the same function, the backtrace would be as expected. You can prevent pdebug from setting LD_BIND_NOW by specifying the -l (el) option.

  • To override the binding strategy for a given shared object, link it with the -znow linker option:
    qcc -Wl,-znow -o libfoo.so foo.o bar.o
      
  • To override the binding for all objects of a given program, link the program's executable with the -znow option:
    qcc -Wl,-znow -o foobar -lfoo.so -lbar.so
      

To see if a binary was built with -znow, type:

readelf -d my_binary

The output will include the BIND_NOW dynamic tag if -znow was used when linking.

You can use the DL_DEBUG environment variable to get the runtime linker to display some debugging information. For more information, see Diagnostics and debugging and Environment variables , later in this chapter.

Applications with many symbols — typically C++ applications — benefit the most from lazy binding. For many C applications, the difference is negligible.

Lazy binding does introduce some overhead; it takes longer to resolve N symbols using lazy binding than with immediate resolution. There are two aspects that potentially save time or at least improve the user's perception of system performance:

  • When you start an application, the runtime linker doesn't resolve all symbols, so you may expect to see the initial screen sooner, providing your initialization prior to displaying the screen doesn't end up calling most of the symbols anyway.
  • When the application is running, many symbols won't be used and thus they aren't looked up.

Both of the above are typically true for C++ applications.

Lazy binding could affect realtime performance because there's a delay the first time you access each unresolved symbol, but this delay isn't likely to be significant, especially on fast machines. If this delay is a problem, use -znow

It isn't sufficient to use -znow on the shared object that has a function definition for handling something critical; the whole process must be resolved now. For example, you should probably link driver executables with -znow or run drivers with LD_BIND_NOW.