Chapter 38: Setting Up a Cross-Compilation Env. for RPi5 on a Host PC
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamental reasons for using a cross-compiler instead of native compilation in embedded systems development.
- Identify the core components of a cross-compilation toolchain, including the compiler, binary utilities, and C standard library.
- Select, download, and install a pre-built AArch64 toolchain suitable for the Raspberry Pi 5 on a Linux host machine.
- Configure the host environment to correctly use the cross-compiler for building applications.
- Successfully cross-compile a C application, deploy it to a Raspberry Pi 5, and execute it on the target hardware.
- Debug common issues related to path configuration and library dependencies.
Introduction
You have likely experienced it as a powerful, desktop-like single-board computer in your journey with the Raspberry Pi 5 so far. You can connect a monitor and keyboard, open a terminal, and use GCC to compile a program directly on the device. This process, known as native compilation, is convenient for simple tasks and initial exploration. However, in the world of professional embedded systems development, this is rarely how products are made. The true power of embedded Linux development is unlocked when you separate the development environment from the target device.
This chapter introduces the cornerstone of professional embedded development: cross-compilation. We will treat your powerful desktop or laptop PC as a “software factory” (the host) to build code for the resource-constrained Raspberry Pi 5 (the target). This approach is not merely a convenience; it is a necessity driven by the demands of efficiency, scale, and complexity. Real-world embedded systems, from automotive infotainment systems to industrial control units and IoT devices, are all built using this methodology. Compiling a complex software stack like the Linux kernel or a large application suite on the target device itself would be impractically slow and, in many cases, impossible due to memory and storage limitations.
In this chapter, we will lay the foundational skills for this professional workflow. We will begin by building a deep, conceptual understanding of what a cross-compiler is and why it’s structured the way it is. Then, we will proceed through a practical, step-by-step guide to obtaining a professional-grade toolchain, configuring it on a Linux host PC, and using it to compile your first C application. Finally, we will transfer this compiled program to your Raspberry Pi 5 and watch it run, completing the full host-target development cycle. This skill is the gateway to more advanced topics, such as custom kernel compilation and building entire bespoke Linux distributions.
Technical Background
The Imperative for Cross-Compilation
To truly grasp the importance of cross-compilation, consider an analogy. Imagine constructing a modern skyscraper. You could, in theory, transport raw materials—steel ore, sand, gravel—to the top floor and fabricate every steel beam and concrete panel on-site using portable tools. The process would be painfully slow, inefficient, and the quality would be constrained by the limited space and equipment available. The professional approach, instead, is to use a massive, dedicated factory. In this factory, components are fabricated with precision and speed using heavy machinery, then transported to the construction site for assembly.
In this analogy, the skyscraper is your complex embedded software project. The slow, on-site fabrication is native compilation on your Raspberry Pi. The well-equipped factory is your host development PC—a machine with gigabytes of RAM, a fast multi-core processor, and vast storage. Cross-compilation is the process of using this powerful host machine to build executable programs for a different machine, one with a completely different CPU architecture. Your x86_64-based PC will be building code that runs on the Raspberry Pi 5’s 64-bit Arm (AArch64) processor.
The practical reasons for this are overwhelming. A full Linux kernel compilation can take hours on a native Raspberry Pi but only minutes on a modern desktop. Furthermore, the host PC provides a rich development ecosystem with advanced editors, debuggers, and version control systems that are simply more powerful and responsive than their on-target counterparts. By offloading the heavy lifting of compilation to the host, the embedded target is free to do what it does best: run the final, optimized application.

Anatomy of a Cross-Compiler Toolchain
The term “toolchain” is fitting because it’s not a single tool but a chain of interconnected programs that work in sequence to turn human-readable source code into a machine-executable binary. When you run a simple command like gcc my_program.c
, you are invoking a driver program that, behind the scenes, calls upon various components of this chain. A cross-compilation toolchain contains specialized versions of these components that understand how to build for the target architecture, not the host.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% graph TD subgraph The Toolchain Process direction TB A[/"<br>Source Code<br><i>(hello_rpi.c)</i>"/] --> B{{"Compiler<br>(aarch64-none-linux-gnu-gcc)"}}; B --> C[/"<br>Assembly Code<br><i>(hello_rpi.s)</i>"/]; C --> D{{"Assembler<br>(aarch64-none-linux-gnu-as)"}}; D --> E["<br>Object Code<br><i>(hello_rpi.o)</i>"]; F[("<br>C Standard Library<br><i>(libc.a / libc.so)</i>")] --> G; E --> G{{"Linker<br>(aarch64-none-linux-gnu-ld)"}}; G --> H["<br><b>Final Executable</b><br><i>(hello_rpi)</i>"]; end %% Styling classDef startNode fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef processNode fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff; classDef systemNode fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff; classDef endNode fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; class A startNode; class C,E startNode; class B,D,G processNode; class F systemNode; class H endNode;
The primary components are the compiler, the binary utilities (binutils), and the C standard library (libc).
The compiler itself, most commonly the GNU Compiler Collection (GCC), is the heart of the process. Its job is to parse high-level source code (like C or C++) and translate it into the low-level assembly language specific to the target’s CPU. A cross-compiler’s gcc
running on an x86 host knows the instruction set, registers, and calling conventions of the AArch64 target. It thinks in Arm, even while running on Intel or AMD hardware.
Next in the chain are the GNU Binutils, a suite of essential programming tools for manipulating binary files. The most critical of these are the assembler and the linker. The assembler (as
) takes the assembly code generated by the compiler and translates it into binary machine code, creating what is known as an object file. The linker (ld
) is the final assembler. It takes one or more object files, stitches them together, and resolves any references to external functions—for example, a call to a function like printf()
. It links your code with the necessary libraries to produce the final, runnable executable file. Other useful binutils include objdump
, to inspect the contents of the binary, and strip
, to remove debugging symbols and shrink the file size, a common practice for resource-constrained embedded systems.
Perhaps the most crucial and often misunderstood component is the C Standard Library (libc
). An application rarely exists in a vacuum; it needs to communicate with the underlying operating system to perform tasks like reading files, allocating memory, or printing to the console. The C library provides the standard API for these operations (fopen()
, malloc()
, printf()
, etc.). It acts as the bridge between your application and the Linux kernel’s system calls. A cross-compiler toolchain must include a version of libc
that has been compiled specifically for the target architecture. When the linker builds your final executable, it links your code against this target-specific library, not the one your host PC uses. This ensures that when your program runs on the Raspberry Pi, its call to printf()
is resolved to the AArch64 machine code that knows how to talk to the AArch64 Linux kernel. The most common libc
in the Linux world is glibc
(GNU C Library), known for its comprehensive feature set, though smaller alternatives like musl
exist for deeply embedded systems.
Demystifying Toolchain Naming Conventions
When you acquire a cross-compiler, you will encounter executables with long, seemingly cryptic names like aarch64-none-linux-gnu-gcc
. This name is not random; it is a standardized tuple that precisely describes the toolchain’s purpose: arch-vendor-kernel-libc
.
arch
: This specifies the target CPU architecture. For the 64-bit Raspberry Pi 5, this isaarch64
. For older 32-bit Pis, it would bearm
.vendor
: This indicates the provider of the toolchain, such aslinaro
,bootlin
, or in many generic cases,none
.kernel
: This defines the target operating system. For our purposes, it is alwayslinux
.libc
: This specifies the C library and its Application Binary Interface (ABI). The identifiergnu
corresponds to the widely usedglibc
.
Understanding this structure instantly demystifies the toolchain. The name aarch64-none-linux-gnu-gcc
tells you that this is the GCC compiler (gcc
) for building applications targeting the aarch64
architecture, running the linux
kernel, and using the glibc
standard library. Every tool in the chain, from the linker (aarch64-none-linux-gnu-ld
) to the object inspector (aarch64-none-linux-gnu-objdump
), will share this prefix, making it clear which toolset you are using.
Sourcing a Toolchain: The Easy Path and the Professional Path
There are two primary ways to get a cross-compilation toolchain: download a pre-built one or build one from scratch using a build system.
For beginners and many application developers, using a pre-built toolchain is the most direct path. Companies like Arm, and communities like Bootlin, provide high-quality, ready-to-use toolchains for various architectures. You simply download an archive, extract it, and add it to your system’s path. The main advantage is simplicity and speed of setup. You can be cross-compiling within minutes. The potential downside is that the toolchain is generic. Its version of glibc
and other components might be slightly different from what’s on your target system. For simple, statically linked applications, this is rarely an issue. For complex applications with many shared library dependencies, it can sometimes lead to version mismatch errors, a problem we will discuss in the troubleshooting section.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% graph TD subgraph "Host PC (x86_64)" A(Developer) --> B[ Arm Developer Website]; B --> C{"Download<br><i>.tar.xz</i> Archive"}; C --> D["Extract to<br><b>/opt/toolchains</b>"]; end subgraph "Cross-Compilation Workflow" D -- "<b>Cross-Compile</b>" --> E((Raspberry Pi 5<br><i>AArch64 Target</i>)); end %% Styling classDef startNode fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff; classDef processNode fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff; classDef endNode fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff; classDef checkNode fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff; class A,B startNode; class C,D processNode; class E endNode;
The second, more advanced method is to use an integrated build system like the Yocto Project or Buildroot. These are incredibly powerful frameworks that do not just build a toolchain; they build your entire custom Linux distribution from source code. You provide them with recipes and configurations, and they automatically download, patch, configure, and compile everything—the bootloader, the kernel, the toolchain, the libraries, and all the user-space applications you need. The result is a perfectly consistent system where the toolchain is guaranteed to be 100% compatible with the target’s root filesystem because it was built as part of it. This eliminates any library versioning problems and provides a fully reproducible build, which is essential for commercial products. While the initial learning curve for Yocto or Buildroot is steeper, it is the standard for professional embedded Linux development.
In this chapter, we will take the first path—using a pre-built toolchain from Arm—as it provides the clearest and most focused introduction to the concepts of cross-compilation itself.
Practical Examples
This section will guide you through the complete, hands-on process of setting up the toolchain and compiling your first cross-platform application.
Host PC Preparation
Before we download the toolchain, we need to ensure your host machine has the basic software development tools installed. We will assume you are using a modern Debian-based Linux distribution like Ubuntu 22.04 LTS.
First, open a terminal on your host PC and update your package lists:
sudo apt update
Next, install the build-essential
package, which includes fundamental tools like make
and a native gcc
compiler, along with other utilities we’ll need.
sudo apt install build-essential libncurses-dev git wget
Obtaining the AArch64 Toolchain
We will use the official Arm A-profile GNU toolchain, which is the successor to the popular Linaro toolchains.
1. Create a Directory for Toolchains: It’s good practice to keep toolchains organized in a central location. We’ll use /opt/toolchains
.
sudo mkdir -p /opt/toolchains
sudo chown $USER:$USER /opt/toolchains
Tip: By changing the ownership of the directory to your current user, you can download and extract the toolchain without needing
sudo
for every step.
2. Download the Toolchain: Navigate to the Arm GNU Toolchain Downloads page. Look for the latest “A-Profile” toolchain for the “AArch64 bare-metal target (aarch64-none-elf)” is not what we want. We need the one for a Linux target. Look for “AArch64 GNU/Linux target (aarch64-none-linux-gnu)”. Download the x86_64
hosted version.As of this writing, a recent version can be downloaded directly. You can use wget
to download it from your terminal. Make sure to copy the link for the latest version from the Arm website. The command will look similar to this:
sudo mkdir -p /opt/toolchains
sudo chown $USER:$USER /opt/toolchains
3. Extract the Toolchain: The downloaded file is a compressed tarball (.tar.xz
). Use the tar
command to extract it.
tar -xf arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-linux-gnu.tar.xz
This will create a new directory containing the toolchain. You can rename it for convenience if you wish.
Configuring the Environment
Your shell needs to know where to find the cross-compiler executables. We do this by adding the toolchain’s bin
directory to your system’s PATH
environment variable.
1. Export the PATH: The full path to the executables is /opt/toolchains/arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-linux-gnu/bin
. You can add this to your PATH
for the current terminal session with the export
command.
export PATH="/opt/toolchains/arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-linux-gnu/bin:$PATH"
2. Verify the Setup: Now, you can test if the shell can find the new compiler.
aarch64-none-linux-gnu-gcc --version
You should see output confirming the compiler’s version, target, and configuration:
aarch64-none-linux-gnu-gcc (Arm GNU Toolchain 13.2.Rel1) 13.2.1 20230929
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Seeing this output confirms your environment is correctly configured for this session.
3. Make the Configuration Permanent: To avoid typing the export
command every time you open a new terminal, you can add it to your shell’s startup script, typically ~/.bashrc
.
echo 'export PATH="/opt/toolchains/arm-gnu-toolchain-13.2.rel1-x86_64-aarch64-none-linux-gnu/bin:$PATH"' >> ~/.bashrc
Now, close and reopen your terminal, or run source ~/.bashrc
. The aarch64-none-linux-gnu-gcc --version
command should now work in any new terminal session.
“Hello, World!” Cross-Compilation
Now for the moment of truth. We will write a simple C program and compile it for the Raspberry Pi 5.
Create the Source File: On your host PC, create a new directory for your project and create a file named hello_rpi.c
.
mkdir ~/rpi_projects
cd ~/rpi_projects
nano hello_rpi.c
Enter the following C code into the file:
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
int main() {
// Create a struct to hold system information
struct utsname sys_info;
// The uname() function gets system information.
// A return value of -1 indicates an error.
if (uname(&sys_info) == -1) {
perror("uname error");
return 1;
}
printf("Hello from the cross-compiled world!\n");
printf("-------------------------------------\n");
printf("System Name: %s\n", sys_info.sysname);
printf("Node Name: %s\n", sys_info.nodename);
printf("Release: %s\n", sys_info.release);
printf("Version: %s\n", sys_info.version);
printf("Machine Arch: %s\n", sys_info.machine);
printf("-------------------------------------\n");
return 0;
}
3. Compile the Code: Use the cross-compiler to build the program. For this first example, we will use the -static
flag.
aarch64-none-linux-gnu-gcc -o hello_rpi hello_rpi.c -static
The -static
flag instructs the linker to bundle all necessary library functions directly into our hello_rpi
executable. This makes the binary larger but self-contained, which cleverly sidesteps any potential mismatches between the C library version used by our toolchain and the one installed on the Raspberry Pi OS. It is an excellent way to guarantee our first program will run.
4. Verify the Binary on the Host: Before moving the file, let’s confirm we built what we expected. Use the file
utility.
file hello_rpi
The output is the proof of our success:
hello_rpi: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, for GNU/Linux 4.2.0, not stripped
This clearly states the file is an ARM aarch64
executable, not an x86 one. If you try to run it on your host PC (./hello_rpi
), you will get an Exec format error
, which is exactly what should happen. You’re trying to run an Arm program on an x86 CPU.
Deploying and Running on the Raspberry Pi 5
The final step is to get our program onto the target and run it.
1. Prerequisites: Ensure your Raspberry Pi 5 is powered on, connected to the same network as your host PC, and has SSH enabled. You will need its IP address. You can find it on the Pi by running hostname -I
.
2. Deploy with scp
: Use the Secure Copy Protocol (scp
) to transfer the compiled binary from your host to the Pi’s home directory.
# Replace <rpi5_ip_address> with the actual IP of your Pi
scp hello_rpi pi@<rpi5_ip_address>:~
Enter the password for the pi
user when prompted.
3. Execute on the Target:
- Use SSH to log into your Raspberry Pi.
ssh pi@<rpi5_ip_address>
- Once logged in, you’ll find
hello_rpi
in your home directory. First, make it executable.
chmod +x hello_rpi
- Now, run it!
./hello_rpi
Y
ou should see the glorious output of your cross-compiled program, executed on the target hardware, reporting details about the Pi itself:
Hello from the cross-compiled world!
-------------------------------------
System Name: Linux
Node Name: raspberrypi
Release: 6.6.20+rpt-rpi-2712
Version: #1 SMP PREEMPT Debian 6.6.20-1+rpt1 (2024-03-07)
Machine Arch: aarch64
-------------------------------------
The Machine Arch: aarch64
line is the final confirmation. You have successfully built a program on one architecture and run it on another, mastering the fundamental workflow of embedded Linux development.
Common Mistakes & Troubleshooting
As you venture into cross-compilation, you may encounter a few common pitfalls. Understanding them in advance can save you hours of frustration.
Exercises
- Extend the System Info Program: Modify the
hello_rpi.c
program to also print the current user ID and group ID. Use thegetuid()
andgetgid()
functions (you’ll need to include<unistd.h>
). Re-compile, deploy to your Pi, and verify the output. - Investigate Binary Size: Take the program from Exercise 1.
- Compile it normally:
aarch64-none-linux-gnu-gcc -o info_dynamic info.c
- Compile it statically:
aarch64-none-linux-gnu-gcc -static -o info_static info.c
- Use the
strip
utility on the static version:aarch64-none-linux-gnu-strip info_static
- Use
ls -lh info_dynamic info_static
to compare the file sizes. Make a note of the significant difference. Why is the static binary so much larger?
- Compile it normally:
- Dynamic Linking Deep Dive: Deploy the
info_dynamic
binary from Exercise 2 to your Raspberry Pi.- Run it. Does it work? If your toolchain’s
glibc
is compatible with the Pi’s, it should. - On the Pi, run the command
ldd info_dynamic
. This will show you which shared libraries the program depends on and where the system found them. This is a critical debugging tool for library issues.
- Run it. Does it work? If your toolchain’s
- Automate with a Makefile: For any serious project, you’ll use
make
to automate builds. Create aMakefile
in your project directory with the following content:
# Define the cross-compiler prefix
CC=aarch64-none-linux-gnu-gcc
TARGET=hello_rpi
# Set the IP address of your Raspberry Pi
PI_IP=192.168.1.100
# Default target
all: $(TARGET)
$(TARGET): $(TARGET).c
$(CC) -o $@ $< -Wall
clean:
rm -f $(TARGET)
deploy: all
scp $(TARGET) pi@$(PI_IP):~
.PHONY: all clean deploy
Replace the PI_IP
with your Pi’s address. Now you can run make
, make clean
, and make deploy
from your host PC.
Summary
- Cross-compilation is the standard professional practice for building software for embedded Linux systems. It involves using a powerful host PC (e.g., x86_64) to compile code for a different architecture target (e.g., AArch64).
- This approach is faster, more efficient, and enables the development of complex software that would be impossible to compile natively on a resource-constrained target.
- A toolchain is a collection of tools including the GCC compiler, Binutils (linker, assembler), and a target-specific C Standard Library (libc).
- Toolchain names follow a standard
arch-vendor-kernel-libc
format that precisely describes their function. - For initial development, using a pre-built toolchain from a vendor like Arm is the most straightforward approach. For production systems, building a custom toolchain with Yocto or Buildroot ensures perfect compatibility.
- Static linking (
-static
) embeds all library dependencies into the executable, creating a portable but larger file. Dynamic linking creates smaller files but requires compatible shared libraries to be present on the target system. - The core workflow involves writing code on the host, cross-compiling, deploying the binary to the target (e.g., with
scp
), and executing it on the target hardware.
You have now acquired one of the most fundamental skills in embedded Linux. The ability to cross-compile opens the door to advanced topics like kernel modification, driver development, and the creation of fully custom Linux systems, which we will explore in subsequent chapters.
Further Reading
- Arm GNU Toolchain Downloads: The official source for the pre-built toolchain used in this chapter. https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
- GCC, the GNU Compiler Collection – Official Documentation: The definitive manual for the compiler itself, covering all command-line options and features. https://gcc.gnu.org/onlinedocs/
- GNU Binutils – Official Documentation: The manual for the assembler, linker, and other essential binary manipulation tools. https://www.gnu.org/software/binutils/manual/
- Buildroot – Official Manual: An excellent resource to read ahead and understand how custom toolchains and filesystems are built from scratch. https://buildroot.org/docs.html
- Yocto Project – Official Documentation: The documentation for the industry-standard build system for creating custom embedded Linux distributions. https://docs.yoctoproject.org/
- Bootlin (formerly Free Electrons) – Embedded Linux Training Materials: An outstanding source of technical presentations and articles on embedded Linux, including toolchains and cross-compilation. https://bootlin.com/docs/
- Raspberry Pi Documentation – The Linux kernel: Official guides from the Raspberry Pi foundation on their kernel and configuration. https://www.raspberrypi.com/documentation/computers/linux_kernel.html