Cogs and Levers A blog full of technical stuff

Writing an ARM Bootloader

Introduction

In this blog post, we’ll dive into the fascinating world of ARM assembly programming by writing and running a basic bootloader. ARM’s dominance in mobile and embedded systems makes it an essential architecture to understand for developers working with low-level programming or optimization.

We’re going to use QEMU, an open-source emulator, we can develop and test our code right on your PC. So we won’t need any hardware (just yet).

What is ARM?

ARM, short for Advanced RISC Machine, is a family of Reduced Instruction Set Computing (RISC) architectures. ARM processors power billions of devices, from smartphones and tablets to embedded systems and IoT devices. Its popularity stems from its energy efficiency and simplicity compared to complex instruction set computing (CISC) architectures like x86.

Why Emulation?

While ARM assembly is usually executed on physical devices, emulation tools like QEMU allow you to:

  • Test code without requiring hardware.
  • Experiment with different ARM-based architectures and peripherals.
  • Debug programs more effectively using tools like GDB.

Supported ARM Hardware

Before we begin coding, let’s take a brief look at some popular ARM-based platforms:

  • Raspberry Pi: A widely used single-board computer.
  • BeagleBone Black: A powerful option for embedded projects.
  • STM32 Microcontrollers: Common in IoT and robotics applications.

Setup

Before we begin, we need to setup our development and build environment. I’m using Manjaro so package names might be slightly different for your distro of choice.

QEMU emulates a variety of hardware architectures, including ARM.

sudo pacman -Ss qemu-system-arm

Now we need to install the ARM toolchain which will include the assembler (as), linker (ld), and other essential tools.

sudo pacman -S arm-none-eabi-gcc binutils

Now you should have everything you need to get going.

Bootloader

Our goal is to write a minimal ARM assembly program that outputs “Hello, World!” via the UART interface.

The Code

Here is the source code for our bootloader, saved as boot.s:

.section .text
.global _start

_start:
    LDR R0, =0x101f1000     @ UART0 base address
    LDR R1, =message        @ Address of the message
    LDR R2, =message_end    @ Address of the end of the message

loop:
    LDRB R3, [R1], #1       @ Load a byte from the message and increment the pointer
    CMP R1, R2              @ Check if we’ve reached the end of the message
    BEQ done                @ If yes, branch to done
    STRB R3, [R0]           @ Output the character to UART0
    B loop                  @ Repeat for the next character

done:
    B done                  @ Infinite loop to prevent execution from going beyond

message:
    .asciz "Hello, World!\n"  @ Null-terminated string
message_end:

Breaking this down line by line, we get:

  • LDR R0, =0x101f1000: Load the memory address of UART0 (used for serial output) into register R0.
  • LDR R1, =message: Load the starting address of the message into R1.
  • LDR R2, =message_end: Load the end address of the message into R2.

After this setup, we move into a loop.

  • Load a byte from the message (R3).
  • Compare R1 (current pointer) with R2 (end of the message).
  • Write the character to UART0 and repeat.

Finally, we finish up with an infinite loop to prevent the program from running into uninitialized memory.

Building

First we need to assemble the code into an object file:

arm-none-eabi-as -o boot.o boot.s

Next, we link the object file to produce an executable:

arm-none-eabi-ld -Ttext=0x10000 -o boot.elf boot.o

The -Ttext=0x10000 flag specifies the memory address where the program will start executing.

Running

We can give our bootloader a go now using the versatilepb machine in QEMU:

qemu-system-arm -M versatilepb -nographic -kernel boot.elf

-nographic here redirects UART ouput to the terminal, so we should see:

Hello, World!

Debugging

If you run into problems with your program, you do have an option to attach gdb for debugging:

qemu-system-arm -M versatilepb -kernel boot.elf -S -gdb tcp::1234

You can then connect to gdb with the following:

arm-none-eabi-gdb boot.elf
target remote :1234

Deployment

For deployment, we’ll use a Raspberry Pi as an example. This process is similar for other ARM-based boards.

Flashing

First, we need to convert the ELF file to a raw binary format suitable for booting:

arm-none-eabi-objcopy -O binary boot.elf boot.bin

Use a tool like dd to write the binary to an SD card:

dd if=boot.bin of=/dev/sdX bs=512 seek=2048

Running

  • Insert the SD card into the board.
  • Power up the device and connect to its UART output (e.g., using a USB-to-serial adapter).
  • You should see “Hello, World!” printed on the serial console.

Conclusion

Congratulations! You’ve successfully written, emulated, and deployed a simple ARM bootloader. Along the way, you learned:

  • How to write and debug ARM assembly.
  • How to use QEMU for emulation.
  • How to deploy code to real hardware.

From here, you can explore more advanced topics like interrupts, timers, or even writing a simple operating system kernel. The journey into ARM assembly has just begun!