Autotools and Cross Compilation

When working with Linux based embedded systems it is essential to cross compile applications to the required target architecture. It is necessary to understand how packages are built, and how the build can be customized to build applications for Linux based embedded systems. This article will be a starting point for discussing about embedded Linux build systems, like Yocto.

Vagrant Environment

The Vagrant based VM environment for this tutorial can be obtained using the following procedure. The "Try Out" sections can be executed within this Vagrant environment.

  1. Download the Vagrantfile from here.

  2. Run vagrant up in the directory where the Vagrantfile is stored to setup the Vagrant box.

  3. SSH to the Vagrant environment using vagrant ssh.

Autotools

Many Free Software packages are written in C, and are distributed as source pacakges. These need to be downloaded and built as required by the user. In this section, we will look at build related problems and the solution provided by GNU Autotools, which is used in many Free Software packages.

One of the features provided by Autotools, is the ability to detect the OS features, and allow the programmer to take appropriate action if required. Before we look into this capability of Autotools, let’s understand what are the different OS variants, their differences, and how programmer’s would have to deal with these differences.

OS Variants

There are many variant of Unix-like operating systems: Linux, Solaris, FreeBSD, NetBSD, etc. These variants provide the same basic set of commands and system calls. But these commands and system calls differ in minor ways. It is generally hard to write a non-trivial program that compiles and runs on all variants. Some of these OS variantions are listed below:

  • Library function availability: Some OS variants do not have certain functions implemented, like memmove().

  • System Call availability: Some variants do not implement certain systems calls, like mmap().

  • System Call alternatives: Some system calls have alternate implementation in each OS variant, like poll() and select().

If we have to implement a program, that has to support multiple Operating Systems, then a local implementation or reduced mode of operation can be implemented based on the OS. The OS can be identified using OS specific macros.

OS Macro

Linux and Linux-derived

__linux__

Android

__ANDROID__

Darwin (Mac OS X and iOS)

__APPLE__

FreeBSD

__FreeBSD__

Solaris

__sun

The following example shows how a local implementation of a library function can be provided based on the OS macro, for a fictional OS called Tiny BSD, that does not have an implementation of memmove.

#if defined(__TINYBSD__)
void *memmove(...)
{
    ...
    ...
}
#endif

This way only if the OS is Tiny BSD, the memmove will be defined. But what if there are tiny variants of other OS, that also need memmove. The implementation would be as shown below.

#if defined(__TINYBSD__) || defined(__NANOLINUX__) || defined(__USOLARIS__)
void *memmove(...)
{
    ...
    ...
}
#endif

The problem with this approach is that, every time a new Operating System without a feature is identified the source code has to be modified to add that OS to the list. If any of these OS, in a future version add support to the feature, then based on the OS version, the alternative action needs to be taken. And hence this solution is not future proof, and will also make the code a mess of preprocessor checks for OS and their versions.

Feature Macros and Feature Probing

Enter feature macros. Instead of checking what OS, that the application is being built for, we write a feature probing shell script that tests for the availability of various functions required by the program, and generate a config.h file with feature macros for each of those features.

#define HAVE_MEMMOVE
#define HAVE_MEMSET
/* #define HAVE_SELECT */
#define HAVE_POLL

These macros can then be used for taking alternate actions in the program.

#include "config.h"

#ifndef HAVE_MEMMOVE
void *memmove(...)
{
    ...
    ...
}
#endif

This approach is future proof. Any future OS that does not support memmove() is already supported, because the feature probing script would detect the absence of memmove() and would define macros in the config.h appropriately.

But how does the feature probing script identify the presence of a function? The feature probing script generates a tiny program containing an invocation of a function, compiles it and checks for compilation errors. If a compilation error occurs then the shell script concludes that the function is not present. An example C code to test for memmove is shown below.

#include <string.h>
#incldue <stdlib.h>

int main()
{
  char a[2], b[2];
  memmove(a, b, 2);
  return 0;
}

The same technique can be used to identify availability of system calls, availability of types, presence of a member within a structure, presence of a header file, etc.

Feature Probing with Autotools

Feature probing is a very flexible and extensible technique for identifying features in the system. Autotools provides a way of creating the feature probing script, which can check for specific features. The feature probing script generated by Autotools is called configure.

The configure script when executed, creates config.h with the results of the feature probing. The feature probing can also check for the presence of libraries. If feature is specified as optional, configure will continue with macro indicating feature is not available. If a feature is required, and is not preset in the system, configure will halt indicating feature is missing. Let’s try this out with the Bash software package, the default shell in GNU/Linux systems.

Create a workspace first, and set the variable $WORK to the workspace location. We will also create a separate downloads folder and where all downloaded packages will be stored.

Try Out
mkdir -p ~/workspace/dl
mkdir -p ~/workspace/autotools
WORK=~/workspace/autotools
DL=~/workspace/dl

Download Bash from http://ftp.gnu.org/gnu/bash/bash-4.3.tar.gz and extract it.

cd $DL
wget -c http://ftp.gnu.org/gnu/bash/bash-4.3.tar.gz
cd $WORK
tar -x -f $DL/bash-4.3.tar.gz

Change into bash-4.3 folder and run ./configure.

cd $WORK/bash-4.3
./configure

A file called config.h is created, check the contents of config.h. Does it have macro definitions like HAVE_MEMMOVE?

This is basically how Autotools allows the OS level feature differences to be identified and the software package configuration for the build step to be generated. This configuration step is based on OS features is completely automatic, but some configuration might be based on user’s preferences.

Manual Configuration

The manual configuration is an essential step in many large programs, like for example the Linux Kernel. The Linux Kernel has an elaborate menu based interface to disable / enable features required. Many application programs might also need something similar. Examples of features could be, to buid with / without GUI, select between alternate GUI libraries like Qt and GTK+.

Autotools provides a minimialist interface through which such configuration can be specified. These features can be specified as an option to the configure script. Example of options would be --enable-gui=no, --with-gtk=yes, --with-qt=no, etc. These are recorded into config.h and other build files generated by the configure script.

Try Out

Let’s try this out in the bash package. Run the configure command as shown below.

cd $WORK/bash-4.3
./configure --enable-history=no

After configure completes, check if the HISTORY macro defined in config.h. Now try configuring with history enabled, and check for the HISTORY macro.

In this section, we have described why autotools is required, how autotools performs automatic feature probing and configuration, and how manual configuration can be specified. In the next section we will look at cross-compilation of software packages using autotools.

Cross Compilation with Autotools

Programs that use Autotools can be built using the following sequence of commands.

./configure
make
make install

As seen in the previous section, the configure script does automatic feature probing and selection and manual feature selection can be done by passing options to the configure script.

When a software package is built, it is built to be executed in the current system. But if the program has to be executed in a different system, then this needs to be specified as part of the configuration.

Naming Conventions

Autotools refers to the system in which a program is built as the "build" system and the system in which the program is to be executed as the "host" system. This can be confusing to new users, since we are accustomed to referring to the system where the program is to be executed as the "target" system. But the name "target" is reserved in autotools for a different purpose. This is only relavent while building a program like a compiler. When building a compiler, there are three different systems that are involved:

host

the system where the compiler is to be executed.

build

the system where the compiler is being built.

target

the system for which compiler will be creating executable for.

For a compiler, each one of these can be a different system! This explains why the name "target" is not used to refer to the system when program is to be executed.

It is required to specify the "host" and the "build" system type when performing a cross-compilation. The system type is specified using a canonical name, that has the format arch-vendor-kernel-os. Examples of canonical names are listed below:

arm-none-linux-gnu

for ARM systems running GNU/Linux

i686-pc-linux-gnu

for PC systems running GNU/Linux

sparc-sun-solaris

for SPARC systems running Solaris

i686-apple-darwin

for Apple systems running Mac OS X

Cross Compiling Packages

The canonical name also happens to be the prefix for the cross-compiler. So if the host system is specified as arm-none-linux-gnu, then autotools will use the compiler arm-none-linux-gnu-gcc. And hence to cross-compile to ARM based Linux systems the following command sequence can be used.

The cross compiler is installed as part of the package gcc-arm-linux-gnueabi.

./configure --host=arm-linux-gnueabi --build=i686-pc-linux-gnu
make
make install

Let’s try this out with Bash.

cd $WORK/bash-4.3
./configure --host=arm-linux-gnueabi --build=i686-pc-linux-gnu
make

Check the architecture of the binary file using the file command, to confirm that the file has infact been compiled for the ARM architecture.

file bash

Compiling a package for a different architecture is only one part of the problem. There are other things that needs to be taken care of, when building a packge for use on an embedded Linux system. One of this, is the location in which the program files will be installed in the root filesystem.

The entire filesystem in a desktop is under the control of the package manager, like dpkg and rpm, except for /usr/local. Any file installed outside /usr/local is likely to be overwritten by the package manager. And hence, when packages manually compiled, are by default installed in /usr/local. And the program expects to find its data files under /usr/local. When the program wants to access its data file, it does as shown below:

fd = open("/usr/local/share/vlc/icon.png");

The program can be built to reside under /usr, using the --prefix option to the configure script, as shown below.

./configure --prefix=/usr

The configure script creates a PREFIX macro definition in config.h. All static data files are accessed relative to PREFIX.

fd = open(PREFIX "/share/vlc/icon.png");

When building for use in an embedded Linux system, we would like to use /usr as prefix.

Try Out

Let’s check the reference to data files within the bash binary using the following command.

cd $WORK/bash-4.3
strings bash | grep '/usr/local/share'

Reconfigure bash with /usr prefix, and check strings for /usr/share.

cd $WORK/bash-4.3
./configure --host=arm-linux-gnueabi --build=i686-pc-linux-gnu --prefix=/usr
make
strings bash | grep '/usr/share'

Install Directory

After the program has been cross-compiled it needs to be installed into the root filesystem directory of the target system. Generally the installation is done using the make install command. But this command will end up installing the program into the filesystem of the build system. Instead, we can specify a location where the target’s root filesystem is being constructed, as shown below.

make install DESTDIR=/path/to/root
Try Out

Run make install (as non-root), to verify that the program is being is installed in the host’s fielsystem. Run make install with DESTDIR set as show below, and verify the contents of $WORK/rootfs.

make install DESTDIR=$WORK/rootfs
find $WORK/rootfs

Conclusion

This article has shown some of the reasons for using the autotools framework, and how to cross-compile software packages that use the autotools framework. In the next article, we will show how to build a basic root filesystem from scratch.

Resources and Further Reading