What Happens During the elf2image Conversion Step for ESP32 Bootloader Files

The elf2image command performs a deterministic, multi-stage transformation that converts linker-generated ELF files into flashable binary images by parsing sections, injecting ROM-specific headers, and merging adjacent memory segments.

The elf2image conversion step is the critical bridge between ESP-IDF linker output and bootable firmware for ESP32 microcontrollers. Implemented in the jason2866/esp_flasher repository, this process resides primarily in esp_flasher/own_esptool.py and handles everything from secure boot padding to flash parameter encoding. Understanding this transformation allows developers to debug boot failures and optimize bootloader layouts for specific hardware configurations.

ELF Parsing and Chip-Specific Image Initialization

The conversion begins by loading the source ELF and selecting the appropriate chip-specific image handler that understands the target ROM's expected binary layout.

Loading the ELF Structure

The tool first instantiates an ELFFile object to parse the input bootloader ELF. This extracts the entry point address, individual sections, and optionally computes a SHA-256 hash of the entire file for integrity verification.

e = ELFFile(args.input)

According to the source in esp_flasher/own_esptool.py (lines 5718-5722), this initialization captures all metadata required for subsequent image generation, including the critical entry point that the ESP32 ROM will jump to after loading the bootloader.

Selecting the Firmware Image Class

Based on the --chip argument, elf2image instantiates the appropriate subclass of BaseFirmwareImage. For ESP32 bootloaders, this creates an ESP32FirmwareImage object (defined around line 4518) that understands the specific header layout, secure-pad handling, and MMU page size requirements of the ESP32 ROM loader.

if args.chip == 'esp32':
    image = ESP32FirmwareImage()

This chip-specific class ensures that the resulting binary matches exactly what the target ROM expects when reading from flash memory.

Flash Parameter Injection and Secure Padding

After initialization, the tool populates the image header with flash configuration parameters and optional security features required for secure boot workflows.

ROM Header Configuration

The function copies critical parameters from the ELF and command-line arguments into the image header structure:

  • Entry point: image.entrypoint = e.entrypoint
  • Flash mode: Translated from strings (qio, dio, dout, qout) into numeric enums that the ROM understands
  • Flash size and frequency: Combined into a single 32-bit word via image.flash_size_freq using helper methods ROM_LOADER.parse_flash_size_arg and parse_flash_freq_arg
  • MMU page size: Set via image.set_mmu_page_size when --flash-mmu-page-size is specified
  • Chip revision bounds: Stored in image.min_rev, image.max_rev_full, and related fields for chips supporting revision checks

These assignments occur between lines 5670-5889 in own_esptool.py, ensuring the bootloader binary communicates compatible flash settings to the ROM before executing user code.

Secure Boot Padding Options

If the --secure-pad or --secure-pad-v2 flags are present, the tool configures additional padding blocks required by the ESP32 secure boot ROM. Setting image.secure_pad to '1' or '2' instructs the ROM to expect a 16 KB (v1) or 32 KB (v2) secure padding block after the bootloader binary, protecting against certain classes of physical flash attacks.

if args.secure_pad:
    image.secure_pad = '1'

This handling occurs at lines 5727-5731, immediately after chip selection.

Segment Processing and Image Validation

The final stages involve selecting data sources, optimizing the memory layout, validating integrity, and writing the output file.

Sections vs. Segments Selection

By default, elf2image builds the image from ELF sections (e.sections). However, when --use_segments is passed, it uses raw ELF segments (e.segments) instead, preserving custom alignments that the linker may have produced for specific memory layouts.

image.segments = e.segments if args.use_segments else e.sections

This choice affects how the linker-generated data is packed into the flash image and can impact boot performance on certain ESP32 variants.

Merging and Padding Strategies

The tool performs two optimization steps to ensure the binary matches ROM expectations:

  1. Optional size padding: When --pad-to-size is specified, the image is padded to a fixed flash size (e.g., the first 0x10000 bytes), ensuring the bootloader occupies a predictable region regardless of actual code size.

  2. Segment merging: image.merge_adjacent_segments() collapses contiguous memory regions into single blocks, reducing flash write operations and ensuring the layout matches what the ESP32 ROM bootloader expects when mapping flash into RAM.

These operations occur at lines 5784-5791, preparing the image for final validation.

Verification and Output Generation

Before writing, image.verify() runs internal consistency checks including header field validation, padding verification, and revision bound checks. If verification fails, a FatalError aborts the conversion immediately.

The output filename is determined either by --output or automatically via image.default_output_name(args.input), which replaces the .elf extension with .bin. Finally, image.save(args.output) serializes the complete flash image including the ROM loader header, optional secure padding, and any appended SHA-256 digest (when --elf-sha256-offset is specified).

image.save(args.output)
print("Successfully created {} image.".format(args.chip))

This sequence at lines 5801-5806 produces a binary ready for flashing via write_flash or merge_bin commands.

Practical elf2image Usage Examples

Converting an ESP32-S3 Bootloader via CLI

The following command converts an ESP32-S3 bootloader ELF with specific flash parameters:

python -m esp_flasher elf2image \
    bootloader/esp32s3/bin/bootloader_qio_80m.elf \
    --chip esp32s3 \
    --flash_mode dio \
    --flash_freq 80m \
    --flash_size 4MB \
    -o bootloader_esp32s3.bin

This invocation parses the ELF, creates an ESP32S3FirmwareImage, encodes DIO mode at 80 MHz, pads to 4 MiB, and writes the merged binary.

Programmatic API Usage

You can invoke the same logic programmatically:

from esp_flasher.own_esptool import elf2image, argparse

args = argparse.Namespace(
    input='bootloader/esp32s3/bin/bootloader_qio_80m.elf',
    chip='esp32s3',
    flash_mode='dio',
    flash_freq='80m',
    flash_size='4MB',
    version='1',
    output='bootloader_s3.bin',
    secure_pad=False,
    secure_pad_v2=False,
    use_segments=False,
    pad_to_size=None,
    elf_sha256_offset=None,
    flash_mmu_page_size=None,
    min_rev=0,
    min_rev_full=0,
    max_rev_full=0,
)

elf2image(args)

Verifying the Generated Image

Confirm the conversion succeeded by inspecting the image metadata:

python -m esp_flasher image_info bootloader_s3.bin --chip esp32s3

This displays the entry point, flash parameters, and segment layout, verifying that elf2image encoded everything correctly.

Summary

  • The elf2image function in esp_flasher/own_esptool.py orchestrates the conversion from ELF to flashable binary through a deterministic 11-step pipeline.
  • Chip-specific image classes like ESP32FirmwareImage handle ROM-specific header layouts and secure boot padding requirements.
  • Flash parameters including mode, frequency, size, and MMU page size are encoded into the binary header exactly as the ESP32 ROM expects.
  • The tool supports both sections (default) and segments (--use_segments) as data sources, with automatic merging of adjacent regions to optimize flash layout.
  • Optional secure padding (16 KB or 32 KB) and SHA-256 insertion support advanced secure boot workflows.
  • Validation via image.verify() ensures the output binary will be accepted by the target ROM before writing the final .bin file.

Frequently Asked Questions

What is the difference between using sections and segments in elf2image?

By default, elf2image uses ELF sections (e.sections) which represent the logical divisions created by the compiler and linker. When you pass --use_segments, the tool uses raw ELF segments (e.segments) instead, preserving the exact memory alignment and permissions set by the linker. Segments are typically larger, page-aligned regions that the OS loader uses, while sections are finer-grained divisions used by the linker for relocation and symbol management.

How does secure padding affect the bootloader binary size?

When --secure-pad or --secure-pad-v2 is enabled, the tool sets image.secure_pad to '1' or '2', instructing the ESP32 ROM to reserve 16 KB or 32 KB of secure padding space immediately following the bootloader. This increases the total binary size to accommodate the padding block, which the ROM uses for cryptographic verification during secure boot. The actual bootloader code size remains unchanged, but the flash image occupies more space to satisfy these security requirements.

Can elf2image generate images for multiple ESP32 variants?

Yes, the tool supports all major ESP32 variants including ESP32, ESP32-S2, ESP32-S3, ESP32-C3, and ESP32-C6 through chip-specific subclasses of BaseFirmwareImage. The --chip argument determines which class is instantiated (e.g., ESP32FirmwareImage, ESP32S2FirmwareImage), ensuring the generated binary uses the correct header format, MMU settings, and ROM layout for the specific silicon revision.

Where does elf2image store flash mode and frequency in the output binary?

The tool combines flash mode (QIO, DIO, etc.) and frequency (40 MHz, 80 MHz, etc.) into a single 32-bit word stored in image.flash_size_freq. This word is written into the image header at a specific offset expected by the ESP32 ROM bootloader. The helper functions ROM_LOADER.parse_flash_size_arg and parse_flash_freq_arg perform the translation from human-readable strings to the numeric enums defined by Espressif's boot ROM specification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →