How to compile freestanding binary for ARMv6?


The overall goal is to compile a simple kernel for my raspberry pi zero which simply blinks a LED. I have created a C version and I am compiling it with

arm-none-eabi-gcc -O -Wall -nostdlib -nostartfiles -ffreestanding  -march=armv6 -std=gnu99 -c -o blink.o blink.c

I am trying to mirror this in Rust and therefor created a very simple project skeleton:

use core::panic::PanicInfo;

pub extern "C" fn _start() -> ! {
    loop {
        unsafe { asm!("mov r0, r0"); }

fn panic(_info: &PanicInfo) -> ! {
    loop {}

.. which basically does nothing but loop indefinitely.

I added the option panic = "abort" to the Cargo.toml to avoid implementing the eh_personality item, pretty much following the popular blog by Philipp Oppermann.

But I am struggling to get the build done mainly because of the following issues:

  • I am not sure about the target: After some research the target arm-unknown-linux-gnueabihf seems to be the correct one. The problem is I rather need a none target instead of linux but I cannot really find anything else?
  • I managed to compile the raw version above with the following build command:
cargo rustc -- --target=arm-unknown-linux-gnueabihf -C linker=arm-none-eabi-gcc -C link-arg=-nostartfiles

but apparently this is not a good thing to do (I suppose it is fine, when we are actually compiling for the required none target?). Also, the resulting file is different from the one generated by C-version:

file target/debug/rust-blink 
target/debug/rust-blink: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /usr/lib/, with debug_info, not stripped
file blink.elf
blink.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
file blink.o
blink.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped

I am not really sure how to fix this.

Also this whole compilation fails as soon as I add a simple for-loop. This seems to be somewhat unexpected to me, but maybe this is the world of no_std?

I was also trying to skip the linking stage completely but I could not make it work. I was trying to add --emit=obj but cargo invokes the linker anyways.

Any help is much appreciated :slight_smile:

The -none target is the only correct one for bare-metal software, which I suppose you mean with "freestanding". -linux builds a binary for Linux, which needs a Linux kernel to already be running.

Yes, I already figured that. But there does not seem to be a proper target for ARMv6?

Running rustup target list does not seem to include a target which is suited for the raspberry pi zero, but I am lacking experience so maybe I am missing something..

I think you're right. As you say, you'd need the arm-none-eabihf target, which is not available through rustup. I think you need to build it using xargo, with your own spec json file. There's an example here (untried though):

Okay, thanks for that resource. I managed to compile a very basic version, though as soon as the compiler does something fancy (predicts the use of the panic-handler f. e.) the linker fails (even though I declared my own panic handler). If I do for-loops the linker throws undefined reference to __aeabi_memcpy and I am not quite sure how to resolve this.

I also had to use xargo new --lib my-project otherwise I still cannot keep cargo from running the linker and the problem is the same as before. This also means that #[no_main] is not need anymore.

Furthermore this implies the need of a simple boot program (boot.s in the mentioned resource).

If you have any idea to resolve these linker issues please let me know :slight_smile:

FYI, this is the script I used to build:

xargo build --target arm-none-eabihf || exit $?

mkdir -p $BUILD_DIR || :

cp target/arm-none-eabihf/debug/lib*.rlib $BUILD_DIR/kernel.rlib
arm-none-eabi-as --warn --fatal-warnings -mcpu=arm1176jzf-s -march=armv6zk -o $BUILD_DIR/start.o start.s || exit $?

arm-none-eabi-gcc -T linker.ld -march=armv6 -nostartfiles -ffreestanding -O -nostdlib -o $BUILD_DIR/kernel.elf $BUILD_DIR/start.o $BUILD_DIR/kernel.rlib || exit $?

I've always used panic="abort" in bare-metal dev. So I have no idea how to implement a panic handler.

As for the __aeabi_memcpy—LLVM assumes some functions to exist to implement certain base primitives, such as memset and memcpy. I think the compiler-builtins crate is supposed to solve this (they even mention that function specifically).

In baremetal development it is common for there to be "bootstrap" assembler code that puts the CPU and board in an expected state then jumps to the higher-level language code. For example for riscv-rt asm.S. This is what boot.s would contain.

Thanks a lot for all the input, I finally managed to get it to work:

I created a binary package with cargo new --bin my-project (rust nightly, v1.49.0).
I moved the bootstrap code into (just for convenience, it does not do much for now anyways):


use core::panic::PanicInfo;

fn panic(_info: &PanicInfo) -> ! {
    loop {}

pub extern "C" fn _start() -> ! {
    unsafe {
        asm!("mov sp, #0x8000");
    loop {}

I also needed to modify Cargo.toml to include the following:

panic = "abort"

panic = "abort"

I created arm-none-eabihf.json based on the targets arm-unknown-linux-gnueabihf and thumbv6m-none-eabi:

    "llvm-target": "arm-none-eabihf",
    "target-endian": "little",
    "target-pointer-width": "32",
    "target-c-int-width": "32",
    "os": "none",
    "env": "eabi",
    "vendor": "unknown",
    "arch": "arm",
    "panic-strategy": "abort",

    "linker-flavor": "gcc",
    "linker": "arm-none-eabi-gcc",
    "pre-link-args": {
        "gcc": [
    "features": "+strict-align,+v6,+vfp2,-d32",
    "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
    "executables": true,
    "relocation-model": "static",
    "no-compiler-rt": true,
    "unsupported-abis": [

Specifically I adjusted the linker flags..

Then I was able to build my executable with

cargo build \
    --target arm-none-eabihf.json \
    -Z build-std=core,compiler_builtin