Using CMake and GCC to Cross-Compile Binaries

8 minute read     Updated:

Rubaiat Hossain %
Rubaiat Hossain

The article provides insights into cross-compiling for ARM64 architectures. Earthly simplifies the process of cross-compilation for those who use CMake. Learn how.

Cross-compilation is the process of compiling your program on a different host than the target system. This enables developers to build binaries for different architectures without using those specific architectures themselves. For example, with cross-compilation, you can compile a binary for ARM-based devices like a Raspberry Pi on your standard x86-64 development machine.

When cross-compiling your software, CMake and the GNU Compiler Collection (GCC) can be helpful. CMake is a robust build system generator that uses configuration files to create cross-compiled binaries, and GCC is a toolchain that includes compilers for various programming languages, including C, C++, Objective C, and Fortran.

In this tutorial, you’ll learn how to build a simple C++ program and then cross-compile it for AArch64 or ARM64-based devices using CMake and GCC.

Building a Simple C++ Program

To start, you need to create a simple C++ program and then build it using Makefiles and GCC. To do this, you need two tools: the GNU make utility and GCC.

Installing GNU Make and GCC

You can install GCC on Debian/Ubuntu using the following command:

sudo apt update && sudo apt install build-essentials

For Red Hat Enterprise Linux (RHEL) and Fedora, use the following command:

sudo dnf groupinstall 'Development Tools'

GNU make comes preinstalled on most Linux distributions. You can check if it’s installed on your machine using the following command:

make --version

If make is not installed, use the following command to install it on Debian/Ubuntu:

sudo apt-get install make

Or for RHEL or Fedora, use this command:

sudo dnf install make

Now that the necessary tools are set up, go ahead and create your C++ program.

Creating and Compiling a C++ Program

Create a new file named hello.cpp and populate it with the following code:

// hello.cpp
#include<iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;

    return 0;
}

This program prints the string Hello, World! onto your console.

Compiling With g++

You can compile the hello.cpp file using g++, the C++ compiler component of GCC:

g++ -o hello hello.cpp

This command generates a new binary executable called hello in your current directory. If you don’t use the -o flag, g++ generates an executable file named a.out instead.

Next, run your program:

./hello

This command should print out the string on your screen:

hello binary

Compiling With Make

Now that you’ve compiled your program with GCC, compile the program with Make, a popular build automation tool that provides granular control over the build process by defining dependencies between files and targets. In doing so, Make can determine the correct order of operations needed to build a project efficiently.

The working procedure of Make revolves around a simple text file called the Makefile. It contains rules that tell Make how to build the project from scratch:

Make diagram

To build your simple C++ program using a Makefile, create a Makefile and populate it with the following:

# Makefile
hello: hello.cpp
    g++ -o hello hello.cpp

clean:
    rm -f hello

This Makefile contains two rules. The first rule specifies that the target is the hello executable and that it depends on hello.cpp. This rule has a single command that uses g++ to compile and link the source file into an executable binary named hello.

The second rule specifies that the target is clean, which uses the rm command to remove the hello executable. It’s important to note that Makefiles use tabs for indentation, not spaces.

Once you verify that your Makefile is formatted correctly, you can run the make command to generate the binary:

make

Make goes through the Makefile and compiles the hello.cpp program using the specified g++ command. Next, delete the generated executable:

make clean

Cross-Compiling With CMake and GCC

Now that you have a simple C++ program running, it’s time to cross-compile it to run on a different architecture using CMake. CMake is a popular tool for managing the build process of C and C++ projects, and it has built-in support for cross-compiling.

Cross-compiling involves configuring CMake to use a cross-compiler and setting the appropriate build settings for the target platform. Once you’ve done that, you can build your project and produce a binary that can be run on the target platform.

To begin, you need to make sure that CMake is installed in your system. If not, you can install it using the following command in Debian/Ubuntu:

sudo apt-get install cmake

Or if you’re working with RHEL or Fedora, use the following command:

sudo dnf install cmake

After you’ve installed CMake, you need to install the following two cross-compilers to produce the target binary for your selected architecture. Here, the selected architecture the target executable runs on is AArch64 and the host that we’ve compiled on thus far is x86-64.

Use the following command to install these compilers on Debian or Ubuntu:

sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install g++-aarch64-linux-gnu

Or use this command if you’re using RHEL or Fedora:

sudo dnf install epel-release
sudo dnf install gcc-aarch64-linux-gnu
sudo dnf install gcc-c++-aarch64-linux-gnu

Before using CMake to cross-compile your program, you need to obtain the prebuilt root file system for the ARM64 target system. This contains the libraries, headers, and other files needed to build and run software on the target system. Here, you’ll use the Ubuntu ARM64 image, but you can download a prebuilt image or build one yourself using a tool like debootstrap.

Download the ISO image to your local machine and extract the content of this image to get the Ubuntu ARM64 root file system:

sudo mkdir /mnt/iso
sudo mount -o loop /path/to/iso /mnt/iso
sudo cp -r /mnt/iso/* /path/to/rootfs/

Substitute /path/to/iso/ with the actual path of the downloaded ISO image. The final copy command extracts the contents of this image to /path/to/rootfs. Make sure you use the exact paths for these on your machine.

After extracting the content of the image, the cross-compilation environment should now be set up. To cross-compile your C++ program with CMake, create a new file called CMakeLists.txt and fill it with the following:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.0)

project(Hello)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++)

set(CMAKE_FIND_ROOT_PATH /path/to/rootfs/)

add_executable(hello hello.cpp)

The first line sets the minimum version of CMake required to build this project to version 3.0. The second line sets the project name to Hello, and the following two lines set the target system name and processor architecture. The target system is Linux, and the target processor is aarch64 (ARM64).

The following two lines set the C and C++ compilers to be used for cross-compiling the code to the target system. Make sure to use the correct path for the cross-compiler based on your system. The second to the last line sets the root directory to /path/to/rootfs/, and finally, you tell CMake to create the hello executable when this project is built.

Save and close this file. Then create a new directory to hold the build files generated by CMake:

mkdir build
cd build

Run CMake using the following command:

cmake ..
CMake cross-compilation

Now, CMake generates the build files, including a Makefile for your project. Use the following code to build the target binary:

make

This creates the binary executable hello in the build directory:

Make for cross-compilation

Testing the Executable

Now that you’ve cross-compiled your C++ program for an ARM64 system, see if it works. To verify that the binary produced by CMake is what you want, run the following command:

file hello
hello binary

This should show that the hello executable is an ELF 64-bit LSB executable based on the ARM AArch64 architecture.

You can test this program in multiple ways: copying it to a 64-bit ARM device like a Raspberry Pi or transferring the binary to an ARM64 virtual machine (VM).

A VM based on ARM64 has been set up on Microsoft Azure to test the binary. You have the option to choose any cloud provider or self-host the VM. However, it’s important to ensure that the file permissions are properly configured:

Cross-compiled hello

As you can see, this cross-compiled hello executable is running as expected on an ARM64 VM.

Troubleshooting Common Issues During Cross-Compiling

Troubleshooting

Even with the help of tools like CMake and GCC, cross-compiling C++ programs can be challenging.

During cross-compiling, common issues you may encounter include compiler incompatibility, lack of readily available libraries, and toolchain conflicts. If you’re facing some of these issues, here are a few tips:

  • Make sure you’ve installed the correct cross-compilation toolchain for your target platform. If you use a different toolchain or an older version, you may encounter compatibility issues.
  • Check that your CMake configuration is correct and make sure you set the right paths and compilers in your CMakeLists.txt file.
  • Ensure you have all the necessary libraries and dependencies for your target platform. Sometimes, specific libraries or dependencies are unavailable for a particular platform, which can cause build errors.
  • Check that your code is compatible with your target platform. For example, if you use assembly code or platform-specific features, you may need to modify your code to make it work on a different platform.
  • Make sure you have the necessary permissions and access rights to write to the build directory and install the binary on your target platform.

Conclusion

This tutorial showed you the basics of cross-compiling a C++ program for ARM64 devices using CMake and GCC. Remember to install the right toolchain, check your CMake setup, make your code compatible with the target platform, and look out for required libraries to avoid build mishaps.

Want to delve deeper into cross-compiling? Check out the official CMake and GCC docs, or explore platform-specific resources and forums. Happy cross-compiling!

And if you are looking for ways to further simplify your cross compile build process, you might want to check out Earthly. It’s a tool that can make cross complication simpler.

If you are looking for a solution to avoid the complexities of Makefile, check out Earthly. Earthly takes the best ideas from Makefile and Dockerfile, and provides understandable and repeatable build scripts, minus the head-scratching parts of the Makefile.
Rubaiat Hossain %
Rubaiat Hossain

Rubaiat is a CS grad with a strong passion for open-source. Apart from being a Unix veteran, he’s also into network security, cryptography, and functional programming. He’s an avid collector of secondhand books and has a never-ending admiration for classic rock.

Writers at Earthly work closely with our talented editors to help them create high quality tutorials. This article was edited by:
Bala Priya C

Bala is a technical writer who enjoys creating long-form content. Her areas of interest include math and programming. She shares her learning with the developer community by authoring tutorials, how-to guides, and more.

Updated:

Published:

Get notified about new articles!
We won't send you spam. Unsubscribe at any time.