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.
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?
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:
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.
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!
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.
How to Use a Raspberry Pi as a Remote OpenOCD Debugger
Learn how to turn a Raspberry Pi into a remote debugging server for the RP2040 using OpenOCD, a complete with step-by-step setup and instructions for building OpenOCD from source to resolve hardware compatibility issues.
Cortex-M0 Profiling: How to Trace Without Hardware Support
The ARM Cortex-M0 and M0+ lack hardware tracing features like SWO, ETM, and ITM, so how do you profile code on them? In this post, I explore software-based techniques to get deeper insight into performance and debugging on these resource-constrained MCUs.
Tools as a Service: A Modern Approach to Embedded DevOps
Ditch dependency hell and manual setups. Learn how to build the Tools as a Service stack, a container-first architecture for scalable embedded development.
Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.