UF2 and ELF: a Comparison

Comparison of the UF2 and ELF binary formats

Two binary formats are commonly used to deploy firmware to microcontrollers: UF2 (USB Flashing Format) and ELF (Executable and Linkable Format). They serve overlapping but distinct purposes, and understanding their differences helps in choosing the right format for a given workflow.

Origins

ELF was introduced in 1983 as part of AT&T’s System V Unix. It became the standard executable format across Unix systems and was adopted by the embedded toolchain ecosystem (GCC, LLVM, binutils) as the native output format for cross-compilation. Today, ELF is the universal format produced by ARM cross-compilers and consumed by debuggers, flash programmers, and analysis tools.

UF2 was created by Microsoft in 2017 for the MakeCode educational programming environment. It was designed to allow firmware to be installed by copying a file to a USB mass storage device – the microcontroller appears as a removable drive, and dropping a UF2 file onto it triggers flashing. UF2 was subsequently adopted by several microcontroller platforms, most notably the Raspberry Pi RP2040 and RP2350.

Structure

ELF

An ELF file is a structured container with a well-defined header, program headers (describing loadable segments), and section headers (describing logical divisions of the binary).

A typical firmware ELF contains:

  • code and data segments with explicit load addresses, sizes, and memory permissions;
  • symbol table mapping names to addresses – every procedure, global variable, and label is recorded with its address, size, and binding;
  • debug sections (DWARF format) containing source file names, line number mappings, type definitions, variable locations, and stack frame descriptions;
  • relocation and linking metadata used during the build process.

The format is extensible: additional sections can carry arbitrary metadata without affecting the loadable content.

UF2

A UF2 file is a sequence of 512-byte blocks, each carrying up to 256 bytes of payload data along with a target address.

The format is deliberately simple:

  • each block is self-contained: magic numbers, flags, target address, payload size, payload data, and a final magic number;
  • blocks can appear in any order – the target address in each block determines where the data is written;
  • the 512-byte block size matches the USB mass storage sector size, which is what enables the drag-and-drop flashing model.

A UF2 file carries only the raw binary content and target addresses. There is no symbol table, no debug information, and no section structure beyond the block sequence.

Capabilities

Capability ELF UF2
Load addresses Per-segment, arbitrary Per-block, arbitrary
Multiple memory regions Yes (multiple segments) Yes (different block addresses)
Symbol table Yes (functions, variables, labels) No
Debug information (DWARF) Yes (source lines, types, variables) No
Section metadata Yes (arbitrary named sections) No
File structure Header + tables + sections Flat block sequence
Human-readable metadata Via standard tools (readelf, objdump) Block headers only
Extensibility New section types Family ID field

Deployment

ELF Loading

ELF files are loaded by tools that understand the format’s segment structure: debuggers (GDB), flash programmers (OpenOCD, picotool, pyOCD), and vendor-specific utilities. The loading tool reads the program headers, extracts the loadable segments, and writes each segment to its specified address in flash or RAM.

Because the tool parses the full ELF structure, it can:

  • program only the flash sectors that the image occupies, leaving other regions untouched;
  • verify the written content against the ELF data;
  • use the symbol table and debug sections for source-level debugging after loading;
  • extract metadata (build information, version strings, section attributes) from the ELF without modifying the target.

UF2 Loading

UF2 files are loaded either by a bootloader running on the microcontroller (via USB mass storage) or by host-side tools that parse the block sequence. The bootloader receives 512-byte blocks, extracts each payload, and writes it to the indicated flash address.

The USB mass storage model requires no host-side tool installation – the microcontroller appears as a drive, and the operating system’s file copy operation delivers the blocks. This makes initial setup straightforward: no driver installation, no tool configuration, no command-line interface.

Trade-offs

ELF Strengths

  • Complete information: a single ELF file carries everything needed for loading, debugging, and post-mortem analysis. There is no separate symbol file or debug database to keep in sync.
  • Selective flash programming: the segment structure allows tools to program only the affected sectors, preserving adjacent flash content. This is essential for systems with multiple independent images in flash (eg. Secure and Non-Secure firmware).
  • Debug integration: loading an ELF gives the debugger immediate access to symbols, source lines, types, and stack unwinding – no additional steps required.
  • Toolchain standard: every ARM cross-compiler produces ELF natively. No conversion step is needed between compilation and deployment.
  • Analysis ecosystem: a rich set of tools (readelf, objdump, nm, size, and custom scripts) can inspect, validate, and transform ELF files.

UF2 Strengths

  • Zero tool installation: deploying firmware requires only a file copy. No debugger, no flash programmer, no command-line tool, no driver.
  • Platform independence: any operating system that supports USB mass storage can deploy UF2 files – no platform-specific tooling.
  • Simplicity: the block format is straightforward to generate and parse. A minimal UF2 creator can be written in a few dozen lines of code.
  • Bootloader resilience: the self-contained block structure means that a partial transfer (interrupted copy) does not corrupt flash – the bootloader can validate each block independently.
  • Partition tagging: UF2 blocks can carry a partition type tag. A single UF2 file can contain blocks with different tags, allowing one file to serve multiple target configurations – the bootloader selects the matching partition and loads only those blocks, skipping the rest. This is useful for distributing a single UF2 that covers several hardware variants or boot modes. Currently (as implemented on the RP2350), only one tagged partition is loaded per boot; true multi-partition loading (eg. loading both Secure and Non-Secure images from one UF2) is not yet supported, though this may change as the platform evolves. This feature is platform-specific and not part of the original UF2 specification.

ELF Limitations

  • Requires a loading tool (debugger, flash programmer, or host utility) that understands the ELF format.
  • Initial setup of the tool and its connection to the target (debug probe configuration, server scripts) has a higher threshold than file copy.

UF2 Limitations

  • No symbol table or debug information – a separate ELF file is needed for debugging, and it must be kept in sync with the deployed binary.
  • No section structure – the loading tool cannot distinguish between different logical regions of the firmware.
  • Limited metadata – the format carries payload and addresses, but no build information, version data, or section attributes.
  • Conversion step required – the compiler produces ELF or plain binary; a separate tool must convert to UF2 before deployment.

In Practice

Many embedded projects use both formats at different stages:

  • ELF during development: the debugger loads ELF directly, providing symbols, source-level debugging, and selective flash programming.
  • UF2 for distribution: end users receive a UF2 file and deploy it without needing a debug probe or development tools.

For projects that require source-level debugging, multi-image flash layouts, or post-mortem analysis, ELF is the primary format throughout the development cycle. UF2 serves distribution and field deployment where simplicity of installation is the priority.

References

  • ELF Specification – Tool Interface Standard (TIS), Executable and Linking Format, Version 1.2 (the foundational spec; processor-specific supplements define ARM extensions)
  • DWARF Debugging Standard – DWARF Version 5, the debug information format carried in ELF sections
  • UF2 Specification – USB Flashing Format, maintained by Microsoft on GitHub
Updated: 2026-03-18