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 sitting on a disk. Those bytes may hold machine instructions, constant text, initial variable values, metadata meant for the loader, and information used only by development tools that will never run on the target. Linux needs to know which of those bytes belong in memory, what permissions each of them will need once they get there, and where execution ought to begin afterward.
On our Ubuntu VM that organization normally comes from ELF, the Executable and Linkable Format. ELF gets used for executable files, shared libraries, relocatable object files, and core dumps alike (one format, four quite different jobs). The header tells us which kind of ELF object we’re inspecting.
The first lesson connected an executable path to a running process. This time we’re going to stop before running anything, and read the map stored inside the file itself.
Numbers and addresses bridge
Section titled “Numbers and addresses bridge”ELF tools print a lot of values in hexadecimal, which is base 16. The digits carry on after 9 with a through f, and a leading 0x marks a hexadecimal number.
decimal 15 16 31 32 4096hex 0xf 0x10 0x1f 0x20 0x1000Hexadecimal is useful here because two hex digits represent exactly one byte, which makes byte boundaries visible at a glance. Do your address arithmetic as arithmetic, and not as string editing:
printf '0x%x\n' $((0x401000 + 0x2a))printf '%d\n' $((0x403000 - 0x401000))An address identifies a byte location, while an offset is a distance measured from some named beginning. Always label the base you’re measuring from, whether that’s a file offset, a virtual address, a module offset, or a plain distance.
Linux usually maps 4096-byte pages (0x1000) on x86-64, and you can confirm that on your own machine with getconf PAGESIZE. A page-aligned address leaves a remainder of zero after division by the page size.
Get the inspection tool
Section titled “Get the inspection tool”We’ll be using readelf, which is a tool built specifically to display ELF metadata. Check whether it’s installed:
readelf --versionIf the terminal reports readelf: command not found, install the Ubuntu package that provides it:
sudo apt updatesudo apt install binutilsThen run readelf --version again. binutils also supplies several tools we’ll want later, including objdump, nm, and strings.
The supported course environment reports GNU binutils 2.42.
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, and it may also report properties like 64-bit, dynamically linked, position-independent, or stripped (all of which matter later). Check your exact output rather than copying the description off this page, because your Ubuntu version may well package a different build.
Calculate a hash to identify the exact bytes you inspected:
sha256sum "$sleep_path"The course doesn’t supply an expected hash for a system executable, because an Ubuntu update can legitimately replace it at any point. Here the hash is an identifier for your evidence rather than a pass or fail answer.
Read the ELF header
Section titled “Read the ELF header”Display the main header:
readelf -h "$sleep_path"The output opens with an ELF header and a pile of fields. These are the ones worth focusing on:
| 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’ll probably see ELF64, little-endian data, and Advanced Micro Devices X86-64 for the machine. Treat your own output as authoritative for your own file.
Type does not mean file extension
Section titled “Type does not mean file extension”The Type field may well say DYN, even though this is a command you execute every day. Modern distributions usually build their executables as position-independent executables, known as PIEs, and ELF represents those using machinery closely related to shared objects, so that Linux is free to place them at a different virtual address on every run.
That’s what supports address-space layout randomization, and we’ll come back to it later. For now, don’t translate DYN into “this can’t be an executable”. Read it alongside the file’s program headers and its interpreter information instead.
The entry point is not necessarily main
Section titled “The entry point is not necessarily main”The executable’s entry point identifies its startup location. For a dynamically linked program, the dynamic loader runs first and later transfers control there. In a C program that location normally belongs to startup code, which prepares the process before it ever calls your main function. main is where the part C programmers usually write begins, and it is not the first instruction the processor executes.
That distinction matters the moment an inspection tool reports an entry point that doesn’t match the address associated with main.
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 the program headers, and -W stops wide values getting broken across lines.
A program header describes a segment, which means a region of the file or of the process image that has some job relevant to loading and execution. Find the entries whose type is LOAD, because those are precisely the regions Linux will map 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 |
The common flags are R for read, W for write, and E for execute. You’ll normally find separate loadable segments for combinations like read-only metadata, executable code, read-only constants, and writable data, each with its own permissions.
Read one LOAD row from left to right, taking the columns in order. Say it reports this:
LOAD 0x002000 0x0000000000402000 ... 0x0002d1 0x0002d1 R E 0x1000The file bytes begin at offset 0x2000, their virtual-address relationship begins at 0x402000, 0x2d1 bytes are file-backed, the permissions are readable and executable, and the values are page-aligned. So an instruction sitting 0x80 bytes into that segment has file offset 0x2080 and described virtual address 0x402080.
Why not just mark everything readable, writable, and executable and be done with it? Because permissions are creating boundaries here. Code normally has no business being writable, and ordinary data has no business being executable. Keeping those permissions apart limits what an accidental write or an attacker-controlled value is able to accomplish immediately, without any further work.
The readelf manual lists separate options for headers, sections, and symbols. Choose the view that answers your question before collecting more output.
File size and memory size can differ
Section titled “File size and memory size can differ”Some program data needs its memory initialized to zero, and it doesn’t need thousands of literal zero bytes taking up room in the executable file to get that. ELF can describe a segment whose MemSiz is larger than its FileSiz, and Linux then supplies zero-filled memory for the extra range while it loads the process.
This is one of the places where “the process is just the executable copied into memory” falls apart. Loading follows the metadata, and it creates runtime state that never existed 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]That interpreter is the dynamic loader used to prepare dynamically linked programs. It locates the required shared libraries, and it resolves relationships the executable can’t complete on its own.
Display the shared-library requirements recorded in the dynamic metadata:
readelf -dW "$sleep_path" | lessUse the arrow keys to move around, and press q to leave less. Find the entries labeled NEEDED, which name the shared libraries this executable requests. They don’t guarantee that any of those libraries is currently loaded into a process, since we are still only inspecting a file here.
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 you’re done. A section groups information by purpose, and the 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, and they are certainly 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 well contain several sections. At the bottom of the readelf -lW output, the Section to Segment mapping shows you which sections belong to which segment.
Executable loading is driven mainly by the program headers. A valid runtime image doesn’t require the loader to preserve the tidy section boundaries that make static analysis pleasant.
Symbols preserve useful names
Section titled “Symbols preserve useful names”A symbol associates a name with something, usually a function or a data object. Display whatever symbol tables are available:
readelf -sW "$sleep_path" | lessYou may see a lot of imported names and very few names for the executable’s own internal functions. The earlier file output may have described the executable as stripped. Stripping removes the symbol and debugging information that isn’t required for ordinary execution, which shrinks the file and takes a great many extremely convenient labels away with it.
Stripped doesn’t mean encrypted, or protected from analysis, or free of names. Dynamically linked programs keep whatever information is needed to resolve imported and exported symbols. The machine instructions are all still sitting there too, because the processor needs something to execute. We’ve lost a map the developers drew, and not the territory itself.
Press q to leave less.
Check your result
Section titled “Check your result”Find the entry point and a LOAD segment in your readelf output. The header gives the entry address; program headers describe loadable regions. Stripping symbols does not remove those instructions. If you mixed up sections and segments, compare readelf -S with readelf -l again.
Common failures and recovery
Section titled “Common failures and recovery”readelf: command not found: install the pinnedbinutilspackage, check its version withreadelf --version, and then rerun the exact command.- The target path changed after an update: resolve it again with the earlier
command -v, and calculate a fresh digest before you compare any output. - The ELF type is
DYNrather thanEXEC: use the value you observed. A position-independent executable normally usesDYN, and you shouldn’t force your output to match somebody else’s build. - Your rows or addresses differ from the sample: compare the field meanings, the flags, and the relationships. Check your actual values rather than copying the example addresses.
lessappears stuck: pressqto return to the shell.
If the header and program-header views still seem interchangeable, compare the entry point in readelf -h with a LOAD row in readelf -l.
What this lets us investigate
Section titled “What this lets us investigate”You can now treat an ELF executable as a structured object, rather than as an opaque blob that happens to have permission to run. You can identify its target architecture and entry point, describe the regions Linux intends to load, connect permissions to those regions, tell sections apart from segments, and recognize when the useful developer symbols have been stripped out.
Next, Virtual Memory and Process Maps runs the executable again, and compares this on-disk map against the regions Linux actually created inside a live process.
Check your understanding
Section titled “Check your understanding”Knowledge check
Question 1 of 2
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.