ELF and eBPF
Published on: March 21, 2025

You must have written eBPF programs in C before, but I am sure very few people are aware of these small, but crucial details. Before an eBPF program can run in the Linux kernel, it needs to be loaded from user space. The ELF (Executable and Linkable Format) plays a key role in this process. eBPF programs are stored in ELF files, allowing the kernel and user-space tools to parse and extract bytecode (compiled eBPF code), maps, and other metadata efficiently.
You must have come across things like SEC("tracepoint/syscalls/sys_enter_write")
but do you know why we even need to put things in certain sections? There is a
little description on how to use them in the official documentation,
but it does not explain things in-depth. It comes down to understanding ELF
files, and how does the Operating system runs a binary using them.
In this article, we’ll explore what are ELF files, how they work and how to get better with understanding them. In the next article, we will tie this knowledge to specific use-cases of eBPF.
There are a few things when it comes to working with eBPF:
-
Custom Sections in eBPF:
eBPF
programs use ELF files with special sections to organise different parts of the program.SEC(".maps")
: DefineseBPF
maps section, which are like shared storage spaces between the kernel and the user-space code. If the kernel needs access to a map, it knows which section to look into.- Tools like
bpftool
orlibbpf
read these sections to load the program and set up maps correctly.
-
Specifying
Tracepoints
and System Calls withSEC
: IneBPF
, theSEC
macro isn't just for generic programs, it lets you hook into specific kernel events liketracepoints
orsystem calls
. For example:SEC("tp/syscalls/sys_enter_write")
: This attaches youreBPF
program to thesys_enter_write
tracepoint
. Which triggers every time thewrite
system call is invoked.
As you can see, sections in ELF are a pretty big deal already when it comes to
working with eBPF
. So, If you have questions like "Why do we need sections",
"what are ELF files" etc. in your mind, this article dives deep into answering
those questions. In fact, it is the only article you need to wrap your head
around these concepts.
What is ELF
The Executable and Linkable Format(ELF)
is a standard file format used in Unix
like operating systems, such as Linux, to store
- executables (programs you can run)
- object files (compiled code)
- shared files (reusable code shared by programs)
We are talking about times when the hardware was not so readily available as it it today, you could not load the entire program in memory all at once to compile and execute it. You had to compile them separately and link them together while actually executing.
Think of an ELF program file as a neatly organized box that holds everything a program needs i.e. code, data and the instructions for how to put it all together.
Imagine you’re baking a cake from a recipe. The ELF file is like a cookbook: it has the ingredients (data), the steps (code), and notes on how to assemble it (metadata). This structure helps the computer turn your code into a running program, whether it’s a simple "Hello, World!" or a complex application.
ELF Sections
Sections in ELF file are like labeled containers that organise different types of data in a program. Each section has a specific job, making it easier for tools like compilers, linkers, and debuggers to find and use what they need.
Here are some common sections:
.text
: The machine code (instructions) your program executes. For example, this is where the steps to print "Hello world!" live..data
: Initialised global or static variables. Example:int x = 10;
goes here because it has a starting value..bss
: Uninitialised global or static variables. Example:int y;
ends up here and gets set to zero (default null) when the program starts..rodata
: Read-only data, like string literals. Example:"Hello World!"
is stored here since it doesn't change..symtab
: A symbol table listing functions and variables with their addresses. Think of it as a directory for findingmain
andprinf
..rela.text
: Relocation entries for the.text
section. These are like "fix me later" notes for addresses the compiler doesn't know yet..strtab
: A string table with names of symbols, such as"main"
or"printf
, so tools can read them as text.
Why do we need them
Sections aren't just random, they're essential for making programs work smoothly:
- Organization: Sections group related data (e.g., code in
.text
or variables in.data
), so tools know where to look. - Efficiency: Each section has attributes, like "executable" for
.text
or "writable" for.data
. This tells the operating system how to load them into memory with the right permissions. - Flexibility: When linking multiple files, sections can be merged or
rearranged. For example,
.text
sections from different.o
files combine into one big.text
in the final program.
Example: When you use a debugger like gdb
, it reads .symtab
to show you
function names and line numbers, linking the machine code back to your original
code.
Different types of ELF files
The ELF header’s type field tells us what the file is for. Each type plays a unique role in turning code into a running program. Here are the main types with examples:
- REL (Relocatable File, value 1):
- What: An object file (
.o
) from the compiler, not ready to run yet. - Use: Contains code and data with relocations "to be filled", waiting to be linked.
- Example:
gcc -c myfile.c -o myfile.o
creates aREL
file with your compiled code.
- What: An object file (
- EXEC (Executable File, value 2):
- What: A fully linked program you can run directly.
- Use: Ready to be loaded into the memory and executed.
- Example:
gcc myfile.c -o myprogram
creates anEXEC
file you can run with./myprogram
.
- DYN (Shared Object File, value 3):
- What: A shared library (
.so
) for dynamic linking. - Use: Provides reusable code that programs load at runtime. Multiple programs can share it, saving space..
- Example:
libc.so
(the C standard library) is aDYN
file used by many programs for functions likeprintf
.
- What: A shared library (
- CORE (value 4):
- What: A core dump file from a crashed program.
- Use: Captures the program’s state (memory, variables) when it crashes, helping you debug.
- Example: If
myprogram
crashes, the kernel might save acore
file for analysis.
Why Different Types?
Relocations are placeholders in an ELF file that mark where addresses need to be filled in later. They’re crucial because the compiler doesn’t always know where things (like external functions or variables) will end up in memory when it first compiles your code.
How They Work
Say your program calls printf
from the C library (libc
). In the object
file (.o
):
- The
.text
section has a placeholder forprintf
’s address (e.g., a temporary0x0
). - The
.rela.text
section adds a note: "At this spot, insertprintf
’s real address." - During linking:
- Static linking: The linker fills in
printf
’s address from a library included in the executable. - Dynamic linking: The address stays unresolved until runtime, when the dynamic
linker (
ld.so
) findsprintf
inlibc.so
.
- Static linking: The linker fills in
Example: If you write fprintf(stderr, "Error");
, the compiler leaves a
relocation entry for fprintf
. The linker (or dynamic linker) later connects it
to the real fprintf
in libc
.
Relocations let you build programs from multiple files or libraries, even when their final locations aren’t known yet.
The Process: From Compilation to Execution
Here’s how a C program goes from source code to running in memory, focusing on what happens after compilation, this will help you understand the overall importance of ELF files:
-
Step 1: Compilation
-
What Happens: When you compile a
.c
file withgcc -c myfile.c -o myfile.o
). You get a.o
file, which is an ELF REL (relocatable) file. -
What’s Inside: Sections like
.text
(code),.data
(variables),.bss
, and.rela.text
(relocations). No segments yet, since it’s not executable. -
Purpose: This file has your code but with unresolved references (e.g., to
printf
) unresolved.
-
-
Step 2: Linking
-
What Happens: The linker (
ld
) combines one or more.o
files and libraries into an executable withgcc myfile.o -o myprogram
). -
Process:
- Combines sections (e.g., merges all
.text
sections into one). - Resolves relocations:
- Static linking: Fills in all addresses (e.g.,
printf
’s location in the executable). - Dynamic linking: Leaves some for runtime (e.g.,
printf
fromlibc.so
).
- Static linking: Fills in all addresses (e.g.,
- Adds segments (program headers) to tell the program how to load the file into the memory.
- Combines sections (e.g., merges all
Output: An ELF EXEC file (myprogram
) with sections and segments.
-
Step 3: Loading and Execution
-
What Happens : When you run
./myprogram
, the OS loads it into memory. -
Loading:
- The OS reads the ELF file’s segments ( from program headers).
- For each
LOAD
segment:- Allocates memory with the right permissions (e.g., executable for
.text
). - Copies data from the file.
- Zeroes out
.bss
(since it’s uninitialized and takes no file space).
- Allocates memory with the right permissions (e.g., executable for
- If dynamically linked:
- The dynamic linker (
ld.so
) loads libraries (e.g.,libc.so
). - Resolves remaining relocations, updating addresses in memory.
- The dynamic linker (
-
Execution: The OS jumps to the program’s entry point (e.g.,
_start
), which callsmain
.
-
Runtime Use Case for Sections
Even after loading, sections can be useful:
- A debugger like
gdb
uses.symtab
and.debug_*
sections to show you variable values and source code lines while the program runs.
An ELF File and Inspecting it
Let's say we have this example C program:
#include <stdio.h>
int main(){
printf("Hello, World!\n");
return 0;
}
We will compile this in two stages:
gcc -c main.c -o main.o
: Creates a REL (relocatable) ELF file.gcc main.o -o main
: Links it into an EXEC (executable) ELF file.
Inspecting REL File
Running readelf -a main.o
reveals a few things:
ELF Headers
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 600 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
- ELF Header:
- Magic:
7f 45 4c 46 ...
(confirms it as an ELF file45
:E
,4c
:L
,46
:F
). - Type:
REL
(relocatable, not ready to run yet). - Machine:
x86_64
(or your system's architecture). - Entry Point: None (not applicable for REL files).
- Section Header Offset: Points to where sections are listed.
- Program Header Offset: 0 (no segments yet).
- Magic:
Section Headers
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000001a 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000198
0000000000000030 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 0000005a
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 0000005a
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 0000005a
000000000000000d 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000067
000000000000001c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000083
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.pr[...] NOTE 0000000000000000 00000088
0000000000000030 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 000000b8
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 000001c8
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 000000f0
0000000000000090 0000000000000018 12 4 8
[12] .strtab STRTAB 0000000000000000 00000180
0000000000000012 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 000001e0
0000000000000074 0000000000000000 0 0 1
.text
: The machine code formain
, including a call toprintf
(placeholder address)..rela.text
: Relocation entries for unresolved references.- Example: “At offset 0x5, fix the call to
printf
.”
- Example: “At offset 0x5, fix the call to
.rodata
: Read-only data, like the string"Hello, World!\n"
..data
: Initialized global variables (none here)..bss
: Uninitialized global variables (none here)..symtab
: Symbol table.main
: Defined, local, in.text
.printf
: Undefined (needs linking).
.strtab
: String table for symbol names.
Inspecting EXEC File
ELF Headers
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: 13496 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 14
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
Headers are similar, except there is a starting point along with file type being executable.
Section Headers
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.pr[...] NOTE 0000000000000350 00000350
0000000000000040 0000000000000000 A 0 0 8
[ 2] .note.gnu.bu[...] NOTE 0000000000000390 00000390
0000000000000024 0000000000000000 A 0 0 4
[ 3] .interp PROGBITS 00000000000003b4 000003b4
000000000000001c 0000000000000000 A 0 0 1
[ 4] .gnu.hash GNU_HASH 00000000000003d0 000003d0
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000000003f0 000003f0
00000000000000a8 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000000498 00000498
000000000000008d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000000526 00000526
000000000000000e 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000000538 00000538
0000000000000030 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000000568 00000568
00000000000000c0 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000000628 00000628
0000000000000018 0000000000000018 AI 5 23 8
[11] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000001040 00001040
0000000000000113 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000001154 00001154
000000000000000d 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000002000 00002000
0000000000000011 0000000000000000 A 0 0 4
[16] .eh_frame_hdr PROGBITS 0000000000002014 00002014
0000000000000024 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000002038 00002038
000000000000007c 0000000000000000 A 0 0 8
[18] .note.ABI-tag NOTE 00000000000020b4 000020b4
0000000000000020 0000000000000000 A 0 0 4
[19] .init_array INIT_ARRAY 0000000000003dd0 00002dd0
0000000000000008 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000003dd8 00002dd8
0000000000000008 0000000000000008 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000003de0 00002de0
00000000000001e0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000003fc0 00002fc0
0000000000000028 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000003fe8 00002fe8
0000000000000020 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000004008 00003008
0000000000000010 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000004018 00003018
0000000000000008 0000000000000000 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 00003018
000000000000001b 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00003038
0000000000000240 0000000000000018 28 6 8
[28] .strtab STRTAB 0000000000000000 00003278
0000000000000126 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 0000339e
0000000000000116 0000000000000000 0 0 1
-
Sections
.text
: Now includesmain
plus startup code fromlibc
..rodata
: Still has"Hello, World!\n"
..plt
: Procedure Linkage Table for dynamic calls (e.g., toprintf
)..got
: Global Offset Table for dynamic addresses..data
,.bss
: As before, plus any from linked libraries..dynamic
: Dynamic linking info (e.g., needed libraries likelibc.so
)..symtab
: Symbols, though often stripped in release builds.
To read in depth description of each section, read this.
In short, ELF is the backbone of how eBPF
programs are packaged and loaded.
Understanding it makes you better at writing, debugging, and deploying eBPF
code.