Mike’s Place

Creating Statically Linked ELF Executables

This writeup was inspired by a conversation on Mastodon.

The Problem

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.

The Solution

This depends on how you link your program.

First off, a few simple rules:

An example

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);

exit(int rc) {
    asm("mov $60, %rax");

_start() {

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 

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.

But! That’s a Linux 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.

Additional Information

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!