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.
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--> processCompilation (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 -4Symbol 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 exitUndefined 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.strippedPatching 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
