1. Overview

The ld command, also known as the GNU linker, is a command-line utility included in the GNU Binutils package. Its primary function as a linker is to resolve references in a program by linking object and library files, thus creating executable programs or libraries. It performs this task by searching for references to objects or libraries from a list of search paths that we’ve set or provided earlier.

In this tutorial, we’ll explore some of the ld linker’s search paths and its resolution order. In addition, we’ll also create a practical example in C programming language to verify the search path resolution order.

All commands in this guide have been tested on Debian 12 (Bookworm) running gcc 12.2.0, and GNU Binutils 2.40 for the ld and ar commands.

2. The ld Linker

The ld command or linker plays an important role in building software because it resolves all references in a program to build executables or libraries. As the software’s complexity increases, the ld linker can become more complicated. However, compilers, such as gcc or g++, automatically invoke ld as part of the build process by default.

Although it may seem impractical to run the linking command as a separate step in the build process, there are advantages of separating the compiling and linking processes. For example, it allows for faster building by recompiling only the changed source files instead of the whole project. Moreover, it enables interoperability by linking object files from different compilers. Further, it supports modular development by grouping source files into various object files.

3. The ld Linker Search Paths

The ld command resolves the references to objects or libraries by scanning its pre-defined or default search paths. However, we can also provide other search paths that it’ll search during the linking process.

Let’s explore some of the ld‘s search paths, methods to add them during the linking process, and their resolution order.

3.1. Default Search Paths

We can print ld‘s default search paths simply by passing the –verbose option to the ld command:

$ ld --verbose
GNU ld (GNU Binutils for Debian) 2.40
...
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR(...
...

The command will print a bunch of information, but we’re only interested in the SEARCH_DIR line. Let’s format that line to make it readable:

$ ld --verbose | grep SEARCH_DIR | tr -s ' ;' \\012
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu")
SEARCH_DIR("=/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu")
...

In the command chain above, we piped ld‘s output to the grep command. Next, grep found and printed lines containing the keyword SEARCH_DIR. Then, we piped grep‘s output to the tr command, which replaced occurrences of space and semi-colon (-s ‘ ;’) with newline characters (\\012, represented in octal format).

3.2. Compiler Search Paths

Besides its own default search paths, the linker also accepts library search paths from the compiler:

$ gcc -print-search-dirs | grep libraries | tr -s ' ;=:' \\012
libraries
/usr/lib/gcc/x86_64-linux-gnu/12/
/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/12/
/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/
/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/lib/../lib/
...

The command chain above works similarly to the one we used for ld‘s output in the previous section.

First, we printed a list of the search directories of the gcc command (gcc -print-search-dirs). Next, we piped the output to the grep command, which found and printed lines containing the keyword libraries.

Finally, we piped grep‘s output to the tr command, which replaced occurrences of space, semi-colon, equal sign, and colon (-s ‘ ;=:’) with newline characters (\\012, represented in octal format).

3.3. The -L Command Line Flag

Another method to specify a search path for the linker is by using the compiler flag -L:

$ gcc [options] -o output -L /path/to/lib/dir source.c -lmylib

We can pass the -L flag more than once, and the linker will search those directories in the order that we provide them. However, if we have a library with the same name in multiple locations, the linker will use the first one it finds and ignore any subsequent occurrences.

Since the library name in the above code snippet is mylib, the linker will search for either libmylib.so or libmylib.a.

3.4. Search Path Resolution Order

The linker resolves dependencies by searching for them in a specific order. It checks:

  • directories specified with the -L option
  • default system library directories
  • RPATH/RUNPATH attributes
  • LD_LIBRARY_PATH environment variable
  • directories specified with -rpath or -rpath-link options
  • default library paths
  • user-specified paths in configuration files
  • toolchain-specific paths
  • project-specific paths

If a library with the same name exists in multiple locations within the search paths, the linker will use the first one it finds and ignore any subsequent occurrences.

4. Verifying the Resolution Order of ld Linker Search Paths

Here, we’ll create a simple C program to demonstrate the order in which the ld linker searches for reference resolution.

4.1. Preparing the Source Files

Let’s develop a program that calls a function from a static library. However, we’ll prepare two identical static libraries which have the same function, and store them in separate directories. Then, we’ll use the -L option flag to pass both directory paths during the linking process. This will enable us to verify that the linker uses the first library it finds with the required function and ignores any subsequent occurrences.

Here are our source files:

$ cat > myprogram.c << EOF
#include <stdio.h>

extern void hello();

int main() {
    hello();
    return 0;
}
EOF
$ mkdir lib1 lib2
$ cat > lib1/mylib.c << EOF
#include <stdio.h>

void hello() {
    printf("Hello from dir lib1\n");
}
EOF
$ cat > lib2/mylib.c << EOF
#include <stdio.h>

void hello() {
    printf("Hello from dir lib2\n");
}
EOF

And here’s our directory structure:

$ tree
.
├── lib1
│   └── mylib.c
├── lib2
│   └── mylib.c
└── myprogram.c

3 directories, 3 files

We used the cat command to append lines to a file by reading the input until it detects certain text (EOF). Afterward, we created a directory using the mkdir command, and finally, we printed the directory structure using the tree command.

4.2. Building the Static Libraries

Let’s build both static libraries:

$ gcc -c lib1/mylib.c -o lib1/mylib.o
$ gcc -c lib2/mylib.c -o lib2/mylib.o
$ ar rcs lib1/libmylib.a lib1/mylib.o
$ ar rcs lib2/libmylib.a lib2/mylib.o
$ tree
.
├── lib1
│   ├── libmylib.a
│   ├── mylib.c
│   └── mylib.o
├── lib2
│   ├── libmylib.a
│   ├── mylib.c
│   └── mylib.o
└── myprogram.c

3 directories, 7 files

First, we compiled all .c source files using gcc with the -c option, which generated the .o object files.

Afterward, we used the ar command with crs options to build the .a archive files. The crs options tell the ar command to create (c) an archive file named libmylib.a in their respective directories, replace or add (r) the object file (mylib.o) to the archive file, and create symbol table (s) for the archive to improve linking efficiency.

4.3. Compiling the Main Program

Next, let’s compile the main program:

$ gcc -c myprogram.c -o myprogram.o

We passed the -c option to tell gcc to compile the code without doing the linking process. The compilation process generated the object file myprogram.o.

4.4. Linking the Main Program

And finally, we only need to link the main program with the static library to build an executable:

$ gcc -o myprogram myprogram.o -L./lib2 -L./lib1 -lmylib

We used gcc instead of the ld command for the linking process because gcc simplifies the process, which can be quite complex if we were to use ld. gcc handles various aspects of linking, minimizing potential errors, and ensuring compatibility with our system to be able to run the program.

Please be aware that in the command above, we specified lib2 directory first using the -L flag, and then lib1.

The linking process created an executable binary file named myprogram with execute permissions.

4.5. Running the Main Program

Let’s run our program that’s been linked with the static library and verify which static library the linker used:

$ ./myprogram
Hello from dir lib2

The output above confirms that the linker resolved the reference to the hello function with the first library file that it found, which is mylib.a in the lib2 directory. This resolution was based on the order in which we passed the -L flag.

Rearranging the -L flag during linking should give a different output:

$ gcc -o myprogram myprogram.o -L./lib1 -L./lib2 -lmylib
$ ./myprogram
Hello from dir lib1

The output shows that the linker used the static library from lib1 directory.

5. Conclusion

In this article, we explored the ld linker’s search paths. As the linker’s primary role is to resolve references within a program, it follows a specific resolution order while searching through directories.

In addition, we also learned a method to specify search paths for the linker using the compiler’s -L flag. Finally, we made a practical example in C programming language to demonstrate the linker’s search path resolution order.