This writeup was inspired by a conversation on Mastodon.
It is really easy to do something silly with an ELF file, such as accidentally create a dynamically-linked one as opposed to a statically-linked one. This is incredibly easy to do because in nine out of ten situations, you want to create an ELF program that uses shared libraries.
Also, perhaps counter-intuitively, when linking a PIE binary, it is an ELF shared object with an entrypoint, not an ELF executable. It behaves like an executable, but it also behaves as a shared library, literally existing in both worlds.
This depends on how you link your program.
First off, a few simple rules:
-fPIC
or your compiler’s equivalent.
In both gcc
and clang
, the -fPIC
option creates position-independent
code. This is good when creating a shared library, but most often is not
what you want. There is an exception for certain embedded architectures,
which can only do position-independent code, but in that case the compiler
will do this implicitly anyway.-fPIE
option (or, again, your
linker’s equivalent flag). This will generate a shared object, which is
always dynamically-linked.DT_NEEDED
libraries.)Here is the Makefile:
all: self
self.o: self.c
gcc -std=gnu11 -ffreestanding -c -o $@ $^
self: self.o
gcc -static -nostdlib -o $@ $^
And the C program:
int main(void);
void
exit(int rc) {
asm("mov $60, %rax");
asm("syscall");
}
void
_start() {
exit(main());
}
int
main() {
return 42;
}
In the compilation step, we tell the compiler that we want to compile a
“freestanding” module (that is what the -ffreestanding
flag does, of course).
That is a fancy way of telling the C compiler that it cannot rely on the fact
that a C runtime will be present. Because it will not be. Our program is
precisely one self-contained file that we desire to have statically linked.
In the linking step, we also specify -static
and -nostdlib
, which tells
the linker that we want a static executable, and also not to try linking with
any of the C runtime at all. This includes the crt0
, crti
, etc. modules
that are normally linked-in to perform C runtime startup and teardown functions.
We can confirm all of this by looking at the output of readelf -Wa
:
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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x40011f
Start of program headers: 64 (bytes into file)
Start of section headers: 528 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 6
Section header string table index: 5
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 00000000004000e8 0000e8 000024 00 A 0 0 4
[ 2] .text PROGBITS 000000000040010c 00010c 000031 00 AX 0 0 1
[ 3] .eh_frame PROGBITS 0000000000400140 000140 000078 00 A 0 0 8
[ 4] .comment PROGBITS 0000000000000000 0001b8 00001a 01 MS 0 0 1
[ 5] .shstrtab STRTAB 0000000000000000 0001d2 000037 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x0001b8 0x0001b8 R E 0x200000
NOTE 0x0000e8 0x00000000004000e8 0x00000000004000e8 0x000024 0x000024 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .text .eh_frame
01 .note.gnu.build-id
02
There is no dynamic section in this file.
There are no relocations in this file.
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
No version information found in this file.
Displaying notes found in: .note.gnu.build-id
Owner Data size Description
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring) Build ID: c752a85d9939ea2dfe5fdc10b9e887093bff582e
Compare this with the output of e.g., readelf -Wa /bin/ls
on any Linux system
(modern or not, it doesn’t really matter, it’s going to be pretty much the same
going all the way back to systems running Linux 2.0) and you will see a huge
difference from this simple example program.
The code present in the exit
function could be changed out for any other code
targeted at any other environment. It could be changed to a DOS API call so
that the example would work on DOS (moot, because there is no dynamic linking
in DOS, but the point remains). It just so happens that the system I use is
a Linux system, and I didn’t want to have to send a signal to terminate the
program.
Check out the ELF article at the OSDev Wiki (one of my favorite places, actually) which contains more details, including a link to the low-level ABI details specifying things like the starting state of an ELF executable and other fun stuff.
Thanks for reading.
If you appreciated this article (or anything else I’ve written), please consider donating to help me out with my expenses—and thanks!