Fundamentals 20 min read

Demystifying ELF: How Linux Turns a Binary into a Running Process

This article explains the ELF (Executable and Linkable Format) file structure, how to identify ELF binaries, the roles of headers, program and section tables, the compilation‑to‑execution lifecycle, and practical tools for inspecting and manipulating ELF files on Linux.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Demystifying ELF: How Linux Turns a Binary into a Running Process

What is an ELF file?

ELF (Executable and Linkable Format) is the standard binary format on Linux. It can represent executable files, object files (.o), shared libraries (.so) and core dump files. An ELF file is a container that stores code, data and metadata in a defined layout.

Identifying an ELF file

Use the file command:

$ file /bin/ls
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, ...

Or inspect the first bytes with hexdump:

$ hexdump -C -n 16 /bin/ls
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|

The magic number 7f 45 4c 46 (".ELF") identifies the file as ELF.

ELF internal structure

ELF Header : basic file information and offsets to other tables.

Program Header Table : tells the loader how to map segments into memory.

Section Header Table : describes sections for linkers and debuggers.

The body consists of sections (e.g., .text, .data) or segments that hold the actual code and data.

ELF Header

Display with readelf -h:

$ readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 ...
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x5850
  Start of program headers:          64 (bytes into file)
  Start of section headers:          136912 (bytes into file)
  ...

Key fields: entry point address, offset of program header table, offset of section header table.

Program Header Table

Show with readelf -l:

$ readelf -l /bin/ls
Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flags  Align
  PHDR           0x40     0x400040           0x400040           0x2d8   0x2d8   R      0x8
  INTERP         0x318    0x400318           0x400318           0x1c    0x1c    R      0x1
    [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0      0x0                0x0                0x4428  0x4428  R      0x1000
  LOAD           0x5000   0x5000             0x5000             0x12d1c 0x12d1c R E    0x1000
  ...

Segments of type LOAD are mapped into memory. The Flags column (R, W, E) defines read, write and execute permissions.

Section Header Table

Show with readelf -S:

$ readelf -S /bin/ls
There are 30 section headers, starting at offset 0x21730:
Section Headers:
  [Nr] Name              Type            Address          Offset   Size    EntSize Flags  Link  Info  Align
  [ 0]                  NULL            0x0000000000000000 0x00000000 0x00000000 0      0      0     0     0
  [ 1] .interp           PROGBITS        0x0000000000000318 0x00000318 0x0000001c 0      A      0     0     1
  [11] .text            PROGBITS        0x00000000000052a0 0x000052a0 0x0012a7c 0      AX     0     0    16
  [23] .data            PROGBITS        0x000000000001e520 0x0001d520 0x00000e60 0      WA     0     0    32
  [24] .bss             NOBITS          0x000000000001f380 0x0001e380 0x00001410 0      WA     0     0    32
  ...

Important sections: .text: machine code .data: initialized global/static variables .bss: uninitialized globals (no file space) .rodata: read‑only data such as string literals .symtab: symbol table (functions, variables) .strtab: string table for symbol names .dynamic: dynamic linking information

ELF lifecycle: source → object → executable → process

source (.c) --compile--> object (.o) --link--> executable --load--> process

Compilation (object file)

Compile a C source file:

$ gcc -c hello.c
# produces hello.o (ELF format, not yet executable)

The object contains relocation entries ("holes") that the linker will fill. View them with:

$ readelf -r hello.o
Relocation section '.rela.text' at offset 0x2d0 contains 2 entries:
  Offset   Info   Type          Sym. Value  Sym. Name + Addend
  0x13     0xa0000004  R_X86_64_PLT32   0x0   printf -4
  0x23     0xb0000004  R_X86_64_PLT32   0x0   exit   -4

Symbol table

$ readelf -s hello.o
Symbol table '.symtab' contains 12 entries:
  Num: Value          Size Type    Bind   Vis   Ndx Name
    0: 0x0            0    NOTYPE  LOCAL  DEFAULT UND 
    1: 0x0            0    FILE    LOCAL  DEFAULT ABS hello.c
    ...
    9: 0x0           41    FUNC    GLOBAL DEFAULT 1   main
   10: 0x0            0    NOTYPE  GLOBAL DEFAULT UND   printf
   11: 0x0            0    NOTYPE  GLOBAL DEFAULT UND   exit

Undefined symbols (UND) indicate references that the linker must resolve.

Linking

The linker merges object files, resolves undefined symbols, and produces an executable. It creates a global symbol table, finds definitions for UND symbols (e.g., printf in libc.so), and patches the code with the correct addresses.

Loading

When ./program is executed, the loader ( ld.so) performs:

Validate the ELF header.

Load segments described by the program header table into memory.

If the file is dynamically linked, locate and load required shared libraries.

Jump to the entry point address to start execution.

Observe the process with strace:

$ strace ./hello
execve("./hello", ["./hello"], 0x7ffcef8db490 /* 52 vars */) = 0
brk(NULL) = 0x55c84f34c000
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
...

Practical ELF toolbox

file

: identify file type. readelf: display ELF headers, sections, symbols, etc. objdump: disassemble code. nm: list symbols. ldd: show dynamic library dependencies. strings: extract printable strings. strip: remove symbols/debug info to shrink the binary. patchelf: modify ELF attributes such as the interpreter.

Dynamic linking details

The .dynamic section records needed shared libraries. List them with:

$ readelf -d /bin/ls | grep NEEDED
 0x0000000000000001 (NEEDED)              Shared library: [libselinux.so.1]
 0x0000000000000001 (NEEDED)              Shared library: [libc.so.6]

Show full dependency paths with ldd:

$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffc961cd000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f27f989e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f27f96b3000)
...

Library search order:

Directories in LD_LIBRARY_PATH.

RPATH/RUNPATH encoded in the executable.

Cache file /etc/ld.so.cache.

Default system directories such as /lib and /usr/lib.

ELF manipulation examples

Binary hardening

# Remove symbol table to make reverse‑engineering harder
$ strip --strip-all myprogram
$ ls -lh myprogram*
-rwxr-xr-x 1 user user 236K myprogram
-rwxr-xr-x 1 user user 176K myprogram.stripped

Patching interpreter

# Change the program interpreter without recompiling
$ patchelf --set-interpreter /opt/mylibs/ld-linux.so myprogram
$ readelf -l myprogram | grep interpreter
      [Requesting program interpreter: /opt/mylibs/ld-linux.so]

Using LD_PRELOAD to intercept functions

Example: a simple malloc tracer.

#include <stdio.h>
#define _GNU_SOURCE
#include <dlfcn.h>
static void* (*real_malloc)(size_t) = NULL;
void* malloc(size_t size) {
    if (real_malloc == NULL) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    void* ptr = real_malloc(size);
    fprintf(stderr, "malloc(%zu) = %p
", size, ptr);
    return ptr;
}
$ gcc -shared -fPIC memtrace.c -o libmemtrace.so -ldl
$ LD_PRELOAD=./libmemtrace.so ./my_program
malloc(100) = 0x55e930e2f6b0
malloc(200) = 0x55e930e2f720
...

This technique is useful for debugging, logging, or applying temporary hot‑fixes without modifying source code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

dynamic linkingelfToolingbinary analysisExecutable Format
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.