How I Finally Cross-Compiled Rust App for Pi Zero (After Many Fails)

A debugging story of segmentation faults, ARM quirks, and Docker containers or how to compile a Rust application for the Pi Zero.

Andre Leppik

Introduction

I recently decided to create a simple TFT driver in Rust for my pwnagotchi running on Pi Zero, which had been gathering dust on my desk. The plan was to use the Pi Zero's SPI interface, write a Rust driver, and control it from a Python application. It seemed like a great way to explore a new setup and fill in some knowledge gaps.

But before diving into the driver, I figured I should start small and cross-compile a "Hello, World!" program on my PC and run it on the Pi Zero. How hard could it be?

It was not, in fact very easy

Hello, World

The Pi Zero is a 32-bit ARMv6 device running Linux, so I needed the arm-none-linux-gnueabihf toolchain (the "hf" stands for hard float, which the Pi Zero uses). The latest version at the time of writing is 14.3 rel1.

After installing the toolchain, I set up a basic Rust project and configured .cargo/config.toml to specify the target and linker:

[target.arm-unknown-linux-gnueabihf]
linker = "arm-none-linux-gnueabihf-gcc"

[build]
target = "arm-unknown-linux-gnueabihf"

Next, I wrote the simplest possible "Hello, World!" Rust app:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    println!("Hello, World!");
    Ok(())
}

I built the project with:

cargo build --release --target arm-unknown-linux-gnueabihf

Then, I copied the cross-compiled binary to my Pi Zero and ran it, expecting to see the glorious "Hello, World!" output.

Instead, I got:

Segmentation Fault

Whaaat...? Why...?

Down the Rabbit Hole

I started messing around with different rustc flags, trying -march=armv6 and even -mcpu=arm1176jzf-s (the Pi Zero’s CPU). Some builds resulted in "Illegal instruction" errors, which made me suspect I was targeting the wrong architecture.

Note

Let me tell you, this part doesn’t capture the hours of Googling, forum-diving, and Stack Overflow I fell into... It was painful (╥﹏╥).

To rule out Rust doing something weird, I created an equivalent C program and compiled it with the same toolchain. Same result: "Segmentation fault".

I checked the binary’s attributes with:

$ arm-none-linux-gnueabihf-readelf -A hello
Attribute Section: aeabi
File Attributes
  Tag_CPU_name: "7-A"
  Tag_CPU_arch: v7

Ah! The target architecture was still ARMv7, even though I specified ARMv6 with -march=armv6. It turns out that ARMv7 is the baseline for most Debian-based ARM Linux distributions, and the Pi Zero’s ARMv6 is the odd one out.

I tried installing older toolchain versions (v4.9, v7.5, v9.2) that rumors on the internet suggested might support ARMv6 with -march=armv6. No luck.

I could compile the toolchain myself with ARMv6 support, but that seemed like overkill. Plus, most pre-built ARMv6 toolchains I found didn’t include Windows versions, which I needed.

Containers to the Rescue!

During my research, I stumbled upon cross, a tool for cross-compiling Rust in Docker containers. Perfect for cases where the mainline toolchain doesn’t support your target.

I installed cross:

cargo install cross

Then, I created a Cross.toml file in my project directory with the following configuration:

[target.arm-unknown-linux-gnueabihf]
image = "ghcr.io/cross-rs/arm-unknown-linux-gnueabihf:latest"
rustflags = [
  "-C", "target-feature=-vfp2,-vfp3,-vfp4",   # Pi Zero W doesn't have VFPv3+
]

[build]
target = "arm-unknown-linux-gnueabihf"

The Dockerfile for this image is available here.

Now, I could build my project using cross just like I would with cargo:

cross build --release --target arm-unknown-linux-gnueabihf

This time, the build succeeded! I double-checked the binary’s attributes:

$ arm-none-linux-gnueabihf-readelf -A hello
Attribute Section: aeabi
File Attributes
  Tag_CPU_name: "6"
  Tag_CPU_arch: v6

Success! The binary was now correctly compiled for ARMv6. I copied it to the Pi Zero and ran it:

./hello
Hello, World!

Finally!

It Working Cartman

Wrap-Up

So that’s how I failed, then succeeded at cross-compiling Rust for the Pi Zero. Maybe there are other ways to compile for ARMv6, but this method worked for me.

If you have more insights or alternative approaches, let me know! I’d love to hear about them! I will be revisiting cross-compiling C applications for the Pi Zero and even experimenting with building a custom Pi Zero kernel.

Need help with embedded systems development?

Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.