DEV Community

Ripan Deuri
Ripan Deuri

Posted on

Dissecting ELF for Bare Metal Development: Sections, Segments, VMA, and LMA Explained

The memory map in the previous post Bare Metal Basics - Part 1: Understanding Memory Maps describes the hardware address space, but it does not explain how compiled code and data are placed into that space. The compiler does not emit instructions directly at fixed memory addresses, nor does it decide where variables reside at runtime. Instead, compilation produces relocatable artifacts that must later be assigned concrete addresses.

The GNU toolchain uses ELF (Executable and Linkable Format) as its standard output at multiple stages. These ELF files contain executable machine code, but they also include metadata such as symbols, relocation records, and debugging information—structures that bare-metal hardware cannot interpret. Since bare-metal systems have no loader, the addresses assigned during linking become the actual runtime addresses used by the CPU. Understanding these distinctions is essential for building reliable bare-metal systems.

This post dissects ELF files, explains sections and segments, and demonstrates how to inspect linker output to verify that the generated layout matches the intended memory map.


From Source Code to Executable

Bare-metal development involves multiple stages, each with a specific responsibility.

Stage 1: Source to Object Files

Source files (C and assembly) pass through the compiler and assembler, producing relocatable object files (.o) that contain machine code and data grouped into sections, but not yet bound to fixed addresses. References to symbols defined in other object files are left unresolved.

Stage 2: Linking Object Files

The linker reads all object files and a linker script. It then:

  • Resolves symbol references to actual memory addresses
  • Combines sections from multiple object files
  • Assigns memory addresses to sections based on the linker script
  • Produces an ELF executable with a defined entry point

The linker script serves as the blueprint: it describes how sections such as .text, .rodata, .data, and .bss map into the hardware memory regions. The linker assigns addresses accordingly, subject to options such as dead-code elimination (--gc-sections). The specific addresses used depend entirely on the memory layout defined in the script.

ELF Structure

An ELF executable is a structured container composed of several logical parts.

ELF Header

The ELF header identifies the file format and describes global properties such as:

  • Magic number (0x7f, 'E', 'L', 'F')
  • Target architecture (ARM, x86, etc.)
  • Entry point address where CPU execution begins
  • Offsets to section headers and program headers

These values guide development tools and loaders but are not interpreted by bare-metal processors.

Section Headers

The linker organizes code and data into named sections. Each section is a logical container:

  • .text: Executable code
  • .rodata: Read-only data
  • .data: Initialized global/static variables
  • .bss: Uninitialized global/static variables
  • Custom sections: Platform-specific sections such as interrupt vectors or application-defined segments

Each section has several important address concepts:

  • LMA (Load Memory Address): Where the section’s initial contents reside in non-volatile memory (typically flash).
  • VMA (Virtual Memory Address): The runtime address where the CPU accesses the section.
  • Size and Alignment: Constraints on how the linker places each section.

VMAs represent the addresses used by executing code, and symbol values correspond to VMAs.

Program Headers (Segments)

Segments describe how an ELF file should be loaded by a tool that understands ELF—such as QEMU or a custom bootloader. Loaders interpret segments, not sections. Each segment describes a contiguous region of memory to populate:

  • Segment type (e.g., LOAD)
  • File offset
  • Virtual or physical destination address
  • File size and memory size

Bare-metal CPUs do not interpret segments, but they matter when using QEMU or bootloaders that load ELF directly. QEMU follows program headers and uses the physical address field when present.

Metadata

Additional ELF components include:

  • Symbol and string tables
  • Debug information (DWARF)
  • Relocation records

These are essential during development but have no meaning for the hardware.

Sections and Their Runtime Meaning

.text: Executable Code

The .text section contains all compiled machine instructions, including startup routines. Some platforms place interrupt vectors in a dedicated section with strict address requirements; linker scripts typically handle this separately.

Properties:

  • Readable and executable
  • Typically stored in flash
  • May execute in place or be copied to RAM depending on design

Use objdump to see disassembled .text with addresses:

arm-none-eabi-objdump -d boot.elf
Enter fullscreen mode Exit fullscreen mode

.rodata: Read-Only Data

The .rodata section contains immutable data:

  • String literals
  • const global variables
  • Compiler-generated lookup tables

This section typically resides in flash alongside .text.

Use:

objdump -s -j .rodata boot.elf
Enter fullscreen mode Exit fullscreen mode

.data: Initialized Global Variables

The .data section contains global and static variables with explicit initializers. These variables must be writable at runtime. Their initial values are stored in flash (LMA), and the startup code copies them into RAM (VMA) before entering the C runtime.

Use:

readelf -S boot.elf
Enter fullscreen mode Exit fullscreen mode

to view VMA and LMA assignments.

.bss: Uninitialized Global Variables

The .bss section contains uninitialized global and static variables. .bss does not appear in the binary because storing zero-filled data in flash would be wasteful.

The linker records:

  • Section name
  • VMA
  • Size

At runtime, the startup code sets the entire .bss region to zero. The memory is not allocated by software; it is reserved by the linker through the memory map.

Raw Binary Image

A processor begins execution at a hardware-defined reset address (for example, 0x00000000 or a device-specific flash base). The CPU does not interpret ELF metadata. It requires only:

  • Executable instructions at the reset address
  • Read-only data accessible at expected locations
  • Writable memory for initialized and uninitialized variables
  • Valid stack space

ELF files must therefore be transformed into binary images whose bytes correspond exactly to the intended load addresses.

Inspecting ELF Output

Common toolchain utilities make ELF inspection straightforward.

readelf

arm-none-eabi-readelf -h boot.elf   # ELF header
arm-none-eabi-readelf -S boot.elf   # Section headers
arm-none-eabi-readelf -l boot.elf   # Program headers
arm-none-eabi-readelf -s boot.elf   # Symbol table
Enter fullscreen mode Exit fullscreen mode

objdump

arm-none-eabi-objdump -h boot.elf   # Section summary
arm-none-eabi-objdump -d boot.elf   # Disassembly
arm-none-eabi-objdump -s -j .rodata boot.elf
Enter fullscreen mode Exit fullscreen mode

objcopy

arm-none-eabi-objcopy -O binary boot.elf boot.bin
Enter fullscreen mode Exit fullscreen mode

This produces a raw binary by extracting only loadable content, arranged according to LMAs. The resulting file contains no address metadata, so it must be programmed into flash at the correct offset corresponding to those LMAs.

Alternative formats such as Intel HEX and S-records convey the same conceptual information with explicit addressing.

How QEMU Loads ELF and Binary Image

QEMU supports both ELF-based loading and raw binary loading.

ELF Loading (--kernel)

When provided with an ELF file, QEMU:

  • Reads the ELF header for architecture and entry point
  • Parses program headers
  • Loads each LOAD segment at its specified destination address
  • Sets the CPU PC to the ELF entry point
qemu-system-arm -M vexpress-a9 -cpu cortex-a9 -m 128M -nographic \
  -kernel boot.elf -S -gdb tcp::1234
Enter fullscreen mode Exit fullscreen mode

Binary Loading (-pflash)

When loading a raw binary, QEMU:

  • Maps the file directly into the flash device model
  • Uses device-specific flash size limits
  • Begins execution from the board’s reset vector

This model mirrors real hardware and validates whether the firmware image matches the expected memory map.

qemu-system-arm -M vexpress-a9 -cpu cortex-a9 -m 128M -nographic \
  -drive if=pflash,format=raw,file=boot.bin -S -gdb tcp::1234
Enter fullscreen mode Exit fullscreen mode

Conclusion

ELF files exist to support the toolchain. They encode executable code along with metadata required for linking, relocation, and debugging. Bare-metal processors, however, require only instructions and data placed at precise addresses.

Understanding the distinctions between sections and segments, between VMA and LMA, and between compilation and linking is foundational to bare-metal work. The linker script provides the concrete mapping between ELF structure and hardware memory.

The next post focuses on linker scripts: how they express memory intent, how the linker interprets them, and how to validate the final layout against the hardware memory map.

Top comments (0)