Dynamic Linking

Updated: May 06, 2022

In a typical system, a number of programs will be running. Each program relies on a number of functions, some of which will be “standard” C library functions, like printf(), malloc(), write(), etc.

If every program uses the standard C library, it follows that each program would normally have a unique copy of this particular library present within it. Unfortunately, this results in wasted resources. Since the C library is common, it makes more sense to have each program reference the common instance of that library, instead of having each program contain a copy of the library. This approach yields several advantages, not the least of which is the savings in terms of total system memory required.

Before we go any further, let's look at some terminology:

Linker
A tool, such as ld, that you typically run just after compiling your program, in order to combine object and archive files, relocate their data, and resolve symbol references.
Runtime linker
A tool that finds and loads shared objects when you run your program. This is also known as a dynamic linker, but we'll use runtime linker to avoid any confusion with dynamic linking, which the (non-runtime) linker does.

The name of the runtime linker is ldd (which is also the name of a utility that lists the shared objects that a program requires). In the .interp section of an ELF file, it's called /usr/lib/ldqnx.so for 32-bit targets, and /usr/lib/ldqnx-64.so for 64-bit targets. You'll need to include the appropriate version in your OS image; see the entry for mkifs in the Utilities Reference for more details.

Statically linked
The program and the particular library that it's linked against are combined by the linker at link time.

This means that the binding between the program and the particular library is fixed and known at link time—well in advance of the program's ever running. It also means that we can't change this binding, unless we relink the program with a new version of the library.

You might consider linking a program statically in cases where you weren't sure whether the correct version of a library will be available at runtime, or if you were testing a new version of a library that you don't yet want to install as shared.

Programs that are linked statically are linked against archives of objects (libraries) that typically have the extension of .a. An example of such a collection of objects is the standard C library, libc.a.

Dynamically linked
The program and the particular library it references aren't combined by the linker at link time.

Instead, the linker places information into the executable that tells the loader which shared object module the code is in and which runtime linker should be used to find and bind the references. This means that the binding between the program and the shared object is done at runtime—before the program starts, the appropriate shared objects are found and bound.

This type of program is called a partially bound executable, because it isn't fully resolved—the linker, at link time, didn't cause all the referenced symbols in the program to be associated with specific code from the library. Instead, the linker simply said: “This program calls some functions within a particular shared object, so I'll just make a note of which shared object these functions are in, and continue on.” Effectively, this defers the binding until runtime.

Programs that are linked dynamically are linked against shared objects that have the extension .so. An example of such an object is the shared object version of the standard C library, libc.so.

You use a command-line option to the compiler driver qcc to tell the tool chain whether you're linking statically or dynamically. This command-line option then determines the extension used (either .a or .so).

Augmenting code at runtime

Taking this one step further, a program may not know which functions it needs to call until it's running. While this may seem a little strange initially (after all, how could a program not know what functions it's going to call?), it really can be a very powerful feature. Here's why.

Consider a “generic” disk driver. It starts, probes the hardware, and detects a hard disk. The driver would then dynamically load the io-blk code to handle the disk blocks, because it found a block-oriented device. Now that the driver has access to the disk at the block level, it finds two partitions present on the disk: a DOS partition and a Power-Safe partition. Rather than force the disk driver to contain filesystem drivers for all possible partition types it may encounter, we kept it simple: it doesn't have any filesystem drivers! At runtime, it detects the two partitions and then knows that it should load the fs-dos.so and fs-qnx6.so filesystem code to handle those partitions.

By deferring the decision of which functions to call, we've enhanced the flexibility of the disk driver (and also reduced its size).