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.

Demystifying the Toolchain Name: aarch64-none-linux-gnu-gcc
Component Example Value Description
Architecture (arch) aarch64 The target CPU architecture. For Raspberry Pi 5, this is 64-bit Arm. For older 32-bit Pis, it would be arm.
Vendor none The supplier or branding of the toolchain. Can be linaro, bootlin, or often none for generic toolchains.
Kernel linux The target operating system kernel. For our purposes, this is always Linux. Could be none-elf for bare-metal development.
C Library (libc) / ABI gnu The C standard library and its Application Binary Interface (ABI). gnu corresponds to glibc. Another common one is musl.
Tool gcc The specific tool from the toolchain suite, such as the GCC compiler (gcc), the linker (ld), or the object dumper (objdump).
  • arch: This specifies the target CPU architecture. For the 64-bit Raspberry Pi 5, this is aarch64. For older 32-bit Pis, it would be arm.
  • vendor: This indicates the provider of the toolchain, such as linarobootlin, or in many generic cases, none.
  • kernel: This defines the target operating system. For our purposes, it is always linux.
  • libc: This specifies the C library and its Application Binary Interface (ABI). The identifier gnu corresponds to the widely used glibc.

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:

Bash
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.

Bash
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.

Bash
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:

Bash
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.

Bash
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.

Bash
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.

Bash
aarch64-none-linux-gnu-gcc --version


You should see output confirming the compiler’s version, target, and configuration:

Plaintext
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.

Bash
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.

Bash
mkdir ~/rpi_projects
cd ~/rpi_projects
nano hello_rpi.c


Enter the following C code into the file:

C
#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.

Bash
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.

Bash
file hello_rpi


The output is the proof of our success:

Plaintext
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.

Bash
# 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.

Bash
ssh pi@<rpi5_ip_address>

  • Once logged in, you’ll find hello_rpi in your home directory. First, make it executable.

Bash
chmod +x hello_rpi

  • Now, run it!

Bash
./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:

Plaintext
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.

Mistake / Issue Symptom(s) Troubleshooting / Solution
PATH Variable Not Set Running aarch64-…-gcc results in:
command not found
  1. Verify current session: Run echo $PATH. Ensure your toolchain’s /bin directory is listed.
  2. Fix current session: If missing, re-run the export PATH=”…” command.
  3. Fix permanently: Ensure the export command is correctly added to your ~/.bashrc or shell startup script.
Running AArch64 Binary on x86 Host On the host PC, running ./hello_rpi results in:
cannot execute binary file: Exec format error
This is not an error! It’s the expected behavior and confirms you have successfully cross-compiled. You must deploy the file to the Raspberry Pi to run it.
Shared Library Mismatch On the Pi, running your program results in an error like:
./my_app: /lib/aarch64-linux-gnu/libc.so.6: version ‘GLIBC_2.34’ not found (required by ./my_app)
  • Easy Fix: Recompile on the host with the -static flag to create a self-contained binary.
  • Debug: Check Pi’s glibc with ldd –version. Compare with your toolchain.
  • Pro Fix: Use a matched toolchain, e.g., one built with Yocto/Buildroot for your specific system.
File Not Executable on Target On the Pi, running ./hello_rpi results in:
bash: ./hello_rpi: Permission denied
The execute permission was lost during transfer. Fix it on the Pi with:
chmod +x hello_rpi
Wrong Toolchain Architecture Linker errors mentioning wrong architecture, or the compiled binary doesn’t run on the 64-bit Pi OS.
  1. Verify Pi Arch: Run uname -m on the Pi. It should be aarch64.
  2. Verify Toolchain: Check your compiler name. It must be aarch64-none-linux-gnu-gcc, not arm-linux-gnueabihf-gcc (32-bit).

Exercises

  1. Extend the System Info Program: Modify the hello_rpi.c program to also print the current user ID and group ID. Use the getuid() and getgid() functions (you’ll need to include <unistd.h>). Re-compile, deploy to your Pi, and verify the output.
  2. 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?
  3. 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.
  4. Automate with a Makefile: For any serious project, you’ll use make to automate builds. Create a Makefile in your project directory with the following content:
Plaintext
# 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 makemake 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.
  • toolchain is a collection of tools including the GCC compilerBinutils (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

  1. 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
  2. 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/
  3. GNU Binutils – Official Documentation: The manual for the assembler, linker, and other essential binary manipulation tools. https://www.gnu.org/software/binutils/manual/
  4. 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
  5. Yocto Project – Official Documentation: The documentation for the industry-standard build system for creating custom embedded Linux distributions. https://docs.yoctoproject.org/
  6. 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/
  7. 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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top