DEV Community

Ripan Deuri
Ripan Deuri

Posted on

Bare Metal Basics - Part 1: Understanding Memory Maps


Bare-metal programming runs without operating-system support. There is no program loader, no virtual memory, and no runtime safety layer - only the CPU, the physical memory map, and the code placed into that memory. So a clear understanding of the physical memory layout - where flash, RAM, and peripheral registers reside is essential. This layout acts as a hardware contract that governs startup behavior, linker scripts, and instruction flow.

This post examines the memory map of the ARM vexpress-a9 platform as modeled by QEMU, focusing on how real hardware organizes memory and how software is expected to interact with it.

The Unified Address Space Model

ARM processors interact with memory and peripherals through an address bus, treating all devices as regions within a single addressable space. From the CPU’s perspective, there is no inherent distinction between RAM, flash, or peripheral registers - it simply reads from and writes to addresses placed on the bus. The behavior depends entirely on what hardware responds at those addresses.

Some important terms:

  • Physical Address: The actual address driven onto the bus by the CPU. Hardware devices decode these addresses and respond accordingly.
  • Symbols and Labels: Toolchain abstractions such as _start or main. These exist only at build time; the CPU never sees symbols - only addresses.
  • Memory Aliasing: A single physical memory device responding to multiple physical address ranges.

This unified model is powerful, but a wrong address does not fail gracefully - it simply talks to the wrong hardware.

ARM vexpress-a9 Memory Layout in QEMU

Different SoCs place RAM, flash, and peripherals in very different locations. With QEMU configured for 128 MB of RAM, the layout looks like this:

Address Range Size Device Access
0x00000000–0x03FFFFFF 64 MB Flash Read, Execute
0x10000000–0x1FFFFFFF 256 MB Peripherals Device-specific
0x60000000–0x67FFFFFF 128 MB RAM Read, Write, Execute

Flash (0x00000000–0x03FFFFFF)

  • Non-volatile storage for code.
  • Persists across reset and power cycles.
  • Typically readable and executable, but not writable at runtime.
  • In QEMU, this region is modeled as an emulated NOR flash device backed by a file.

On the vexpress-a9 platform, QEMU aliases flash to address 0x00000000 at reset, allowing the CPU to fetch its initial instructions from this location.

RAM (0x60000000–0x67FFFFFF)

  • Volatile, writable memory used at runtime.
  • Holds stacks, global variables, heap, and optionally executable code.
  • Contents are lost on reset or power loss.
  • Faster to access than flash in most systems.

Most bare-metal programs eventually transition to executing from RAM, even if initial boot code starts in flash.

Peripherals (0x10000000 and above):

  • Memory-mapped device registers (UART, GPIO, timers, interrupt controllers). For more info on MMIO Memory Mapped IO (MMIO)

Writing to these addresses is how bare-metal software communicates with hardware.

Summary:

Region Address Range Purpose
Reset Flash Alias 0x00000000–0x03FFFFFF CPU reset entry; aliased flash
Peripherals 0x10000000–0x1FFFFFFF MMIO registers (UART, timers, sysctl, etc.)
A9 Private Region 0x1E000000–0x1E001FFF SCU, GIC CPU interface, timers
L2 Cache Controller 0x1E00A000–0x1E00AFFF L2 cache control registers
Flash0 / Flash1 0x40000000–0x47FFFFFF Actual flash devices
On-Chip SRAM 0x48000000–0x49FFFFFF Fast internal SRAM
Main RAM 0x60000000–0x67FFFFFF Runtime data, stacks, heaps, and optionally executable code

Memory Aliasing

The vexpress-a9 platform implements physical memory aliasing. The same flash device is accessible through more than one physical address range.

At reset, flash is aliased to 0x00000000 so the CPU can fetch its reset vector from a well-known location. The same flash may also exist at another address used internally by the platform. Both addresses refer to the same physical storage.

This technique simplifies reset behavior without constraining the rest of the memory map.

Execute-in-Place vs Copy-to-RAM

Bare-metal systems typically use one of two strategies for placing executable code.

Execute-in-Place (XIP)

The CPU fetches instructions directly from flash.

  • Minimal RAM usage
  • Simple startup logic
  • Slower execution due to flash access latency

XIP is well suited for small systems or early boot code.

Copy-to-RAM

Code is stored in flash but copied into RAM by startup code before execution continues.

  • Faster instruction execution
  • Requires RAM for code storage
  • Introduces a copy step during startup

Many systems use a hybrid approach: minimal startup code executes from flash, then hands control to a RAM-resident program.

Inspecting Memory with the QEMU Monitor

QEMU provides a monitor interface that allows inspection of memory and device state while the system is running or paused—even before guest code executes.

Launching QEMU with a Telnet Monitor

qemu-system-arm -M vexpress-a9 -cpu cortex-a9 -m 128M -nographic \
  -monitor telnet:127.0.0.1:4444,server,nowait
Enter fullscreen mode Exit fullscreen mode

This opens a QEMU monitor accessible via Telnet:

telnet 127.0.0.1 4444
Enter fullscreen mode Exit fullscreen mode

Examining Physical Memory with the xp Command

Basic syntax:

(qemu) xp /[count][format][size] address
Enter fullscreen mode Exit fullscreen mode

Example: read four 32-bit words from 0x60000000 after running the minimal startup code from From ARM Assembly to Machine Code: A Bare-Metal Primer

(qemu) xp /4xw 0x60000000
0000000060000000: 0xe3a00000 0xe59f1004 0xe59f2004 0xe59ff004
Enter fullscreen mode Exit fullscreen mode

Common format options include:

  • /4xw: four 32-bit words in hexadecimal
  • /16xb: sixteen bytes in hexadecimal
  • /8xh: eight 16-bit halfwords in hexadecimal

Viewing Memory Hierarchy with info mtree command

(qemu) info mtree
Enter fullscreen mode Exit fullscreen mode

Example output:

address-space: memory
  0000000000000000-ffffffffffffffff (prio 0, i/o): system

    0000000000000000-0000000003ffffff (prio 0, romd):
      alias vexpress.flashalias @vexpress.flash0
        → 0000000040000000-0000000043ffffff

    0000000040000000-0000000043ffffff (prio 0, romd):
      vexpress.flash0

    0000000044000000-0000000047ffffff (prio 0, romd):
      vexpress.flash1

    0000000060000000-0000000067ffffff (prio 0, ram):
      vexpress.highmem
Enter fullscreen mode Exit fullscreen mode

Conclusion

Bare-metal systems are defined by their memory maps. On the vexpress-a9 platform, flash is aliased at reset, RAM resides at a fixed high address range, and peripherals are accessed through memory-mapped registers. These characteristics directly shape startup code, linker scripts, and execution flow.

With the physical layout established, the next step is examining how toolchains represent this memory. The upcoming post will explore ELF files, sections, and segments, showing how compiler and linker decisions result in code and data being placed into flash and RAM.

Top comments (0)