Reading an ELF File
Linux needs a map, not a pile of bytes
Section titled “Linux needs a map, not a pile of bytes”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.
Get the inspection tool
Section titled “Get the inspection tool”We will use readelf, a tool designed to display ELF metadata. Check whether it is installed:
readelf --versionIf the terminal reports readelf: command not found, install the Ubuntu package that provides it:
sudo apt updatesudo apt install binutilsThen repeat readelf --version. binutils also provides tools we will use later, including objdump, nm, and strings.
Select and verify the target
Section titled “Select and verify the target”Use the same sleep executable from the previous lesson:
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:
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.
Read the ELF header
Section titled “Read the ELF header”Display the main header:
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.
Type does not mean file extension
Section titled “Type does not mean file extension”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 is not necessarily main
Section titled “The entry point is not necessarily main”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
Knowledge check complete
You answered all 3 questions correctly.
Program headers describe the loadable view
Section titled “Program headers describe the loadable view”Display the program header table:
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.
File size and memory size can differ
Section titled “File size and memory size can differ”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.
The interpreter starts dynamic linking
Section titled “The interpreter starts dynamic linking”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:
readelf -dW "$sleep_path" | lessUse 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:
readelf -SW "$sleep_path" | lessPress 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
Knowledge check complete
You answered all 3 questions correctly.
Symbols preserve useful names
Section titled “Symbols preserve useful names”A symbol associates a name with something such as a function or data object. Display the available symbol tables:
readelf -sW "$sleep_path" | lessYou 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.
Write an ELF identity record
Section titled “Write an ELF identity record”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.
What this gives us as researchers
Section titled “What this gives us as researchers”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.