This is a minimal bare-metal AArch64 application that demonstrates how U-Boot loads and executes a standalone binary. It's meant to be used in conjunction with U-boot build as a Code-in-Motion project. Which is the reason we see relatevies pre-defined paths here.
The application:
- Runs directly on the hardware (or QEMU) without an operating system
- Outputs "Hello from bare-metal AArch64!" to the UART
| File | Description |
|---|---|
start.S |
Assembly entry point - sets up stack and calls C main |
main.c |
C code that prints the hello message via UART |
linker.ld |
Linker script defining memory layout at 0x40400000 |
Makefile |
Build rules using the aarch64-none-elf toolchain |
boot.cmd |
U-Boot script source (text) |
boot.scr |
U-Boot script binary (auto-generated by mkimage) |
run-hello.sh |
Automated test script using expect |
Simply run:
makeOutput files:
hello-world.elf- ELF binary (for reference)hello-world.bin- Raw binary (loaded by U-Boot)boot.scr- U-Boot script (auto-boot capable)
If you have expect installed:
./hello-world/run-hello.shThis will automatically:
- Start QEMU with U-Boot
- Load
hello-world.binfrom the virtio disk (or use auto-boot) - Execute it using the
gocommand - Show the output
Since boot.scr is generated, U-Boot will auto-boot it:
make qemu-helloWait for U-Boot to scan bootflows - it will find boot.scr and auto-execute it!
-
Start QEMU with the hello-world directory as a virtio disk:
make qemu-hello
-
At the U-Boot prompt (
=>), type:fatload virtio 0:1 0x40400000 hello-world.bin go 0x40400000 -
See the output, then quit with Ctrl+A then X.
With auto-boot (boot.scr):
- QEMU starts:
- U-Boot loads (from u-boot.bin)
- U-Boot scans bootflows
- Finds boot.scr on virtio disk
- Executes script automatically
- fatload + go commands run
- hello-world runs
- Returns to U-Boot
Manual method:
QEMU is started
- U-Boot loads (from u-boot.bin)
- U-Boot waits for commands
- fatload virtio 0:1 0x40400000 hello-world.bin
- Binary loaded from disk to memory at 0x40400000
- go 0x40400000
- U-Boot jumps to the binary
- hello-world runs
- Returns to U-Boot via 'ret' instruction
- 0x40000000: QEMU virt RAM start
- 0x40400000: Hello-world load address (defined in
linker.ld) - 0x40500000: Stack top (grows down toward 0x40400000)
| Command | Purpose |
|---|---|
fatload <dev> <part> <addr> <file> |
Load file from FAT filesystem to memory |
go <addr> |
Jump to and execute code at address |
bootelf <addr> |
Parse and execute ELF file at address (not used here) |
We use the go command with a raw binary because:
- It's simpler - no ELF parsing needed
- It avoids potential issues with 64-bit ELF validation in U-Boot
- It's faster to load (smaller binary)
- For bare-metal learning, it's more explicit about what's happening
The same pattern applies to booting Linux:
- Build Linux kernel → produces
arch/arm64/boot/Image - Load kernel →
fatload virtio 0:1 0x40400000 Image - Load device tree →
fatload virtio 0:1 0x43000000 qemu-arm64.dtb - Boot →
booti 0x40400000 - 0x43000000
The main differences:
- Linux uses
booti(boot image) instead ofgo - Linux needs a device tree blob (DTB)
- Linux may need an initramfs
- Linux doesn't return to U-Boot (it takes over the system)
Q: I get "Unknown command 'go'"
A: Your U-Boot build doesn't have the go command enabled. Check that CONFIG_CMD_GO=y is in your U-Boot config.
Q: Nothing is printed
A: Check that the UART address (0x09000000 in main.c) matches your QEMU machine type. The virt machine uses this address for PL011 UART0.
Q: "Bad Linux ARM64 Image magic!" when using bootelf
A: The bootelf command has stricter requirements. Use the go command with the raw binary instead.