1. Overview
There are several steps executed while spawning a process in Linux. If a program depends on shared libraries, a dynamic linker is also loaded to the memory in addition to the executable binary program.
In this tutorial, we’ll discuss the dynamic linker, namely /lib64/ld-linux-x86-64.so.2. Firstly, we’ll briefly discuss the internals of running a program and where the dynamic linking takes place in this process. Then, we’ll see an example showing the usage of the dynamic linker.
2. Brief Information About the Dynamic Linker
Before learning about the dynamic linker, we’ll first briefly discuss the execution of a program to understand where the dynamic linker takes the stage.
2.1. How Is an Executable Binary Executed?
When we start a process from the command line, the shell first calls the execve() system call to execute the program. It opens the executable file, and after some preparation, loads the executable file into memory.
If the executable file has shared library dependencies, an interpreter is also loaded and run to assemble the dependent shared libraries. This interpreter is the dynamic linker.
Once the dynamic linker finishes its job, the control passes to the user program.
2.2. The Dynamic Linker
ELF (Executable and Linkable Format) is the standard binary file format in Linux.
An ELF file for an executable always has a part called the Program Header Table. This part provides information that is necessary while running the executable.
One of the entries in the Program Header Table that is important while running the executable is the PT_INTERP entry. This entry specifies the run-time or dynamic linker needed to assemble the complete program before running.
The dynamic linker performs several tasks:
- Locating the necessary shared libraries
- Loading the shared libraries into memory
- Resolving the program’s undefined symbols using the symbols in shared libraries
- Assembling the program so that the process can call the functions in the shared libraries at run-time
In other words, the interpreter specified by the PT_INTERP entry in the Program Header Table manages the shared libraries at run-time on behalf of the executable.
The dynamic linker is by default set to /lib64/ld-linux-x86-64.so.2 by gcc. We can check its presence using the readelf command:
$ readelf -l /usr/bin/ls | head -20
Elf file type is DYN (Shared object file)
Entry point 0x6b10
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000003510 0x0000000000003510 R 0x1000
LOAD 0x0000000000004000 0x0000000000004000 0x0000000000004000
0x0000000000013111 0x0000000000013111 R E 0x1000
LOAD 0x0000000000018000 0x0000000000018000 0x0000000000018000
0x0000000000007530 0x0000000000007530 R 0x1000
LOAD 0x000000000001ff70 0x0000000000020f70 0x0000000000020f70
We use the head command to have a shorter output. The -l option of readelf lists the program headers in an ELF file. We list the program headers of /usr/bin/ls as an example. As we see in the INTERP entry of the Program Headers, the executable requires the dynamic linker /lib64/ld-linux-x86-64.so.2 as the interpreter.
Therefore, the dynamic linker is run indirectly when we run an executable with shared library dependencies. However, it’s also possible to run a dynamically linked program directly by calling the dynamic linker. We’ll see an example in the next section.
The ldd command also lists the dynamic linker:
$ ldd /usr/bin/ls
linux-vdso.so.1 (0x00007fff54f89000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f1000b17000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f1000b0d000)
libc.so.6 => /lib64/libc.so.6 (0x00007f1000903000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f100086c000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1000b74000)
The dynamic linker is the last entry in the output.
3. An Example
In this section, we’ll experiment with the dynamic linker using an example.
3.1. Example Code
We’ll use the following C program in our example, hello.c:
#include <stdio.h>
void main(int ac, char **av)
{
printf("Hello %s\n", av[1]);
}
The program just greets the name passed to it as a parameter using the printf(“Hello %s\n”, av[1]) statement.
Let’s build the program using gcc to generate the executable binary hello:
$ gcc -o hello hello.c
$ ls -l hello
-rwxrwxr-x 1 centos centos 25680 Aug 27 05:17 hello
Having built the executable, let’s run it:
$ ./hello Baeldung
Hello Baeldung
The executable runs as expected.
3.2. Using the Dynamic Linker Explicitly
Now, let’s run hello using the dynamic linker, /lib64/ld-linux-x86-64.so.2:
$ /lib64/ld-linux-x86-64.so.2 ./hello Baeldung
Hello Baeldung
The executable runs as expected again. We just pass the executable together with its parameters to the dynamic linker.
We can run a binary file using the dynamic linker even if the binary file doesn’t have execute rights:
$ chmod -x hello
$ ls -l hello
-rw-rw-r-- 1 centos centos 25680 Aug 27 05:17 hello
$ /lib64/ld-linux-x86-64.so.2 ./hello Baeldung
Hello Baeldung
As the output shows, after removing the execute rights of the binary hello using chmod -x hello, we’re still able to run the application using the dynamic linker.
3.3. Setting the Dynamic Linker Explicitly
As we’ve already seen, gcc sets /lib64/ld-linux-x86-64.so.2 as the dynamic linker by default. However, it’s possible to set the dynamic linker explicitly during linkage.
Firstly, let’s copy /lib64/ld-linux-x86-64.so.2 to /tmp with the name my_ld.so:
$ cp /lib64/ld-linux-x86-64.so.2 /tmp/my_ld.so
We’ll specify /tmp/my_ld.so as the dynamic linker while building hello:
$ gcc -o hello hello.c -Wl,-I/tmp/my_ld.so
The -Wl option of gcc lets us pass linker options. The -Wl,-I/tmp/my_ld.so in our case specifies /tmp/my_ld.so to be used as the dynamic linker. Let’s check the PT_INTERP entry in the Program Header Table using readelf:
$ readelf -l ./hello | grep -B2 interpreter
INTERP 0x0000000000000318 0x0000000000400318 0x0000000000400318
0x000000000000000e 0x000000000000000e R 0x1
[Requesting program interpreter: /tmp/my_ld.so]
We use grep to refine the output of readelf. As is apparent from the output, the dynamic linker is now /tmp/my_ld.so. Let’s run hello using /tmp/my_ld.so:
$ /tmp/my_ld.so ./hello Baeldung
Hello Baeldung
The program runs as expected. Therefore, we’re successful in changing the default dynamic linker.
4. Conclusion
In this article, we discussed the dynamic linker. We saw that the dynamic linker plays a crucial role in managing the shared library dependencies of a process while spawning it.
We learned that /lib64/ld-linux-x86-64.so.2 is the default dynamic linker. Then, we discussed an example. We saw that we could change the dynamic linker while linking the program.