Today, many small computers, such as the Raspberry Pi, are in use despite having only limited resources. Running a compiler on such a computer is often not possible or it takes too much time. Thus, a common requirement for a compiler is to generate code for a different CPU architecture. The whole process of having a host compile an executable for a different target is called cross-compiling.
In cross-compiling, two systems are involved: the host system and the target system. The compiler runs on the host system and produces code for the target system. To denote the systems, the so-called triple is used. This is a configuration string that usually consists of the CPU architecture, the vendor, and the operating system. Furthermore, additional information about the environment is often added to the configuration string. For example, the x86_64-pc-win32 triple is used for a Windows system running on a 64-bit X86 CPU. The CPU architecture is x86_64, pc is a generic vendor, and win32 is the operating system, and all of these pieces are connected by a hyphen. A Linux system running on an ARMv8 CPU uses aarch64-unknown-linux-gnu as the triple, with aarch64 as the CPU architecture. Moreover, the operating system is linux, running a gnu environment. There is no real vendor for a Linux-based system, so this part is unknown. Additionally, parts that are not known or unimportant for a specific purpose are often omitted: the aarch64-linux-gnu triple describes the same Linux system.
Let’s assume your development machine runs Linux on an X86 64-bit CPU and you want to cross-compile to an ARMv8 CPU system running Linux. The host triple is x86_64-linux-gnu and the target triple is aarch64-linux-gnu. Different systems have different characteristics. Thus, your application must be written in a portable fashion; otherwise, complications may arise. Some common pitfalls are as follows:
- Endianness: The order in which multi-byte values are stored in memory can be different.
- Pointer size: The size of a pointer varies with the CPU architecture (usually 16, 32, or 64-bit). The C int type may not be large enough to hold a pointer.
- Type differences: Data types are often closely related to the hardware. The long double type can use 64-bit (ARM), 80-bit (X86), or 128-bit (ARMv8). PowerPC systems may use double-double arithmetic for long double, which gives more precision by using a combination of two 64-bit double values.
If you do not pay attention to these points, then your application can act surprisingly or crash on the target platform, even if it runs perfectly on your host system. The LLVM libraries are tested on different platforms and also contain portable solutions to the aforementioned issues.
For cross-compiling, the following tools are required:
- A compiler that generates code for the target
- A linker capable of generating binaries for the target
- Header files and libraries for the target
Fortunately, the Ubuntu and Debian distributions have packages that support cross-compiling. We’re taking advantage of this in the following setup. The gcc and g++ compilers, the linker, ld, and the libraries are available as precompiled binaries that produce ARMv8 code and executables. The following command installs all of these packages:
$ sudo apt –y install gcc-12-aarch64-linux-gnu \
g++-12-aarch64-linux-gnu binutils-aarch64-linux-gnu \
libstdc++-12-dev-arm64-cross
The new files are installed under the /usr/aarch64-linux-gnu directory. This directory is the (logical) root directory of the target system. It contains the usual bin, lib, and include directories. The cross-compilers (aarch64-linux-gnu-gcc-8 and aarch64-linux-gnu-g++-8) are aware of this directory.