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
.rodata: Read-Only Data
The .rodata section contains immutable data:
- String literals
-
constglobal variables - Compiler-generated lookup tables
This section typically resides in flash alongside .text.
Use:
objdump -s -j .rodata boot.elf
.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
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
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
objcopy
arm-none-eabi-objcopy -O binary boot.elf boot.bin
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
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
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)