Shared libraries as executables
Aug 20th, 2022
Typically you either have an executable or a shared library, but can you have an executable shared library too?
The ELF file header has a tag for the object type, which includes ET_EXEC
or ET_DYN
. That suggests an ELF file is either an executable or a shared library. However, in practice many executables are in fact ET_DYN
objects:
$ cat hello.c #include <stdio.h> void hello() { puts("hello"); } int main() { hello(); hello(); } $ gcc -o hello hello.c $ ./hello hello hello $ file hello hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bec6318280e1ed6d7d271d870a7d22f589c233f2, for GNU/Linux 3.2.0, not stripped
Notice that file
outputs shared object which means the executable is an ET_DYN
object.
The reason for this is that my GCC is configured to enable -fpie
by default.
Trying to link against hello
However, the hello
executable is not particularly useful as a shared library, since it does not export the hello
symbol:
$ nm --dynamic --defined-only hello $ # nothing here.
If we compile it with -shared
both symbols get exposed
$ gcc -o hello -shared hello.c $ nm --dynamic --defined-only hello 0000000000001139 T hello 0000000000001150 T main
The main
symbol in a shared library certainly looks odd, but the linker does not complain:
$ cat main.c void hello(); int main(){ hello(); } $ gcc -o main main.c -L. -l:hello -Wl,-rpath,. $ ./main hello
However, by specifying -shared
we lost the ability to meaningfully execute hello
:
$ ./hello Segmentation fault (core dumped)
Giving the shared library a program interpreter
One effect of specifying -shared
is that the linker will not add a PT_INTERP
type program header to the binary. This program header specifies an absolute path to the interpreter, which is typically the dynamic linker. When executing an ELF file, the kernel parses the ELF file and looks for the interpreter. When it finds it, it effectively executes the interpreter instead and gives it a file descriptor of the ELF file, so the dynamic linker can continue to parse the ELF file, mapping the relevant sections of it and its dependencies into memory before executing the entry point.
So, to create a shared executable, we will need to manually create a PT_INTERP
section. In my case the dynamic linker lives in /lib64/ld-linux-x86-64.so.2
. Registering it goes as follows:
$ cat hello.c #include <stdio.h> const char interp[] __attribute__ ((section(".interp"))) = PT_INTERP; void hello() { puts("hello"); } int main() { hello(); hello(); } $ gcc -shared -o hello hello.c -D 'PT_INTERP="/lib64/ld-linux-x86-64.so.2"' $ file hello hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=492b8f47d786a0d3d7152e131fd2c096135f1e53, not stripped
So, we now have a shared library with an interpreter. Unfortunately it's not quite enough:
$ ./hello Segmentation fault (core dumped)
Setting the entrypoint
To fix the segfault, we need to inform the linker what symbol to use as entrypoint of execution:
$ gcc -shared -o hello -Wl,--entry,main hello.c $ ./hello hello hello Segmentation fault (core dumped)
So, it almost works, except for a segfault on exit. The reason is we're missing an exit
call:
$ cat hello.c #include <stdio.h> #include <stdlib.h> const char interp[] __attribute__ ((section(".interp"))) = PT_INTERP; void hello() { puts("hello"); } int main() { hello(); hello(); exit(EXIT_SUCCESS); } $ gcc -shared -o hello -Wl,--entry,main hello.c -D 'PT_INTERP="/lib64/ld-linux-x86-64.so.2"' $ ./hello hello hello
Linking an executable against hello
again
Finally, let's verify our main
executable can be linked against the hello
executable:
$ gcc -o main main.c -L. -l:hello -Wl,-rpath,. $ ldd main | grep hello hello => ./hello (0x00007fd419bb6000) $ ./main hello
Success!
Examples in the wild
So far I've seen two cases where shared libraries are used as executables.
The first and most obvious example is the dynamic linker itself. For example glibc
's dynamic linker allows you to run an executable as follows:
$ /lib64/ld-linux-x86-64.so.2 ./main hello
The use case is that you can run existing executables under a new glibc version without the need to modify the PT_INTERP
in the executable.
Another neat use case is truly relocatable and self-contained software. If you want to ship your software with all its dependencies up to glibc
to another system, you immediately hit a Linux issue where PT_INTERP
can only be an absolute path, so you can't make it point to your own glibc
, and you'll end up using the possibly incompatible system glibc
(if it is even available...). This can be worked around by replacing your executables with a script that invokes the dynamic linker directly, since the dynamic linker itself does not specify an interpreter.
The second example is libQt5Core.so
, which dumps version info when executing it. Sounds useful!
$ ./libQt5Core.so.5 This is the QtCore library version Qt 5.15.2 ...