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.
Including External Libraries in CMake Projects
Learn how to use CMake’s FetchContent module to automatically download and integrate libraries like CMSIS into your embedded projects, eliminating the hassle of manual copying and updates.
How to Find the Memory Address of a Symbol in an ELF File
A quick and useful tip for locating symbol memory addresses in ELF files using arm-none-eabi-nm combined with grep—perfect for embedded debugging scenarios like setting up SEGGER RTT or inspecting linker placements and runtime symbols.
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.
Whether you're building something new, fixing stability issues, or automating what slows your team down — we can help.