
ELF Files Explained – Part 1: History and Fundamentals
Part 1 of the “ELF Files Explained” Series
Every software developer has encountered executable files, yet relatively few stop to consider the file format that makes them possible. Whether you are developing applications for Linux, writing firmware for an embedded microcontroller, or debugging software using GCC-based tools, there is a good chance an ELF file is being created somewhere in the build process.
Despite its widespread use, the Executable and Linkable Format (ELF) is often treated as a black box. Most developers know that the compiler produces one, the debugger loads one, and the embedded programmer may ultimately generate a HEX file from one — but the format itself remains largely invisible. Understanding what an ELF file is, and why it was designed the way it was, provides valuable insight into how modern software development tools work together.
This first article looks at the origins of executable file formats, tracing the evolution from early UNIX formats such as a.out and COFF to the development of ELF. We will also see how ELF grew beyond its UNIX roots to become the standard executable format for Linux and an essential component of modern embedded development toolchains.
Introduction
Whether you are developing desktop software, embedded firmware, shared libraries or operating system components, one file format quietly sits at the centre of the build process: the Executable and Linkable Format, more commonly known as ELF.
For many developers, ELF files are simply another build artefact that appears alongside object files, map files and executable binaries. Embedded developers working with toolchains such as AVR-GCC may recognise the generated .elf file but quickly move on to programming the corresponding Intel HEX file into a microcontroller. Linux developers may run an application every day without ever considering how the operating system interprets the file behind the scenes.
Despite this familiarity, relatively few engineers fully understand what an ELF file actually contains, or why it exists.
The ELF format is considerably more than a container for executable code. It provides a standardised way of representing compiled programs, object modules, shared libraries and debugging information, and it enables compilers, linkers, debuggers and operating systems to communicate using a common language. That shared language is what allows sophisticated software development environments to function reliably across an enormous range of hardware platforms.
Perhaps more remarkably, the format has remained relevant for over three decades. Processors, operating systems and development environments have all evolved dramatically since ELF was introduced, yet its core design has proven flexible enough to keep pace. It continues to underpin modern Linux systems, Android devices, networking equipment, industrial controllers and countless embedded applications.
This article is the first in a four-part series exploring ELF files from both a software engineering and an embedded systems perspective. In this instalment, we examine why executable file formats became necessary, trace the historical evolution that led to ELF, and introduce the role ELF plays within today’s software development toolchains. Later articles explore the internal structure of the ELF format, examine how AVR ELF Production Files are generated by the AVR-GCC toolchain, and finally show how those production files are used to program AVR microcontrollers.
Why Do Executable File Formats Exist?
At first glance, the need for an executable file format may seem obvious. A processor ultimately executes machine instructions, so why not simply write those instructions directly to disk?
In reality, the process is considerably more complicated.
Modern software is rarely developed as a single source file. Even a modest application may consist of hundreds or thousands of source files, written in multiple languages and drawing on numerous external libraries. Before a processor can execute the finished program, all of these components must be translated into machine code, combined into a coherent whole, and organised into a structure that both the operating system and the development tools can understand.
An executable file format provides that structure. Rather than storing only machine instructions, it also records how those instructions should be loaded into memory, where variables should reside, how different modules are connected together, and — in many cases — additional information to assist debugging and analysis.
This separation between executable code and descriptive metadata brings several advantages:
- Standardised loading by the operating system
- Support for modular software development
- Efficient linking of independently compiled source files
- Reliable debugging using symbolic information
- Portability across different hardware architectures
- Compatibility between development tools from different vendors
Without a standardised executable format, every compiler, linker and debugger would need to implement its own incompatible representation of compiled software. Building complex software systems would become significantly harder, and interoperability between tools would suffer accordingly. Executable file formats act as a common contract between every stage of the software development process.
Early UNIX Executable Formats
To understand why ELF was developed, it helps to look at the formats that preceded it.
The earliest versions of UNIX appeared in the early 1970s, when computers possessed only a fraction of today’s processing power and memory. Programs were small, operating systems were comparatively simple, and executable formats reflected those limitations.
One of the earliest and most influential formats was known simply as a.out, short for assembler output. Despite its modest name, a.out became the standard executable format across many UNIX systems for well over a decade. The format was intentionally straightforward: a small header followed by regions representing executable instructions, initialised data and uninitialised variables. This simplicity made it efficient for the hardware of the time and easy for operating systems to load into memory.
As UNIX grew in popularity, a.out became deeply embedded within software development workflows. Compilers generated it, linkers combined it, and operating systems executed it, serving the needs of both academic institutions and commercial UNIX vendors for many years.
But computing was changing rapidly. Software projects were becoming larger, processors more sophisticated, and operating systems increasingly needed capabilities that a.out had never been designed to support.

Figure 1. Evolution of UNIX Executable Formats.
The progression from the original UNIX a.out format through COFF to the modern Executable and Linkable Format (ELF), which today underpins Linux, BSD systems and many embedded GCC toolchains.
The Limitations of a.out and COFF
Although a.out served UNIX well throughout its early years, its design eventually became restrictive.
Its principal limitation was inflexibility. The format assumed a fixed organisation of executable code and data, leaving little room for expansion as software systems became more sophisticated. Supporting new processor architectures or adding metadata often required incompatible extensions, and different UNIX implementations gradually fragmented as a result.
As software development matured, developers increasingly needed richer information within executable files. Symbol tables became more important for debugging. Dynamic linking gained popularity. Shared libraries reduced memory consumption by allowing multiple programs to reference the same code simultaneously. Development tools demanded more detailed information describing source files, functions and variables. Retrofitting these capabilities into a.out proved increasingly difficult.
UNIX System V addressed some of these shortcomings by introducing the Common Object File Format (COFF). COFF was a significant improvement: it introduced a more structured organisation, expanded symbol handling, and provided better support for relocation and linking. The format was influential, adopted by several commercial UNIX vendors, and went on to inspire formats used well beyond the UNIX ecosystem.
For a time, COFF appeared to be the solution developers needed. But software complexity continued to increase, and COFF’s own shortcomings became apparent. Different vendors introduced proprietary extensions to support their own processors and operating systems, gradually eroding portability. The specification fragmented, making it increasingly difficult for development tools to remain compatible across platforms. Its debugging information was also beginning to show its age — as integrated development environments became more sophisticated, developers needed richer symbolic information than the format could comfortably provide.
By the late 1980s, it was clear that another generation of executable format was needed — one designed from the outset to support portability, extensibility and future growth, rather than simply extending earlier designs.
The Birth of ELF
Recognising the limitations of existing executable formats, members of UNIX System Laboratories began developing a new specification in the late 1980s. Their objective was not simply to replace COFF, but to create a format capable of supporting future generations of processors, operating systems and development tools.
The result was the Executable and Linkable Format (ELF).
Unlike its predecessors, ELF was designed with flexibility as a primary goal. Rather than assuming a fixed internal organisation, it introduced clearly defined data structures that allowed new capabilities to be added without redesigning the specification from scratch — an extensibility that would prove to be one of ELF’s greatest strengths.
Portability was another key design goal. ELF deliberately separated machine-independent concepts from processor-specific details, allowing the same overall structure to be used across a wide range of architectures. Whether targeting x86 desktop computers, ARM processors, RISC-V devices or 8-bit AVR microcontrollers, the underlying organisation of an ELF file remains remarkably consistent.
ELF also distinguished between the needs of different stages of the software development process. Linkers, loaders and debuggers each require different information, and the format was designed to present that information in a structured, efficient manner. This clean separation of responsibilities greatly simplified toolchain development and contributed to the interoperability developers now take for granted.
Perhaps the greatest testament to the design is its longevity. More than thirty years after its introduction, ELF continues to serve as the standard executable format for Linux and many other UNIX-like operating systems. It is equally at home in desktop applications, cloud servers, networking equipment and embedded development environments. Every time an AVR project is built using AVR-GCC, or a Linux application is compiled with GCC or Clang, an ELF file is quietly produced as the central product of the compilation process.
The remainder of this series explores exactly what that file contains, how it is organised internally, and why it has become one of the most enduring standards in modern software engineering.
Adoption Across UNIX, Linux and Embedded Toolchains
Following its introduction, ELF was rapidly adopted by UNIX System V and soon became the preferred executable format for many UNIX-like operating systems. Its flexible design allowed hardware vendors and operating system developers to support new processor architectures without fundamentally redesigning their toolchains.
When Linux emerged in the early 1990s, it initially used the older a.out format before transitioning to ELF in the mid-1990s. The change brought improved dynamic linking, better debugging support and greater compatibility with modern compiler toolchains. ELF has remained the standard executable format for Linux ever since, and is now deeply embedded within the GNU software ecosystem.
Today, ELF spans a remarkable range of computing platforms — desktop Linux distributions, enterprise servers, networking equipment, Android’s native software stack, BSD operating systems and many real-time operating systems all rely on it in one form or another.
Less obviously, perhaps, ELF is also fundamental to embedded software development. Modern embedded toolchains, including AVR-GCC, ARM GCC, RISC-V GCC and many vendor-supplied development environments, all produce ELF files as the primary output of the build process. Embedded engineers often interact with HEX or BIN files when programming a device, but these are usually generated from the ELF file rather than directly from the compiler output.
As a result, the ELF file is typically the most complete representation of a firmware build. It contains not only the executable machine code but also symbolic information, debugging data and numerous items of metadata used by the wider development toolchain — a distinction that becomes increasingly relevant later in this series.
The Software Build Pipeline
Although developers often think of compilation as a single operation, building software is actually a pipeline consisting of several distinct stages.
The process begins with one or more source files written in a language such as C or C++. These human-readable files are translated by the compiler into machine code, producing a collection of object files. Each object file contains only a portion of the finished program — references to functions or variables defined elsewhere remain unresolved, which is precisely what allows each source file to be compiled independently.
The linker then combines the required object files together with any referenced libraries. During this stage it resolves symbol references, allocates memory locations and produces the final ELF file representing the complete application or firmware. On desktop operating systems such as Linux, this ELF file is normally executed directly by the operating system loader.
Embedded systems often take one further step. Programming tools generally require firmware in a device-specific format such as Intel HEX or raw binary, so utilities such as objcopy extract the relevant executable information from the ELF file and generate a programming file suitable for the target device.
Note: The HEX file is a derivative of the ELF file, not the primary build output. Once symbolic information, debugging data and other metadata have been stripped out during conversion, they cannot be recovered from the HEX file alone. For this reason, professional development teams typically archive the ELF file alongside any released firmware, ensuring that future debugging and analysis can always be performed using the complete build artefact.

Figure 2. Software Build Pipeline.
The typical software build process used by modern compiler toolchains. The linker produces an ELF file, which can subsequently be executed directly by an operating system or used as the basis for embedded firmware.
What Exactly Is an ELF File?
Despite its name, an ELF file is not necessarily an executable program. The name Executable and Linkable Format reflects its broader purpose as a general-purpose container used throughout the software build process. Depending on how it is created, an ELF file may represent an executable application, a partially linked object module, a shared library, or even a memory image captured after a program has crashed.
Rather than thinking of an ELF file as simply “a program”, it is more accurate to view it as a structured description of compiled software. Within the file is a carefully organised collection of information that may include:
- Machine instructions generated by the compiler
- Initialised program data
- Read-only constants
- Symbol tables
- Relocation information
- Debugging information
- Processor and operating system metadata
- Linking information
Different development tools make use of different parts of this information. A linker primarily uses relocation records and symbol information to combine multiple object files into a finished program. An operating system loader is mainly interested in determining which portions of the executable should be loaded into memory before execution begins. A debugger, by contrast, relies heavily on symbolic and debugging information to associate machine instructions with source code, variables and functions.
One of ELF’s greatest strengths is that all of this information can coexist within a single, well-defined file format, while allowing individual tools to ignore whatever is not relevant to their particular task. This separation of responsibilities has contributed significantly to the format’s longevity.
Types of ELF Files
The ELF specification defines several classes of file, each serving a different purpose within the software development process.
| ELF File Type | Typical Extension | Produced By | Purpose |
|---|---|---|---|
| Relocatable file | .o |
Compiler | Compiled output from a single source file, awaiting linking |
| Executable file | (none, or .elf) |
Linker | A complete program ready to be loaded and executed |
| Shared library | .so |
Linker | Code shared between multiple running processes |
| Core dump | (none) | Operating system | A snapshot of a crashed process’s memory, for post-mortem debugging |
Relocatable Files
Relocatable files are typically produced directly by the compiler and usually have the .o extension. Each object file contains the compiled output from a single source file, together with the information the linker needs to combine it with other object files. At this stage, many references between functions and variables remain unresolved, allowing modules to be compiled independently before being linked into a complete application.
Executable Files
Executable ELF files represent complete programs, ready to be loaded and executed by an operating system. Most Linux applications ultimately exist as executable ELF files, containing everything the operating system loader needs to prepare the process for execution.
In embedded systems, executable ELF files also represent the completed firmware image produced by the linker. Although the firmware may subsequently be converted into Intel HEX or raw binary form for programming, the ELF file remains the authoritative output of the build.
Shared Libraries
Shared libraries allow multiple applications to make use of common code without each application carrying its own private copy. Rather than embedding identical library routines into every executable, operating systems can load a shared library once and let multiple processes reference it simultaneously. Under Linux these commonly appear as .so (shared object) files, and are themselves valid ELF files.
Core Dumps
When a program terminates unexpectedly, many operating systems can save the contents of its memory into a special ELF file known as a core dump. Unlike an executable file, a core dump captures the state of a running process — memory contents, register values and other diagnostic information — that a developer can later analyse using a debugger to determine the cause of a crash. Most embedded developers will never encounter an ELF core dump, but it is an invaluable tool in desktop and server software development.

Figure 3. The Many Roles of an ELF File.
ELF serves multiple purposes throughout modern software development, acting as the common format for executable programs, object files, shared libraries, debugging information and embedded firmware.
ELF in Modern Embedded Development
Although ELF is most commonly associated with Linux and other UNIX-like operating systems, it is equally important within the embedded systems industry. Embedded development arguably demonstrates the flexibility of the format more clearly than desktop software, since the same underlying structure supports devices ranging from tiny 8-bit microcontrollers to powerful 64-bit application processors.
Modern embedded toolchains typically follow a similar workflow regardless of the target architecture: source code is compiled into object files, those files are linked into a final ELF file, and the resulting image is then converted into a format suitable for programming the target device. For many developers, this conversion step obscures the importance of the ELF file itself.
Consider a typical AVR project built with Atmel Studio. After a successful build, the output directory will usually contain several files, including an ELF file, a HEX file, and often additional listing or map files. Since the programming utility normally requests the HEX file, it is easy to assume the ELF file is simply an intermediate artefact with little practical value. In reality, the opposite is true.
The HEX file is a transport format whose sole purpose is to describe the data that should be programmed into non-volatile memory — little more than memory addresses and the bytes to be written. The ELF file, by contrast, contains the complete firmware image together with a wealth of additional information used throughout development: symbol tables, debugging information, relocation records, compiler metadata and more, depending on the build configuration.
Integrated development environments make extensive use of this information. When a debugger displays source code alongside executing instructions, shows the current value of variables, allows breakpoints to be placed on function names, or produces a meaningful call stack, it is drawing on data stored in the ELF file — not on anything present in the programmed firmware itself. This is why development environments often require access to both the programmed device and the original ELF file: the microcontroller contains only the executable machine code, while the debugger relies on the ELF file to relate that machine code back to the original source.
As embedded software becomes increasingly sophisticated — with modern firmware projects spanning hundreds of source files, multiple libraries and complex optimisation settings — this additional information becomes ever more valuable. Without the symbolic information preserved in the ELF file, debugging such systems would be considerably harder. Consequently, many organisations treat the ELF file as the definitive build artefact, generating the HEX or BIN file only as a deployment package for programming the target hardware.

Figure 4. ELF Across Modern Platforms.
Although closely associated with Linux, ELF has become the standard executable format across many operating systems and is widely used by modern embedded GCC toolchains, making it one of the most important file formats in contemporary software development.
Looking Ahead
Having explored the history of executable file formats and introduced the role of ELF within modern software development, an obvious question remains: what does an ELF file actually look like internally?
Although developers routinely generate ELF files every day, relatively few ever examine their contents. Behind the scenes, each ELF file contains a carefully organised collection of headers, tables and data structures that allow compilers, linkers, operating systems and debuggers to perform their respective roles.
Understanding this internal organisation provides valuable insight into how executable code is arranged, how variables are represented, how linkers combine independently compiled modules, and how debuggers associate machine instructions with source code. It also demystifies many of the command-line utilities supplied with modern toolchains — tools such as readelf, objdump, nm and size simply present different views of information already contained within the ELF file. For embedded developers in particular, this knowledge proves invaluable when investigating firmware size, memory usage, optimisation behaviour and debugging issues.
These topics form the focus of Part 2.
Summary
The Executable and Linkable Format is one of the most enduring standards in modern software engineering. Originally developed to overcome the limitations of earlier executable formats, it has grown into a flexible, extensible specification that supports a vast range of operating systems, processor architectures and development environments.
Rather than serving solely as an executable program, an ELF file acts as a comprehensive description of compiled software. It provides a common format through which compilers, linkers, loaders and debuggers exchange information, allowing complex software systems to be developed using interoperable tools. Whether building desktop applications, Linux services, or embedded firmware for AVR microcontrollers, the ELF file typically represents the primary output of the software build process — the source from which deployment formats such as Intel HEX or binary images are subsequently derived.
A solid understanding of ELF therefore offers far more than an appreciation of a particular file format. It provides insight into how modern software is constructed, linked, analysed and debugged across an enormous range of computing platforms.
Next in the Series
In Part 2 – Inside the ELF Format, we move beyond the historical background and begin exploring the structure of the format itself. We will examine the ELF header, program headers, section headers, common sections such as .text, .data and .bss, symbol tables, relocation records and debugging information — using utilities including readelf, objdump, nm and size to inspect real ELF files along the way.
By the end of Part 2, readers will have a detailed understanding of how an ELF file is organised internally, providing the foundation for the final articles, where these concepts are applied to AVR development using Atmel Studio and AVR-GCC.