ELF and Program Execution
Time: 45 min | Prerequisites: SSH Login, Exploring Linux | Theory companion: Linux Fundamentals, Sections 1.1–1.2
Learning Objectives
By the end of this tutorial you will be able to:
- Compile a C program on the Raspberry Pi and identify its file type
- Read ELF headers and sections with
readelfandobjdump - Explain the difference between static and dynamic linking
- Trace program loading with
strace - Cross-compile an ARM binary and run it on x86 with QEMU
- Diagnose common "Exec format error" and missing-library failures
ELF: How Linux Runs Your Program
Every executable on a Linux system is an ELF (Executable and Linkable Format) file. When you type ./my_program, the kernel reads the ELF header to determine the target architecture, entry point, and which shared libraries are needed. The dynamic linker (ld-linux) then maps the program and its libraries into memory before jumping to _start (which eventually calls main).
ELF files are divided into sections (.text for code, .data for initialized globals, .rodata for constants, .bss for zero-initialized data) that the loader maps into memory segments with appropriate permissions (read, write, execute). This section-to-segment mapping is what the MMU enforces at runtime.
Dynamic linking means most programs share a single copy of libc.so in RAM, saving memory on resource-constrained embedded systems. Static linking bakes all library code into the binary -- larger but self-contained, which is why BusyBox and rescue tools use it.
Understanding ELF internals helps you diagnose "Exec format error" (architecture mismatch), missing library failures, and unexpected binary sizes -- all common issues when cross-compiling for embedded targets.
For the full conceptual background on processes, ELF, and dynamic linking, see Linux Fundamentals, Sections 1.1--1.2.
The Example Program
Throughout this tutorial (and the Debugging Practices tutorial), we use a small C program that reads the CPU temperature from sysfs — a meaningful embedded task that uses libc functions (fopen, fgets, printf) so linking behavior is visible.
Create the file on your Pi:
// sensor_reader.c — Read CPU temperature from sysfs
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fp = fopen("/sys/class/thermal/thermal_zone0/temp", "r");
if (fp == NULL) {
perror("Failed to open thermal sensor");
return 1;
}
char buf[16];
if (fgets(buf, sizeof(buf), fp) == NULL) {
perror("Failed to read temperature");
fclose(fp);
return 1;
}
fclose(fp);
int raw = atoi(buf);
printf("CPU temperature: %d.%d °C\n", raw / 1000, (raw % 1000) / 100);
return 0;
}
1. Compile and Run
# Compile
gcc -o sensor_reader sensor_reader.c
# Run
./sensor_reader
# Expected: CPU temperature: 45.2 °C (or similar)
# What kind of file did we create?
file sensor_reader
The file command tells you:
- ELF — it is an Executable and Linkable Format binary
- 32-bit / 64-bit — matches your Pi's architecture (ARM aarch64 on Pi 4 with 64-bit OS, or ARM 32-bit)
- dynamically linked — it depends on shared libraries at runtime
- interpreter — the dynamic linker path (e.g.,
/lib/ld-linux-armhf.so.3)
Checkpoint 1
file sensor_reader shows an ELF executable for ARM, dynamically linked. The program prints the CPU temperature.
2. ELF Structure
2.1 ELF Header
Find these fields in the output:
| Field | What It Tells You |
|---|---|
| Magic | 7f 45 4c 46 — the four bytes that identify every ELF file |
| Class | ELF32 or ELF64 — word size |
| Machine | ARM (or AArch64) — target architecture |
| Entry point address | Where execution begins (_start, not main) |
| Type | EXEC (fixed address) or DYN (position-independent) |
2.2 Sections
Fill in this table from the output:
| Section | Size | Purpose |
|---|---|---|
.text |
Machine code | |
.data |
Initialized globals | |
.bss |
Zero-initialized globals | |
.rodata |
String constants, read-only data | |
.dynamic |
Dynamic linking info |
2.3 Find Your Strings
You should see "CPU temperature: %d.%d °C\n" and the sysfs path in the output.
2.4 See the Assembly
Even without knowing ARM assembly, you can spot:
blinstructions — function calls (tofopen,printf, etc.)- Labels like
<fopen@plt>— calls through the Procedure Linkage Table (dynamic linking)
Checkpoint 2
You can read ELF headers, identify sections and their sizes, find string constants in .rodata, and see function calls in the disassembly.
3. Dynamic Linking
3.1 List Shared Libraries
Typical output:
Our program uses fopen, printf, atoi — all from libc. The dynamic linker (ld-linux) loads libc at runtime.
3.2 Runtime Memory Layout
# Start the program in the background and inspect its memory map
./sensor_reader &
READER_PID=$!
cat /proc/$READER_PID/maps
kill $READER_PID
Tip
The memory map shows where the ELF segments, shared libraries, stack, and heap are loaded. Each has different permissions (r/w/x). This is the MMU in action — Section 9 of Theory 2.
3.3 Static vs Dynamic
# Compile a statically linked version — all library code is embedded
gcc -static -o sensor_reader_static sensor_reader.c
# Compare sizes
ls -lh sensor_reader sensor_reader_static
# Compare library dependencies
ldd sensor_reader
ldd sensor_reader_static
# "not a dynamic executable"
# Both produce the same output
./sensor_reader_static
| Dynamic | Static | |
|---|---|---|
| Binary size | ~16 KB | ~600+ KB |
| Shared libraries needed | Yes (libc) | None |
| Startup time | Slightly slower (linker runs) | Faster (no linking step) |
| Disk usage with many programs | Efficient (libc shared) | Wasteful (each binary has its own copy) |
| Use case | Normal Linux systems | Buildroot/BusyBox, rescue tools |
Checkpoint 3
You can list shared library dependencies with ldd, inspect runtime memory maps, and compare static vs dynamic linking.
4. Tracing Program Loading
strace intercepts system calls — the interface between your program and the kernel.
# Trace the dynamic version — watch the kernel load libraries
strace -e trace=execve,openat,mmap ./sensor_reader 2>&1 | head -40
You will see:
execve("./sensor_reader", ...)— the kernel starts loading the ELF- Multiple
openatcalls — opening shared libraries (libc.so.6) - Multiple
mmapcalls — mapping library code and data into memory - Eventually, your program runs and reads the temperature
# Now trace the static version
strace -e trace=execve,openat,mmap ./sensor_reader_static 2>&1 | head -20
The static version has far fewer system calls — no library loading.
# Count total system calls for each version
strace -c ./sensor_reader 2>&1 | tail -5
strace -c ./sensor_reader_static 2>&1 | tail -5
Checkpoint 4
You can trace program loading with strace and explain why the dynamic version makes more system calls than the static version.
5. QEMU: Run ARM Binaries on Your Laptop
Note
This section runs on your host laptop (x86/amd64), not on the Pi. You need gcc-arm-linux-gnueabihf and qemu-user installed.
5.1 Cross-Compile
# Compile for ARM on your x86 laptop
arm-linux-gnueabihf-gcc -static -o sensor_reader_arm sensor_reader.c
file sensor_reader_arm
# ELF 32-bit LSB executable, ARM, ...
5.2 Run with QEMU User-Mode
Warning
The sysfs path /sys/class/thermal/thermal_zone0/temp may not exist on your laptop (different hardware). The program will print an error — that is expected and demonstrates that QEMU emulates the CPU, not the hardware.
5.3 Debug with QEMU + GDB
# Terminal 1: Start under QEMU, paused, waiting for GDB on port 1234
qemu-arm -g 1234 ./sensor_reader_arm
# Terminal 2: Attach GDB
gdb-multiarch ./sensor_reader_arm
(gdb) target remote :1234
(gdb) break main
(gdb) continue
(gdb) next
(gdb) print buf
(gdb) quit
This gives you full source-level debugging of ARM code on your x86 laptop — no Pi needed.
Checkpoint 5
You can cross-compile for ARM, run the binary on x86 with QEMU, and attach GDB to debug it.
6. Diagnosing Common Problems
6.1 Exec Format Error
# Try running an ARM binary directly on x86 (without QEMU)
# This simulates what happens when you copy a binary to the wrong platform
file sensor_reader_arm
# ELF 32-bit LSB executable, ARM, ...
# If run on x86 without binfmt/QEMU:
# bash: ./sensor_reader_arm: cannot execute binary file: Exec format error
Cause: The kernel checks the ELF header and finds an architecture mismatch. Fix: Use QEMU, or compile for the correct target.
6.2 Missing Shared Library
# Simulate a missing library path
LD_LIBRARY_PATH=/nonexistent ./sensor_reader
# error while loading shared libraries: libc.so.6: cannot open shared object file
Cause: The dynamic linker cannot find a required .so file.
Fix: Install the library, fix LD_LIBRARY_PATH, or use ldd to identify what is missing.
6.3 Wrong Interpreter
# Check the interpreter (dynamic linker) path
readelf -l sensor_reader | grep interpreter
# [Requesting program interpreter: /lib/ld-linux-armhf.so.3]
If this path does not exist on the target system (e.g., different libc version), the binary will fail to start. This is common when copying binaries between different distributions.
Checkpoint 6
You can explain and diagnose "Exec format error", missing library, and wrong interpreter failures.
Summary
| Tool | What It Shows | Command |
|---|---|---|
file |
File type, architecture, linking | file ./binary |
readelf -h |
ELF header (arch, entry point) | readelf -h ./binary |
readelf -S |
Section table (.text, .data, .bss) |
readelf -S ./binary |
readelf -d |
Dynamic linking info | readelf -d ./binary |
ldd |
Shared library dependencies | ldd ./binary |
objdump -d |
Disassembly | objdump -d ./binary |
strace |
System call trace | strace ./binary |
/proc/PID/maps |
Runtime memory layout | cat /proc/$$/maps |
qemu-arm |
Run ARM binary on x86 | qemu-arm ./arm_binary |
Challenge
Write a script elf_report.sh that takes a binary as an argument and prints:
- File type and architecture (
file) - Entry point address (
readelf -h) - Number of sections (
readelf -S | grep -c '^\s*\[') - Whether it is statically or dynamically linked (
ldd) - List of shared libraries (if dynamic)
- Size of
.textsection (readelf -S)
chmod +x elf_report.sh
./elf_report.sh /usr/bin/ls
./elf_report.sh sensor_reader
./elf_report.sh sensor_reader_static
Deliverable
Upload to your lab journal:
- Filled-in ELF sections table from Section 2.2
- Output of
lddfor both dynamic and static versions strace -ccomparison (system call counts)elf_report.shscript
Next Steps
- Debugging Practices — use GDB, gdbserver, and QEMU to debug the same
sensor_readerprogram - Linux Fundamentals — full theory on processes, files, and debugging