
ELF Files Explained – Part 3: AVR ELF Production Files – The Complete Firmware Image
Part 3 of the "ELF Files Explained" Series
In Part 1, we explored the history of executable file formats and saw how ELF became the industry standard for modern operating systems and GCC-based development toolchains. In Part 2, we looked inside the format itself, examining the headers, sections, symbol tables and debugging information that let compilers, linkers and debuggers work together using a common file format.
Understanding the internal structure of an ELF file is valuable, but it naturally leads to another question: what does all of this mean for an embedded firmware project?
In this article, we turn to AVR development and examine the AVR ELF Production File generated by Atmel Studio and the AVR-GCC toolchain. We will see how it becomes the definitive output of the build process, bringing together executable code, EEPROM data, fuse settings, lock bits, User Signature memory and debugging information into a single engineering artefact that represents the complete firmware project.
From Source Code to ELF
Every AVR application begins life as source code — whether that is a few hundred lines written for a hobby project or hundreds of source files forming a complex commercial product. The build process follows the same fundamental sequence regardless of scale: source files are compiled, object files are produced, those object files are linked together, and a complete firmware image is created. Although this process is largely hidden behind the Build button in modern development environments, each stage contributes something essential to the finished application.
Compilation is the first step. Each C or C++ source file is processed independently by the compiler, translating the high-level language into AVR machine instructions. The output is an object file, conventionally identified by the .o extension. These object files already contain executable code, but they do not yet represent a complete application: functions defined in one source file may still reference functions in another, global variables have not yet been assigned their final addresses, and no overall memory layout has been established. At this stage the project exists only as a collection of individual modules.
Transforming those modules into a complete firmware image is the job of the linker. It is sometimes described simply as "the program that joins object files together", but its role is considerably more sophisticated than that. The linker resolves references between modules, incorporates the startup code and runtime libraries the application requires, allocates memory for code and data, positions interrupt vectors, and determines where every function and variable will reside within the finished firmware image. By the time this process completes, the project has ceased to be a collection of independently compiled source files and has become a single, coherent application ready for further processing.
The output of this stage is the project's ELF Production File.
This point is often misunderstood. The linker does not generate an Intel HEX file as its primary output — it produces a complete ELF file describing the firmware in its entirety. If additional programming files are required by the development environment or programming software, they are derived from this production file. The ELF therefore occupies a unique position in the development workflow: it is the first file that represents the finished firmware as a complete engineering project, rather than a collection of individual software modules.

Figure 1. The AVR Build Pipeline.
The AVR development process from source files through AVR-GCC compilation and linking to the ELF Production File. The ELF represents the completed firmware project before any programming-file conversion takes place.
Seen in this way, the build process is less about producing a programming file and more about constructing an increasingly complete description of the application. Each stage adds information unavailable at the previous stage until, after linking, the project has become a fully defined firmware image — captured within the ELF Production File, which records not only the executable code but also the information needed by development and analysis tools to understand precisely how the application has been constructed.
More Than an Executable
Developers whose experience lies primarily in desktop software often think of an executable file as something that exists solely to be loaded into memory and run by an operating system. Embedded systems have rather different requirements. An AVR microcontroller has no operating system to load applications, resolve shared libraries or manage virtual memory. Instead, the firmware image must describe exactly what should be programmed into the device and how that device should be configured before the application can execute correctly.
This is where the flexibility of the ELF format becomes particularly valuable. Rather than storing nothing more than executable instructions, an AVR ELF Production File provides a structured framework capable of describing every significant aspect of the firmware project. Some parts of the file describe the application's executable code; others record initialised data, symbolic information and debugging metadata; and the format is also flexible enough to accommodate information unique to the AVR architecture itself, allowing the production file to represent far more than simply the contents of Flash memory.
It is this ability to bring together multiple aspects of the firmware within a single engineering artefact that distinguishes the ELF Production File from simpler programming formats. Rather than a collection of unrelated files and settings, the project becomes one complete description of the firmware, generated directly from the source code and project configuration.
The next section looks at how this information is organised — first the standard ELF sections inherited from the generic format, then the AVR-specific additions that allow executable code, EEPROM data, fuse settings, lock bits and other device configuration information to coexist within a single, unified firmware image.
Inside an AVR ELF Production File
By the time the linker has finished its work, the AVR project has been transformed from a collection of independently compiled modules into a single ELF Production File. Although we now refer to it as a single file, it is worth remembering that an ELF file is not simply a block of machine code. As we saw in Part 2, it is a carefully organised container made up of numerous sections, each serving a specific purpose within the development process.
Some of these sections are common to almost every ELF file, regardless of processor architecture — they describe the executable program itself, store initialised data, record symbolic information, or provide the debugging data required by source-level development tools. Others are unique to particular processor families and reflect the specific requirements of those architectures. This extensibility is central to ELF's success: rather than inventing a new file format for every processor family, the specification provides a common framework that can be extended where necessary while remaining compatible with the existing toolchain.
The AVR architecture takes full advantage of this flexibility. An AVR ELF Production File contains all the standard information expected by the compiler, linker and debugger, but it can also describe elements unique to AVR microcontrollers — resulting in a file capable of representing not only the application itself but also the additional information needed to configure the target device correctly.
The Standard ELF Sections
Every AVR ELF Production File begins with the familiar sections found in almost every ELF file generated by GCC. These were introduced in Part 2, but it is worth briefly revisiting them, since they form the foundation on which the AVR-specific additions are built.
The most recognisable section is .text, containing the application's executable machine instructions. When the firmware is programmed into the device, it is this section that ultimately occupies Flash program memory — every compiled function, interrupt service routine and startup routine eventually finds its place here.
Alongside the executable code sit the sections that describe the application's data. Initialised global and static variables are placed in .data, while uninitialised variables occupy .bss. Although these variables reside in the AVR's SRAM while the application is running, their initial values are recorded within the ELF so that the startup code can initialise memory correctly on every reset. Read-only constants are typically placed into .rodata, keeping lookup tables, fixed strings and other constant data separate from executable instructions.
Symbol tables record the names and addresses of functions and variables, letting development tools relate machine code back to the original source. Debugging sections, including the DWARF information discussed in Part 2, preserve line numbers, variable types and other metadata that make source-level debugging possible.
Together, these sections provide a complete description of the executable application — not only what instructions should execute, but how the application is organised, where its data resides, and how development tools should interpret the finished firmware. For a desktop application, that may be sufficient. For an AVR microcontroller, it is only part of the story.
Extending ELF for AVR Microcontrollers
Unlike a desktop computer, an AVR microcontroller contains several distinct areas of non-volatile memory, each serving a different purpose. Programming the application code is only one stage of configuring the device. Depending on the project, the firmware may also require predefined EEPROM contents, fuse settings that determine how the microcontroller starts and operates, lock bits that control code protection, and — on supported devices — information stored in User Signature memory.
These are all fundamental parts of the finished firmware, yet very different in nature from executable instructions. Rather than forcing developers to manage them independently, the AVR toolchain extends the standard ELF format so that they can be represented within the same production file.
The result is an ELF Production File that describes the firmware as a whole, not merely its executable code. This is an important distinction from an engineering perspective: instead of treating configuration information as separate documentation to be remembered during production, the project itself becomes the authoritative source of that information. Rather than asking "which files are needed to program this device?", it becomes more natural to ask: "what information has been defined within the project's ELF Production File?"
From ELF Sections to Device Memory
One of the easiest ways to understand an AVR ELF Production File is to compare its internal organisation with the physical memory architecture of the target microcontroller. Although the ELF exists as a single file on disk, it describes several distinct regions within the AVR itself, allowing the firmware to represent far more than just the contents of program memory.

Figure 2. Mapping an AVR ELF Production File to Device Memory.
An AVR ELF Production File can describe multiple regions of the target device, including Flash program memory, SRAM, EEPROM, fuse bytes, lock bits and User Signature memory where supported.
This mapping highlights an important characteristic of the AVR toolchain: the ELF Production File does not blur the distinction between these memory regions. It records each one separately while preserving the relationship between them — Flash remains distinct from EEPROM, fuse bytes remain separate from executable code, and User Signature memory retains its own identity within the project. Yet all of these elements coexist within a single structured file, allowing the firmware to be treated as one coherent engineering artefact.
The practical advantages become increasingly apparent as projects grow in complexity. A production firmware release is no longer defined solely by the machine instructions programmed into Flash — it also includes the configuration that determines how the microcontroller behaves after reset, the non-volatile data required when the application first starts, and the information needed by debugging tools throughout development. By bringing these elements together within the ELF, the toolchain ensures that every significant aspect of the firmware remains associated with the build that created it.
Defining the Complete Firmware
If the ELF Production File can describe executable code, EEPROM contents, fuse settings and other device-specific information, where does that information actually come from? Is it added manually after the build, or generated automatically as part of the normal development process?
It originates from the project itself. The same source code and project configuration that define the application's behaviour also define the additional information the target device requires. During the build, the compiler and linker recognise these declarations and place them into the appropriate sections of the ELF Production File alongside the executable application.
The executable code is easy enough to understand: every function written in C or C++ is compiled into machine instructions before being placed into the executable sections of the ELF by the linker, and symbol tables and debugging information are generated automatically as part of the normal build process. The AVR-specific information — EEPROM data, fuse settings, lock bits and User Signature contents — deserves a closer look, since it can be defined directly within the project rather than relying on external configuration files or programming scripts.
This approach offers an important practical advantage. Instead of relying on separate documentation to describe how a microcontroller should be configured, the firmware project itself becomes the authoritative source of that information. When the project is rebuilt, every element of the firmware is regenerated together, keeping the executable code and the device configuration synchronised.
Defining the Complete Firmware

Figure 3. Building a Complete AVR ELF Production File.
Information defined by the AVR firmware project is brought together during compilation and linking to produce a single ELF Production File containing executable code, EEPROM data, fuse settings, lock bit configuration and User Signature information where supported.
Defining EEPROM Data
Although many embedded applications use EEPROM to store values generated while the system is running, it is equally common for a project to require predefined EEPROM contents from the moment the device is first programmed. Calibration constants, default configuration values, serial number templates and lookup tables are all examples of information that may need to be present before the firmware executes for the first time.
AVR-GCC makes this straightforward through the EEMEM attribute. Rather than requiring a separate EEPROM programming file to be created manually, the developer simply declares the required data within the application source code:
#include <avr/eeprom.h>
uint16_t EEMEM calibrationValue = 1234;
Although this declaration resembles an ordinary global variable, its purpose is quite different. Instead of allocating storage in SRAM, the compiler recognises the EEMEM attribute and records the variable within the EEPROM section of the finished ELF Production File. The data becomes part of the firmware project itself, rather than a separate programming resource — the source code is not merely describing how the application behaves after power-up, but also defining how the device should be prepared before the application ever begins to execute.
Defining Fuse Settings
If executable code determines what an AVR does, the fuse bytes determine how it starts. Fuse settings configure fundamental characteristics of the microcontroller — clock selection, startup timing, brown-out detection and, depending on the device family, a variety of other hardware options. Incorrect fuse settings can prevent an otherwise perfect application from operating correctly, making them just as important as the application code itself.
Rather than treating these settings as values to be remembered during production, AVR-GCC allows them to be defined as part of the firmware project:
#include <avr/io.h>
FUSES =
{
.low = LFUSE_DEFAULT,
.high = HFUSE_DEFAULT,
.extended = EFUSE_DEFAULT
};
During compilation, these declarations are recognised by the toolchain and incorporated into the appropriate sections of the ELF Production File, becoming part of the project's engineering definition rather than existing solely as notes in a production document or entries in a programmer's configuration dialogue. This significantly improves the reproducibility of a firmware build: anyone rebuilding the project has access not only to the application source code but also to the intended device configuration used when the firmware was originally created.
Lock Bits and Device Protection
Many commercial products require some degree of firmware protection once manufacturing is complete. AVR devices provide lock bits that can restrict access to program memory, preventing accidental overwriting or unauthorised extraction of application code. As with fuse bytes, these settings can be defined directly within the project:
#include <avr/io.h>
LOCKBITS =
{
.lockbits = LB_MODE_3
};
By incorporating these settings into the ELF Production File, the project records not only the firmware itself but also the intended protection policy for the finished product. The security configuration becomes part of the firmware definition, rather than a separate manufacturing instruction that must be applied manually — reflecting an important engineering principle: every piece of information required to produce a correctly configured device should, wherever possible, originate from the same controlled source.
User Signature Memory
Many modern AVR devices provide an additional non-volatile memory region known as the User Signature or User Row. Unlike Flash memory or EEPROM, this area is intended for application-specific information that remains associated with the device throughout its lifetime — manufacturing identifiers, hardware revision information, calibration constants and other product-specific data that does not naturally belong within the executable application.
Where supported by the target device, the AVR toolchain provides mechanisms for defining this information within the firmware project, so that it too becomes part of the completed ELF Production File. Not every AVR family implements User Signature memory in exactly the same way, but the underlying philosophy remains consistent: information that forms part of the finished firmware package should be represented within the project's production file wherever possible.

Figure 4. AVR-Specific Information Within the ELF.
AVR-specific declarations such as EEPROM data, fuse settings, lock bits and User Signature information are incorporated into dedicated sections of the ELF Production File, keeping device configuration associated with the firmware project.
One Project, One Engineering Record
Stepping back from the implementation details, a clear pattern emerges. Each of the examples above starts life as an ordinary declaration within the project source code. During compilation and linking, those declarations are recognised by the AVR toolchain and placed into their appropriate sections within the ELF Production File. The result is a single file that captures far more than executable instructions alone.
Figure 3 – Building a Complete AVR ELF Production File
Application Source Code
│
├─────────────► Executable Code
│
├─────────────► EEPROM Data
│
├─────────────► Fuse Settings
│
├─────────────► Lock Bit Configuration
│
└─────────────► User Signature Information
│
▼
Project.elf
Seen in this light, the ELF Production File becomes much more than the output of the linker — it is the complete engineering record of the firmware project. Every significant aspect of the application, from its executable code and memory initialisation to its device configuration and supporting metadata, is brought together into a single, structured artefact generated directly from the project's controlled source.
This marks an important shift in perspective. Rather than thinking in terms of separate firmware files and independent device settings, it becomes more natural to think in terms of a single production file that represents the firmware in its entirety — a distinction fundamental to understanding modern AVR development workflows.
Conclusion
Throughout this article we have looked beyond the executable code of an AVR project and examined the production file that sits at the heart of the entire build process. Although many developers interact primarily with Intel HEX files day to day, it is the ELF Production File that represents the complete output of the toolchain. Created by the linker, it brings together every significant aspect of the firmware project into a single, structured engineering artefact — machine instructions, EEPROM initialisation data, fuse settings, lock bit configuration and, where supported, User Signature memory, combined with the symbol tables and debugging information required during development.
Viewing the project in this way changes the role of the ELF file. Rather than an intermediate build artefact used solely by the debugger, it becomes the authoritative representation of the firmware itself — the single source from which every other stage of the development process can proceed.
Coming Next
Part 4 – Programming AVR Microcontrollers Using ELF Production Files
In the final article of this series, we follow the AVR ELF Production File beyond the build system and into the programming process. We begin by looking at why Intel HEX became the traditional firmware programming format, before exploring how modern programming software can work directly with an ELF Production File instead — mapping executable code, EEPROM data, fuse settings, lock bits and other device configuration to their appropriate regions within the target microcontroller.
Finally, we see how Kanda's AVR programming software simplifies this entire workflow by automatically using the GNU Binutils behind the scenes, allowing developers to work with a single authoritative firmware image throughout development, testing and production.