Understanding what happens before main() is essential when working on bare-metal systems. This article examines the reset behavior of ARMv7 and shows how to take control of the first instructions executed after reset on the vexpress-a9 platform in QEMU. A minimal vector table and linker script demonstrate how the CPU fetches its initial instruction from address 0x0 and how a simple branch verifies that control flow behaves exactly as intended.
The Myth of “PC Starts at main”
In a bare-metal system, main is simply a C function invoked after a sequence of hardware-defined and software-defined steps:
- Hardware reset forces the CPU into a well-defined privileged mode.
- A reset vector determines the first instruction the CPU fetches.
- Low-level startup code configures the execution environment.
- Only after this preparation does the C runtime transfer control to
main.
This post focuses on step 2: how the CPU selects its first instruction after reset and how software controls that decision.
ARMv7 Reset Behavior: What the CPU Actually Does
When an ARMv7-A core such as Cortex-A9 exits reset, the architecture defines only a minimal and deterministic subset of the processor state. The SoC integration adds further rules, but the essential behaviors are consistent:
- CPU mode: The core enters Supervisor (SVC) mode, a privileged mode suitable for exception entry.
- Program Counter (PC): Loaded from the reset vector address, which is implementation-defined but commonly 0x00000000 or a remapped alias of another memory region.
- General-purpose registers: All registers except the PC (and certain status bits) are architecturally undefined. Software must not assume values for r0–r12, SP, or LR.
- Interrupts: External interrupts are disabled at reset, preventing accidental entry into uninitialized exception handlers.
- MMU and caches: Disabled. Execution begins in a flat physical address space without virtual memory.
This initial state contains just enough information for the CPU to fetch and execute the first instruction from the reset vector. Everything else—stack initialization, memory sections, BSS clearing, C runtime setup—must be implemented in startup code.
Historically, many ARM systems located the vector table at 0x00000000, simplifying early boot ROM design. Modern systems provide more flexibility:
- Some support high-vector mode, placing the vector table at 0xFFFF0000.
- Many SoCs implement memory remapping so that ROM or flash appears temporarily at 0x0 during reset.
- The physical storage for the bootloader may exist elsewhere (e.g. 0x40000000) but is made visible at 0x0 through a small alias window.
On QEMU’s vexpress-a9 model, the NOR flash device is mapped at 0x40000000 but aliased at 0x00000000, ensuring the reset vector resides at address 0x0. This is a QEMU modeling choice that mirrors typical early boot behavior.
Vector Table
The vector table defines CPU entry points for exceptions such as reset, undefined instructions, software interrupts, data aborts, IRQ, and FIQ.
Key properties:
- The table resides at a fixed virtual address: 0x00000000 or 0xFFFF0000, depending on mode and SoC configuration.
- Each entry corresponds to an exception type.
- On ARMv7, typically entries are instructions, commonly unconditional branch instructions that jump to full handlers.
- The first entry is the reset vector, which contains the first instruction fetched after reset.
A minimal vector table for experiments may contain only a reset vector, acknowledging that any other exception would lead to undefined behavior. Example:
.section .vectors, "ax"
.global _vectors
_vectors:
b _start @ Reset vector: branch to startup
The instruction at the vector table address branches to _start, which performs the earliest software-controlled action in the system.
How QEMU Wires Reset to Memory
QEMU offers multiple ways to load and boot an ARM image, each influencing reset behavior and initial PC selection:
-
-kernel boot.elf(ELF as kernel image):- QEMU parses the ELF program headers.
- Loadable segments are placed at their specified VMAs.
- The CPU’s initial PC is set to the ELF entry point from the ELF header.
- This bypasses the classic reset-vector mechanism and does not reflect hardware reset routing.
-
-drive if=pflash,format=raw,file=flash.bin(NOR flash image):- QEMU memory-maps the raw binary directly into the emulated NOR flash region.
- On vexpress-a9, NOR flash content is aliased at 0x00000000, so the CPU fetches the reset vector from the binary’s first instruction.
- This faithfully models how a real SoC remaps flash or boot ROM to 0x0 during reset.
This article uses the -pflash method so execution begins naturally at the reset vector located at address 0x0.
Minimal Vector Table Example
A minimal example demonstrating control over the reset vector:
startup.s:
.section .vectors, "ax"
.global _vectors
_vectors:
b _start @ Reset vector: branch to startup
.section .text
.global _start
_start:
b . @ Infinite loop to prove we reached here
linker.ld:
ENTRY(_start)
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64M
RAM (rwx): ORIGIN = 0x60000000, LENGTH = 128M
}
SECTIONS
{
.vectors : {
*(.vectors)
} > FLASH
.text : {
*(.text)
} > FLASH
}
Notes:
-
.vectorsand.textboth live in FLASH. - No
.data,.bss, or RAM usage.
Build steps:
# Assemble the startup code
arm-none-eabi-as -mcpu=cortex-a9 -g startup.s -o startup.o
# Link using the custom linker script
arm-none-eabi-ld -T linker.ld startup.o -o boot.elf
# Convert ELF to raw binary for NOR flash
arm-none-eabi-objcopy -O binary boot.elf flash.bin
# Create a 64 MB NOR flash image
truncate -s 64M flash.bin
Artifacts:
- boot.elf – Contains symbol information useful for GDB debugging.
- flash.bin – Raw memory image that QEMU maps into its NOR flash region.
Verification with QEMU and GDB
Start QEMU:
qemu-system-arm \
-M vexpress-a9 \
-cpu cortex-a9 \
-m 128M \
-nographic \
-drive if=pflash,format=raw,file=flash.bin \
-S \
-gdb tcp::1234
Key points:
- The flash image is mapped into the emulated NOR flash device.
- The alias at 0x00000000 ensures
_vectorsis fetched at reset. -
-Shalts the CPU until GDB connects.
Connect GDB:
gdb-multiarch boot.elf
(gdb) target remote :1234
Remote debugging using :1234
_vectors () at startup.s:4
4 b _start @ Reset vector: branch to startup
Check PC:
(gdb) info registers pc
pc 0x0 0x0 <_vectors>
This confirms that the CPU began executing at the reset vector address.
Disassemble the vector table and startup code
(gdb) disassemble _vectors
Dump of assembler code for function _vectors:
=> 0x00000000 <+0>: b 0x4 <_start>
End of assembler dump.
(gdb) disassemble _start
Dump of assembler code for function _start:
0x00000004 <+0>: b 0x4 <_start>
End of assembler dump.
The first instruction at 0x0 is a branch to _start.
Inspect section placement
arm-none-eabi-objdump -h boot.elf
Excerpt:
Sections:
Idx Name Size VMA LMA File off Algn
0 .vectors 00000004 00000000 00000000 00001000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .text 00000004 00000004 00000004 00001004 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
.vectors is placed exactly at 0x0; .text begins at 0x4.
Step Through the Reset Vector
(gdb) break _start
Breakpoint 1 at 0x4: file startup.s, line 9.
(gdb) continue
Continuing.
Breakpoint 1, _start () at startup.s:9
9 b . @ Infinite loop to prove we reached here
(gdb) info registers pc
pc 0x4 0x4 <_start>
(gdb) stepi
Breakpoint 1, _start () at startup.s:9
9 b . @ Infinite loop to prove we reached here
(gdb) info registers pc
pc 0x4 0x4 <_start>
The b . instruction keeps the PC at _start, proving that:
- The reset vector branch executed correctly.
- The CPU reached
_start. - Execution remains in the infinite loop.
Demonstrating aliasing with the QEMU monitor
Enter the monitor (Ctrl-A, then c):
(qemu) xp /4xw 0x0
0000000000000000: 0xeaffffff 0xeafffffe 0x00000000 0x00000000
(qemu) xp /4xw 0x40000000
0000000040000000: 0xeaffffff 0xeafffffe 0x00000000 0x00000000
Both regions reflect the same underlying flash content, confirming the aliasing behavior used during reset.
Conclusion
The minimal example demonstrates complete control over the system’s first executed instruction by placing a reset vector at address 0x0 and directing it to custom startup code. QEMU’s aliasing of NOR flash provides a convenient environment for experimenting with early boot behavior that closely reflects real hardware.
This foundation forms the basis for building full startup routines: stack setup, memory initialization, exception-vector expansion, and transition into higher-level runtime code. With reset behavior understood and verified, the next steps involve constructing a complete bare-metal initialization sequence.
Top comments (0)