DEV Community

Ripan Deuri
Ripan Deuri

Posted on

My First Bare-Metal Program: From Reset to hello


Bare-metal programming involves running software directly on a processor without an operating system or runtime layer. At reset, the CPU starts execution with minimal state, and all necessary initialisation must be handled by the program itself.

This post presents a minimal bare-metal program that uses an ARMv7 platform under QEMU to show how startup code, a linker script, and a simple C program work together to produce serial output "hello".

How the Pieces Fit Together

A bare-metal program is typically split into three parts, each with a distinct role:

  • startup code to define the first instructions the CPU executes
  • application code written in C
  • linker script to describe how everything is placed in memory

Startup Code

After reset, the CPU has no stack and no notion of a C environment. Execution begins at a symbol named _start. The startup code establishes the minimum state required to run C code and then transfers control.

startup.s

.global _start
_start:
    ldr sp, =stack_top
    bl main
Enter fullscreen mode Exit fullscreen mode

Two things happen here:

  • The stack pointer is set to an address defined by the linker
  • Control jumps to main

From the processor’s point of view, this file defines how execution begins.

Application Code

There is no standard output or console abstraction. Output is produced by writing directly to a UART peripheral using Memory Mapped IO MMIO registers.

main.c

#define UART0_DR   ((volatile unsigned int*)0x10009000)
#define UART0_FR   ((volatile unsigned int*)0x10009018)

void main(void) {
    const char *s = "hello\n";

    while (*s) {
        while (*UART0_FR & (1 << 5));
        *UART0_DR = *s++;
    }

    while (1);
}
Enter fullscreen mode Exit fullscreen mode

The UART registers live at fixed addresses in memory. Writing a value to the data register transmits a character. The flag register indicates whether the transmit FIFO can accept new data.

The program runs indefinitely after printing to avoid falling through into undefined execution.

Linker Script

Without an operating system, memory layout must be defined explicitly. The linker script describes where code lives and where the stack is placed.

linker.ld

ENTRY(_start)

SECTIONS
{
    . = 0x60010000;

    .text : { *(.text) }

    . = ALIGN(8);
    . = . + 0x1000;  /* 4KB stack */
    stack_top = .;
}
Enter fullscreen mode Exit fullscreen mode

This file defines:

  • The program entry point
  • The address where code is loaded
  • A region reserved for the stack

Building and Running on QEMU

The program is built using a bare-metal ARM toolchain and executed on the vexpress-a9 machine model.

arm-none-eabi-as -mcpu=cortex-a9 -o startup.o startup.s
arm-none-eabi-gcc -mcpu=cortex-a9 -c -O2 -nostdlib -nostartfiles -o main.o main.c
arm-none-eabi-ld -T linker.ld -o hello.elf startup.o main.o
arm-none-eabi-objcopy -O binary hello.elf hello.bin
Enter fullscreen mode Exit fullscreen mode

To run:

qemu-system-arm -M vexpress-a9 -m 32M -nographic -kernel hello.bin
Enter fullscreen mode Exit fullscreen mode

The serial output appears directly in the terminal:

hello
Enter fullscreen mode Exit fullscreen mode

Conclusion

This example illustrates the basic mechanics of bare-metal execution: code starts at reset, control is handed to C, and peripherals are accessed directly through MMIO registers. Subsequent posts will explore the startup code, linker script, and application logic in greater depth.

Top comments (0)