GNAT Cross-Compiler

From OSDev Wiki
Jump to navigation Jump to search
Difficulty level
Difficulty 1.png
Beginner

This article focuses on creating a cross-compiler suitable for cross-compiling Ada code for operating system development.

GNAT is a fully featured Ada compiler maintained by the Free Software Foundation, which forms part of the GCC compiler collection. Additionally, there is a proprietary version of the GNAT compiler maintained by AdaCore. For the remainder of this tutorial, any reference to GNAT will be referring to the FSF version, which is freely available from the Free Software Foundation.

As of the time of writing, GNAT remains the most fully-featured freely available Ada compiler on the market. So this will be used as the basis of the cross-compiler.

Prerequisites

In order to build a GNAT cross-compiler a local installation of both GCC and GNAT is required. Additionally, since GNAT is a front-end to GCC, all of the pre-requisite dependencies necessary for building a GCC cross-compiler are required.

For best results, building a version of GCC/GNAT which is identical to your system GCC/GNAT is recommended. This minimises the chance of incompatibility with the host compiler resulting in errors during the build process.

Installing Build Dependencies

The table below lists the various build dependencies for building GNAT from source. These can either be built from source, or installed via the package manager in a *nix distribution. The name of the package in Debian based Linux distributions is provided for convenience.

Dependency Source Code Apt package (Debian, Ubuntu, Mint, WSL, ...)
GCC N/A build-essential
GNAT N/A gnat
Make N/A build-essential
Bison [1] bison
Flex [2] flex
GMP [3] libgmp3-dev
MPC [4] libmpc-dev
MPFR [5] libmpfr-dev
Texinfo [6] texinfo

Build

The following section of this guide details the build instructions for creating a GNAT cross-compiler. This will take the form of a series of shell commands which perform the individual necessary instructions to complete the build. This form is chosen since it provides easily visualised instructions and can be combined to produce an easily reproducible recipe in the form of a single shell script. A bash compatible shell is assumed, however the instructions should be easily adaptable to any other common shell scripting format. The following section assumes that the reader is building a compiler on an x86_64 system running Linux and targeting the i686-elf architecture.

Preparation

The following sets up the base variables used within our build script. The BUILD_TARGET variable specifies the target triplet for our build. This is a specific string format used by GNU tools to specify the target system for a build. The BUILD_PREFIX variable provides the installation directory prefix for our cross-compiler installation. ${HOME}/opt/cross/ is a common location for installing cross-compilers. Any sensible location can be used here, however sticking to established conventions is recommended for new developers. The HOST variable provides the target triplet that identifies our host system.

#!/usr/bin/env bash

# Recipe for building and installing a GNAT capable GCC cross-compiler from `x86-64-linux-elf`
# to `i686-elf` from source.

# The target triplet for the build.
export BUILD_TARGET="i686-elf"
# The install prefix.
export BUILD_PREFIX="${HOME}/opt/cross/${BUILD_TARGET}"
# The host target triplet.
export HOST="x86_64-pc-linux-gnu"

# Update the PATH variable for this script so it includes the build directory.
# This will add our newly built cross-compiler to the PATH.
# After the initial GCC cross-compiler build this initial cross-compiler 
# will be used to build a version with Ada support.
export PATH="${BUILD_PREFIX}/bin:${PATH}"

# The concurrency to use during the build process.
# Adjust this based upon the capabilities of the host system.
concurrency=8

# The directory where local library dependencies are installed.
# This is used to find installed dependencies such as MPFR, GMP, MPC, etc.
# This is the default location for installations using aptitude.
# Adjust this as necessary for the target system.
local_lib_dir="/usr/local"

This guide assumes that the source for the build prerequisites is stored in the location ${HOME}/src with another directory ${HOME}/src/build used as an intermediate build directory prior to installation. If necessary, these directories can be substituted for others in the scripts below by modifying these variables.

# The directory where the source directories are located.
source_dir="${HOME}/src"
# The directory to use as storage for the intermediate build dirs.
build_dir="${HOME}/src/build"

The below variables define the version of each dependency to build. These correspond to the source directories contained within the parent source directory listed above. These are defined here to facilitate easily changing the versions of the various dependencies in a single place.

# The versions of each dependency to build.
binutils_version="2.32"
gcc_version="8.3.0"

Binutils

The following code details the process for building GNU binutils, which is a required component in the cross-compiler build. Binutils provides utilities such as as, ld, and objdump built for the target architecture.

binutils_dir="binutils-${binutils_version}"

cd "${build_dir}" || exit 1

if [[ ! -d "${build_dir}/${binutils_dir}" ]]; then
	mkdir "${build_dir}/${binutils_dir}" || exit 1
fi

cd "${build_dir}/${binutils_dir}" || exit 1

${source_dir}/${binutils_dir}/configure          \
	--target="${BUILD_TARGET}"               \
	--prefix="${BUILD_PREFIX}"               \
	--host="${HOST}"                         \
	--disable-nls                            \
	--disable-multilib                       \
	--disable-shared                         \
        --with-sysroot || exit 1

# Check the host environment.
make configure-host || exit 1
make -j${concurrency} || exit 1
make -j${concurrency} install || exit 1

Detailed explanations for the selected configuration options can be seen below:

Option Explanation
--target="${BUILD_TARGET}" Sets the target triplet for this cross-compiler build.
--prefix="${BUILD_PREFIX}" Sets the install directory for the final build.
--host="${HOST}" Sets the host architecture for the cross-compiler.
--disable-nls Disables internationalisation, which is not necessary for this build.
--disable-multilib Disables multilib support. Specifies that multiple target variant support, such as bi-endianness support, soft/hard float support, etc, will not be added. Assuming this build is targeting i686-elf it is not required.
--disable-shared Disables building of shared libraries. Since this is a cross-compiler build, these are not necessary.
--with-sysroot Enable sysroot support in the cross-compiler. Using the bare argument here without a directory specified will default to an empty directory. Supplying this argument will allow support for integrating a custom sysroot at a later point.

Additional configuration information can be found by calling the autoconf script with the --help argument.

GCC

In order to successfully create a build of GCC with Ada support, an initial GCC cross-compiler for the target architecture is required. This initial bootstrapping build will be built with only C support. This will be used to bootstrap our final GCC build with GNAT support.

gcc_dir="gcc-${gcc_version}"

cd "${build_dir}" || exit 1

if [[ ! -d "${build_dir}/${gcc_dir}" ]]; then
	mkdir "${build_dir}/${gcc_dir}" || exit 1
fi

cd "${build_dir}/${gcc_dir}" || exit 1

${source_dir}/${gcc_dir}/configure          \
	--target="${BUILD_TARGET}"          \
	--prefix="${BUILD_PREFIX}"          \
	--enable-languages="c"              \
	--disable-multilib                  \
	--disable-shared                    \
	--disable-nls                       \
	--with-gmp=${local_lib_dir}         \
	--with-mpc=${local_lib_dir}         \
	--with-mpfr=${local_lib_dir}        \
	--without-headers || exit 1

make -j${concurrency} all-gcc || exit 1
make -j${concurrency} install-gcc || exit 1

Detailed explanations for the configuration options specific to this step can be seen below:

Option Explanation
--enable-languages="c" In order to bootstrap our GNAT build, only C support is required. This will speed up the cross-compiler build.
--disable-nls Disables internationalisation, which is not necessary for this build.
--disable-multilib Disables multilib support. Specifies that multiple target variant support, such as bi-endianness support, soft/hard float support, etc, will not be added. Assuming this build is targeting i686-elf it is not required.
--disable-shared Disables building of shared libraries. Since this is a cross-compiler build, these are not necessary.
--with-gmp/mpc/mpfr Instructs the build where to find these library dependencies.
--without-headers Tells GCC not use any target headers from a libc when building the cross-compiler.

Additional configuration information can be found here.

GNAT

The code below shows the recipe for the final GCC build which will add Ada support.

gcc_dir="gcc-${gcc_version}"

cd "${build_dir}" || exit 1

if [[ ! -d "${build_dir}/${gcc_dir}" ]]; then
	mkdir "${build_dir}/${gcc_dir}" || exit 1
fi

cd "${build_dir}/${gcc_dir}" || exit 1

${source_dir}/${gcc_dir}/configure          \
	--target="${BUILD_TARGET}"          \
	--prefix="${BUILD_PREFIX}"          \
	--enable-languages="c,c++,ada"      \
	--disable-libada                    \
	--disable-nls                       \
	--disable-threads                   \
	--disable-multilib                  \
	--disable-shared                    \
	--with-gmp=${local_lib_dir}         \
	--with-mpc=${local_lib_dir}         \
	--with-mpfr=${local_lib_dir}        \
	--without-headers || exit 1

make -j${concurrency} all-gcc || exit 1
make -j${concurrency} all-target-libgcc || exit 1
make -j${concurrency} -C gcc cross-gnattools ada.all.cross || exit 1
make -j${concurrency} install-strip-gcc install-target-libgcc || exit 1


Detailed explanations for the configuration options specific to this step be seen below:

Option Explanation
--enable-languages="c,c++,ada" This is where Ada support is added to the compiler. C++ support is required for the build.
--disable-libada Disables building the target system Ada Runtime Library. GCC does not understand how to build a suitable run-time library. As required, this will need to be built manually.

Final Setup

In order to use the newly built cross-compiler its binaries must be visible on the system's PATH. On a Linux system this can be facilitated by adding the following line to the current user's ~/.bashrc file, or the equivalent file on the host system:

# Adjust the following to reflect the location of the installed cross-compiler build.
export PATH="${HOME}/opt/cross/i686-elf:$PATH"

GPRbuild Integration

GPRbuild is a build tool designed for instrumenting the compilation of GNAT projects. It is written and maintained by AdaCore and is installed alongside their GNAT compiler or can be built from source. Its source code is publicly available on AdaCore's Github profile. Its function is analogous to that of GNU Make. It is capable of compiling projects featuring a variety of source languages such as Ada, C and Assembly, among others. Modern versions of GNAT do not natively feature the capability to compile GNAT project files, and will require the use of GPRbuild.

GPRbuild will not automatically recognise the existence of the new cross-compiler toolchain. Additional steps are necessary before GPRbuild will recognise our target architecture as being valid. This is accomplished by adding configuration information about the newly built toolchain to GPRbuild's 'knowledge base' linker configuration file. This file instructs GPRbuild how to link executables and libraries using the various toolchains supported by the host system. If GPRbuild was installed as part of an AdaCore GNAT installation, this configuration file will be located at ${prefix}/share/gprconfig/linker.xml, where ${prefix} is the location of the GNAT install directory. The easiest way to add a new target architecture is to duplicate an existing toolchain's target configuration. Inside the linker configuration file, search for an existing configuration such as leon-elf, and duplicate each entry for the existing configuration. modifying it to reflect the newly built i686-elf, or similar, target.

Note: GPRbuild uses regular expressions to pattern match the target triplets.

A truncated example of the new configuration entries can be seen below:

  <configuration>
    <targets>
       <!-- ... -->
       <target name="^i686-elf$" />
    </targets>
    <config>
   for Library_Support  use "static_only";
   for Library_Builder  use "${GPRCONFIG_PREFIX}libexec/gprbuild/gprlib";
    </config>
  </configuration>

  <!-- ... -->

  <configuration>
    <targets>
      <target name="^i686-elf$" />
    </targets>
    <config>
   for Archive_Builder  use ("i686-elf-ar", "cr");
   for Archive_Builder_Append_Option use ("q");
   for Archive_Indexer  use ("i686-elf-ranlib");
   for Archive_Suffix   use ".a";
    </config>
  </configuration>

Once each entry has been duplicated and modified to reference the newly built toolchain, paying special attention to preserve any regex special characters, the newly built toolchain should be visible to GPRbuild.

Additional information on GPRbuild configuration, and GPRbuild's toolchain knowledge base, can be found here: [7]