Systems Programming · Cross Compilation

Build, Host, and Target: Three Machines in One Toolchain

Why a cross build needs three machine names rather than two, which of them selects the compiler, and how a compiler that runs somewhere other than where it was built gets produced.

A single configure invocation can name two different machines and produce a build that is correct for a third. ./configure --build=x86_64-pc-linux-gnu --host=aarch64-linux-gnu tells the script that it is executing on an x86-64 machine and that the program being configured will execute on an AArch64 one. Nothing in that command mentions where the compiler runs, because for an ordinary package the compiler runs on the build machine and that is the only place it needs to run. Change the package to GCC itself and the missing slot becomes a problem: a compiler that produces AArch64 code and must itself execute on AArch64 cannot be compiled by an x86-64-hosted compiler in one step unless the build accepts producing an AArch64 binary with an x86-64 executable.

The three-machine model exists to name all three positions without ambiguity, and the vocabulary is old enough that its terms no longer match their plain-English meanings. --build is the machine running the configure script. --host is the machine the configured package will run on. --target is the machine whose code the package generates, and it applies only to packages that generate code.

Which slot selects the compiler

Autoconf derives the compiler name from --host. With --host=aarch64-linux-gnu and no explicit CC, it searches for aarch64-linux-gnu-gcc in PATH, then aarch64-linux-gnu-cc, and fails if neither is found, unless CC was set in the environment first. That single rule is the entire mechanism behind most successful cross builds: the --host triple selects a prefixed driver, and the prefixed driver carries its own include paths, libraries, linker emulation, and startup files. The other tool variables follow the same convention — AR becomes aarch64-linux-gnu-ar, RANLIB becomes aarch64-linux-gnu-ranlib — and a build that sets CC by hand without setting those two tends to produce a static library that the target ranlib was never asked to index.

--target does nothing for a package that is not a compiler, a linker, a debugger, or a similar code generator. Passing it to a library’s configure script is not an error; it is simply recorded and ignored. Confusing --host with --target in that situation yields a build that compiles with the host’s native compiler and reports success.

The Canadian cross, and why it needs all three slots

Consider producing a GCC that generates ARM code and runs on AArch64 hardware, starting from an x86-64 workstation. The intermediate compiler must run on the x86-64 machine, because that is where the build happens, and generate AArch64 code, because the final compiler will execute there. That intermediate compiler is an ordinary cross compiler: --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=aarch64-linux-gnu. Build it, install it, then use it to configure the final compiler with --build=x86_64-linux-gnu --host=aarch64-linux-gnu --target=arm-linux-gnueabihf. Now all three slots are distinct, and the configure script must not use one machine’s answers for another’s questions.

The Autoconf manual calls this a Canadian cross. The naming is conventional and the mechanics are not: the configure script must decide, for every test, which machine the answer describes. Checking whether the C compiler works is a question about the build machine when the compiler being tested runs there, but a question about the host machine when the answer depends on whether the produced binary can execute. Autoconf resolves this by setting cross_compiling during the compiler check: if the compiler’s output cannot be run, cross_compiling=yes and every AC_TRY_RUN-style test falls back to a compile-only test or to a cached answer.

Where each slot’s answers are used

Four categories of test appear in a configure script, and each belongs to a different machine.

Tests of the build machine’s capabilities are run normally: whether the shell supports $(( )), whether sed accepts a particular expression, whether a header exists that the build tools need. These are answered by executing things and are unaffected by either cross slot.

Tests of the host machine’s C environment are answered by compiling, not running. AC_CHECK_SIZEOF compiles candidate answers into an array bound and keeps the candidate that the compiler accepts, which works without executing anything. AC_CHECK_FUNCS links a program that references the function and inspects the exit status of the link. Both give correct answers for the target as long as the compiler and linker are the right ones, because the compiler’s own knowledge of the target supplies the answer.

Tests that require running a target program have no valid answer. The classic symptom is a configure run that stops with cannot execute binary file, or, worse, one that succeeds because the test binary was an x86-64 executable and the question was about AArch64. Passing --host is what prevents the second outcome: with a prefixed compiler that cannot produce a runnable binary, cross_compiling becomes yes and the tests take their cross branches. The consequences when a package has no cross branch are covered in configure tests that execute.

Tests of the build machine’s tools that are invoked at build time — m4, bison, flex, code generators — must run on the build machine even when a host-prefixed equivalent exists in PATH. This is the failure the three-slot model was invented for. A build that sets CC to a cross compiler and then runs a host-prefixed code generator produces source files generated on the wrong machine, or fails with an exec error, and the resulting Makefile needs separate CC_FOR_BUILD and CC variables.

# CC_FOR_BUILD names a native compiler for programs that must run
# during the build; CC is the cross compiler for target code.
CC_FOR_BUILD=gcc ./configure \
  --build=x86_64-pc-linux-gnu \
  --host=aarch64-linux-gnu

Two slots, one machine

Most cross builds are not Canadian. When the compiler runs on the same machine it was built on, --build and --host are equal and --target names the only foreign machine. That is the ordinary arrangement: an x86-64 workstation runs an aarch64-linux-gnu-gcc binary that was compiled there and that emits AArch64 objects. Such a toolchain is usually described as a cross toolchain for AArch64, and the distinction between “the machine running the compiler” and “the machine the compiler targets” collapses into the one name aarch64-linux-gnu, which is why the triple in the binary’s name is so often read as if it described the compiler’s own architecture.

The collapse is where confusion starts. readelf -h on /usr/bin/aarch64-linux-gnu-gcc reports EM_X86_64, because that program runs on the workstation. The AArch64 part of its name describes its output, not itself. A global cross-compilation variable such as ARCH and CROSS_COMPILE in Kbuild makes the same point differently: the architecture named there is the output architecture, and the compiler prefix is resolved relative to the build machine’s PATH.

Failures that the slots diagnose

Omitting --host is the most common error and the most expensive, because the build proceeds. A configure script that believes it is native runs every test binary, and the binaries it runs are target binaries only if a cross compiler was already selected some other way. Either the tests fail to execute, or, if the target happens to be able to execute them through an emulator registered with the kernel, they succeed and report the emulator’s behaviour as the target’s. The sysroot, the emulator, and the triple must agree with each other; a build that mixes them produces a package configured for an environment that does not exist. The relationship between a triple and the emulator that answers for it is the subject of user-mode emulation and binfmt_misc.

Setting --host correctly but leaving CC pointed at the native compiler is the second. Autoconf honours an explicit CC over the prefix search, so the build compiles for the build machine while every path, every library search, and every runtime assumption is drawn from the host. Header mismatches usually surface late, in a link error naming a symbol the target libc does not export, or not at all when the two libcs happen to share a symbol name.

The third is subtler: --build deliberately set to the host triple because the build “should not depend on the machine it runs on”. Configure scripts do use the build machine for tool checks, for the choice of shell constructs, and for the EXEEXT convention, so a wrong --build produces tests that pass on the wrong basis and a Makefile that invokes tools with the wrong suffixes.

What the three names still do not capture

Build, host, and target describe machines, not environments. Two hosts with the same triple can have different C libraries, different kernel headers, and different CPU feature levels, and configure will treat them as identical. The triple is a key into a toolchain, and the toolchain is what carries the real environment: its sysroot holds the headers and libraries, its driver holds the default flags, and its -print-sysroot reports where those live. A build that records the three triples and not the sysroot path has recorded the names of the machines and not the contents of their libraries.

References

  1. Autoconf manual: Specifying Target Triples — GNU Project
  2. Autoconf manual: Canonicalizing — GNU Project
  3. GCC documentation: Configuring GCC — GNU Project