Skip to content
UnderDunn Courses

Reading an ELF File

How Programs WorkLesson 2 of 760 minutesLab included

An executable file is bytes on disk. Those bytes may contain machine instructions, constant text, initial variable values, metadata for the loader, and information used by development tools. Linux needs to know which bytes belong in memory, what permissions they require, and where execution should begin.

On our Ubuntu VM, that organization is normally provided by ELF, the Executable and Linkable Format. ELF is used for executable files, shared libraries, relocatable object files, and core dumps. The same container format serves several jobs, so the header tells us which kind of ELF object we are holding.

The first lesson connected an executable path to a running process. This time we will stop before running it and read the map stored inside the file.

We will use readelf, a tool designed to display ELF metadata. Check whether it is installed:

Terminal window
readelf --version

If the terminal reports readelf: command not found, install the Ubuntu package that provides it:

Terminal window
sudo apt update
sudo apt install binutils

Then repeat readelf --version. binutils also provides tools we will use later, including objdump, nm, and strings.

Use the same sleep executable from the previous lesson:

Terminal window
sleep_path="$(command -v sleep)"
printf 'Target: %s\n' "$sleep_path"
file "$sleep_path"

file should identify an ELF object. It may also report properties such as 64-bit, dynamically linked, position-independent, or stripped. Record the exact output rather than copying the description from this page. Your Ubuntu version may package a different build.

Calculate a hash so your notebook identifies the exact bytes you inspected:

Terminal window
sha256sum "$sleep_path"

The course does not provide an expected hash for a system executable because Ubuntu updates can legitimately replace it. Here the hash is an identifier for your evidence, not a pass/fail answer.

Display the main header:

Terminal window
readelf -h "$sleep_path"

The output begins with an ELF header and several fields. Focus on these:

Field Question it answers
Class Does this object use the 32-bit or 64-bit ELF layout?
Data How are multi-byte values ordered?
Type What kind of ELF object is this?
Machine Which processor architecture is it built for?
Entry point address Where should initial execution begin after loading?
Start of program headers Where is the loader-oriented table?
Start of section headers Where is the tool-oriented section table?

On a typical x86-64 Ubuntu VM, you will probably see ELF64, little-endian data, and Advanced Micro Devices X86-64 as the machine. Treat your output as authoritative for your file.

The Type field may say DYN even though this is a command you execute. Modern distributions commonly build executables as position-independent executables, or PIEs. ELF represents them using machinery closely related to shared objects so Linux can place them at different virtual addresses.

This supports address-space layout randomization, which we will examine later. For now, do not translate DYN into “this cannot be an executable.” Read it alongside the file’s program headers and interpreter information.

The entry point address tells the loader where execution begins. In a C program, that location normally belongs to startup code that prepares the process before calling your main function. main is the beginning of the part C programmers usually write, not the first instruction the processor executes.

That distinction will matter when a debugger stops at the actual entry point and the source code you remember is nowhere nearby.

Knowledge check

Question 1 of 3

What does the ELF Machine field identify?

Program headers describe the loadable view

Section titled “Program headers describe the loadable view”

Display the program header table:

Terminal window
readelf -lW "$sleep_path"

-l requests program headers. -W prevents wide values from being broken across lines.

A program header describes a segment: a region of the file or process image with a job relevant to loading and execution. Find entries whose type is LOAD. These are regions Linux maps into the process’s virtual address space.

Important columns include:

Column Meaning
Offset Where the segment’s bytes begin in the file
VirtAddr The segment’s intended virtual-address relationship
FileSiz How many bytes are represented in the file
MemSiz How much memory the segment occupies after loading
Flg Whether the loaded region is readable, writable, or executable
Align The alignment expected for the mapping

Common flags are R for read, W for write, and E for execute. You will normally find separate loadable segments for combinations such as read-only metadata, executable code, read-only constants, and writable data.

Why not mark everything readable, writable, and executable? Because permissions create boundaries. Code normally does not need to be writable, and ordinary data normally does not need to be executable. Keeping those permissions separate reduces what an accidental write or attacker-controlled value can immediately accomplish.

Some program data needs memory initialized to zero but does not need thousands of literal zero bytes stored in the executable. ELF can describe a segment whose MemSiz is larger than its FileSiz. Linux supplies zero-filled memory for the additional range while loading the process.

This is one place where “the process is the executable copied into memory” fails. Loading follows metadata and creates runtime state that is not present as an identical range of file bytes.

Look for an INTERP program header and a line resembling:

[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

This interpreter is the dynamic loader used to prepare dynamically linked programs. It locates required shared libraries and resolves relationships the executable cannot complete alone.

Display the shared-library requirements recorded in the dynamic metadata:

Terminal window
readelf -dW "$sleep_path" | less

Use the arrow keys to move and press q to leave less. Find entries labeled NEEDED. They name shared libraries requested by the executable; they do not guarantee that every library is currently loaded into a process, because we are still inspecting a file.

Sections describe the linker’s and tools’ view

Section titled “Sections describe the linker’s and tools’ view”

Display the section header table:

Terminal window
readelf -SW "$sleep_path" | less

Press q when finished. A section groups information by purpose. Common names include:

Section Typical purpose
.text Machine instructions
.rodata Read-only constants
.data Initialized writable data
.bss Zero-initialized or uninitialized writable data
.dynsym Symbols needed for dynamic linking
.dynstr Strings used by dynamic-linking metadata

Sections and segments are related, but they are not synonyms.

  • Segments describe what the loader maps and with which permissions.
  • Sections organize content for linking, relocation, and analysis tools.

One loadable segment may contain several sections. At the bottom of readelf -lW output, the Section to Segment mapping shows which sections belong to each segment.

Executable loading is driven primarily by program headers. A valid runtime image does not require the loader to preserve the clean section boundaries that make static analysis pleasant.

Knowledge check

Question 1 of 3

Which ELF table primarily tells Linux what regions to map for execution?

A symbol associates a name with something such as a function or data object. Display the available symbol tables:

Terminal window
readelf -sW "$sleep_path" | less

You may see many imported names but few names for the executable’s internal functions. The earlier file output may have called the executable stripped. Stripping removes symbol and debugging information that is not required for ordinary execution, reducing file size and removing many convenient labels.

Stripped does not mean encrypted, protected from analysis, or free of names. Dynamically linked programs retain information needed to resolve imported and exported symbols. The machine instructions also remain because the processor needs something to execute. We have lost a map drawn by the developers, not the territory.

Press q to leave less.

Add the following record to your research notebook:

Property Your observed value Command that established it
Path
SHA-256
ELF class and byte order
ELF type
Machine
Entry point
Number of LOAD segments
Executable segment flags
Writable segment flags
Requested interpreter
One required shared library
Stripped or not stripped

This record is more useful than pasting the complete output without comment. It states the conclusions we care about and preserves the commands needed to reproduce them.

You can now treat an ELF executable as a structured object rather than an opaque blob with permission to run. You can identify its target architecture and entry point, describe the regions Linux intends to load, connect permissions to those regions, distinguish sections from segments, and recognize when useful developer symbols have been removed.

Next, Virtual Memory and Process Maps will run the executable again and compare this on-disk map with the regions Linux created inside a live process.