Is there an ELF file that could be both executed or dynamically loaded? An answer is the default dynamic loader in Linux: ld.so 1, it usually points to /lib/ld-*.so or /lib64/ld-*.so where * represents the version of glibc in your system.

When you execute a normal program like ls, the dynamic load is responsible for loading the dependent libraries the target program needs (you can see the needed libraries and how they are resolved via ldd). When you execute the dynamic loader itself, the output would be:

$ /lib64/ld-2.17.so
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file.  This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it.  You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run.  This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.

Dynamic Loader

For example, when you loss the executable permission of chmod, you can use /lib64/ld-2.17.so to restore it back:

$ /lib64/ld-2.17.so /bin/chmod +x /bin/chmod

But the chmod does not have executable permission, how does it could be executed?

When a executable is invoked, the system will decide which loader to load it from the .interp section of the ELF executable. You can see it with objdump like this:

$ objdump -s -j .interp /usr/bin/ls
 
/usr/bin/ls:     file format elf64-x86-64
 
Contents of section .interp:
 400238 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
 400248 7838362d 36342e73 6f2e3200           x86-64.so.2.

So when you type chmod, the system kernel equivalently runs /lib64/ld-2.17.so /bin/chmod as you do manually. Then the shell will fork a process and use the execve system call to load the program. Since the first argument is lib64/ld-2.17.so which has the executable permission, the execve permission check does not panic and /bin/chmod is just the argument passed to the loader. It works the same way as bash a.sh and ./a.sh.

So when a dynamically-linked program is called, the system firstly load its dynamic linker into the virtual memory space of that process, then start to executing code of the linker to load necessary libraries into memory space and resolve undefined symbols to the correct definitions in those libraries. After the link process, the interpreter can start the origin program itself, at the address recorded at the executable’s ELF header. 2

Executable Shared Object

Since ld.so could be executed, it should be an ELF executable. But it’s also a shared object:

$ file /lib64/ld-2.17.so
/lib64/ld-2.17.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=62c449974331341bb08dcce3859560a22af1e172, not stripped

This answer 3 gives how to compile such target: add -pie -fPIC -wl,-E flags to the compiler so the target is compiled as shared library (-fPIC) but also a regular executable (-pie), and it’s symbol table is exportable (-Wl,-E) such that it can be usefully linked against. Unless only those symbols that are referenced would be exported.

But why ld.so needs to be both shared library and executable? If it is only an executable, then it must be loaded at the address it is linked at, which may be conflict with the origin target a.out is linked to 4.

Moreover, there is another approach named dynamic loading which is commonly used like plugin. It allows a program to load and resolve symbols from dynamic shared libraries at runtime. Therefore it needs ld.so already loaded to help load other dependencies for the libraries to be opened.

How does ld.so Load Itself?

The ldd command gives a conflict result with the one that file gives (as shown above):

$ ldd /lib64/ld-2.17.so
        statically linked

One says the loader is dynamic linked and the other says it’s statically linked. Why?

In fact, file and ldd report different things for the dynamic linker because they have different definitions of what constitutes a statically-linked binary. For ldd, a binary is statically linked if it has no DT_NEEDED symbols, i.e. no undefined symbols. For file, an ELF binary is statically linked if it doesn’t have a PT_DYNAMIC section 5. ldd is indeed just a script, for example, ldd /usr/bin/ls is equivalent to LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 /usr/bin/ls. The environment variable set is used to inspect the program’s dynamic dependencies.

So ld-2.17.so itself it statically compiled and does not rely on any other libraries for it does not contain the .interp section. The code executes bootstraps and carefully relocates ld.so itself.

Footnotes

  1. man page of ld.so ↩

  2. an article explaining how programs get run ↩

  3. How to build runnable shared library ↩

  4. Why is ld.so a shared object? ↩

  5. How does the dynamic loader itself be dynamically linked? ↩