Appearance
Linux executables are not ET_EXEC
Many think for an ELF to be an executable, its type must be ET_EXEC
, this is false.
TL;DR:
Almost all Linux executables are dynamically linked against glibc
and compiled with -fPIE
, they have type ET_DYN
despite being executable.
One way implication
An ELF having type ET_EXEC
implies it's an executable, but the converse is false.
By default, gcc
on modern Linux links to glibc
dynamically and enables -fPIE
(Position-Independent Executable) since version 6.2. The result has DT_DYN
while being an executable.
c
// main.c
#include <stdio.h>
int main() {
printf("Hello World\n");
}
bash
gcc main.c -o main
readelf -h ./main
outputs:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1040
Start of program headers: 64 (bytes into file)
Start of section headers: 13520 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
PIE implies DYN
To support ASLR, you must compile with -fPIE
or -fPIC
. After all, the entire purpose of ASLR is to place your instructions at a random address, if the executable must be run at a particular offset, ASLR can't work by definition.
PIE doesn't imply static linking
If you want want to avoid taking on a versioned dependency on glibc
, you can compile with
bash
gcc -static-pic main.c -o main
The ELF type is still ET_DYN
bash
readelf -h ./main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x77e0
Start of program headers: 64 (bytes into file)
Start of section headers: 806312 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 34
Section header string table index: 33
but it's statically linked
bash
ldd ./main
statically linked
ET_EXEC is bad actually
To actually obtain an ELF with DT_EXEC
, you have to compile without PIE.
bash
gcc -static main.c -o main
readelf -h ./main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401720
Start of program headers: 64 (bytes into file)
Start of section headers: 769304 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 10
Size of section headers: 64 (bytes)
Number of section headers: 28
Section header string table index: 27
As mentioned earlier, you can't utilize ASLR when the code is not position independent.
glibc static linking is not what you think it is
It's a well known fact that glibc
doesn't like being statically linked, in particular, any functions related to NSS, which importantly handles domain resolution/DNS, cannot be entirely statically linked.
shameless plug: for more reasons why DNS on Linux is bad, see my other blog.
Suppose a file nss.c
calls to getaddrinfo(3)
, compiling it with -static
, you will get the following warnings.
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
While readelf -h ./nss
might tell you it's an EXEC
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401a80
Start of program headers: 64 (bytes into file)
Start of section headers: 1012760 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 10
Size of section headers: 64 (bytes)
Number of section headers: 28
Section header string table index: 27
and file ./nss
and ldd ./nss
might seen to imply it doesn't need any runtime component,
nss: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=15a59367bfed4d9f1b6f4d0efdd71d51ef3bac76, for GNU/Linux 4.4.0, not stripped
not a dynamic executable
running it with
strace -e trace=openat nss <whatever args>
reveals that if you don't have the component at runtime, it won't work
openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/host.conf", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libnss_mymachines.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/ld-linux-x86-64.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/run/systemd/machines/google.ca", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libnss_mdns_minimal.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libnss_resolve.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/gai.conf", O_RDONLY|O_CLOEXEC) = 3
Hence, whether an ELF is ET_EXEC
or ET_DYN
tells you little about whether it's statically linked or not, and it tells you little about it's an executable or not. It should not be relied on.
Can an ELF be both a library and an executable?
Yes! Two major examples, /usr/lib/libc.so.6
and /usr/lib/ld-linux-x86-64.so.2
are mostly used as dynamic libraries but can be executed directly.
liangw@RAM-dump ~/d/tmp> /usr/lib/libc.so.6
GNU C Library (GNU libc) stable release version 2.40.
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 14.2.1 20240805.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 4.4.0
For bug reporting instructions, please see:
<https://gitlab.archlinux.org/archlinux/packaging/packages/glibc/-/issues>.
liangw@RAM-dump ~/d/tmp> /usr/lib/ld-linux-x86-64.so.2
/usr/lib/ld-linux-x86-64.so.2: missing program name
Try '/usr/lib/ld-linux-x86-64.so.2 --help' for more information.
In a future blog, I'll show you how this is done. You can even have a .so
that has a main()
function while still usable as a library.