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.
-
Download the
Vagrantfile
from /static/code/autotools/Vagrantfile[here]. -
Run
vagrant up
in the directory where theVagrantfile
is stored to setup the Vagrant box. -
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()
andselect()
.
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 |
|
Android |
|
Darwin (Mac OS X and iOS) |
|
FreeBSD |
|
Solaris |
|
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.
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.
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.
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
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
-
The OS macros for each operating system was obtained from http://nadeausoftware.com/articles/2012/01/c_c_tip_how_use_compiler_predefined_macros_detect_operating_system
-
Autotools Mythbuster: https://autotools.io/
-
The GNU Autoobook Autoconf, Automake, and Libtool. URL: http://sources.redhat.com/autobook/autobook/autobook_13.html