Skip to content
ALB Wiki

LFS Cross-Toolchain

This is an old revision of this page — revision 5 (1970-01-21 18:02:21 UTC). It may differ from the current revision.

The cross-toolchain is where Linux From Scratch becomes real: you compile a new compiler — one that runs on your host but builds for a system that does not yet exist. Binutils, then GCC, then glibc, then GCC's C++ library — four packages that together sever the build's dependence on the host's toolchain. When this chapter ends, $LFS/tools holds a working compiler whose binaries know nothing of the host's libraries — the foundation every remaining package is built upon.

What cross-compilation actually is

A native compiler builds binaries for the machine it runs on; a cross compiler builds for a different target — here, the not-yet-existing LFS system, named by the triplet x86_64-lfs-linux-gnu. The mechanism is GNU's three-machine model:

  • build — where the package compiles (your host, x86_64-pc-linux-gnu)
  • host — where the built program will run (the future LFS system)
  • target — what a compiler produces code for (only meaningful for compilers/binutils)

For LFS, host = target = x86_64-lfs-linux-gnu while build = the host. The configure flags --build, --host, --target encode this; config.guess inside every tarball sniffs the build machine automatically. The trick: once gcc-for-LFS exists, every later package compiles with it — the host toolchain retires after this chapter.

The build ritual — every package, always

Every LFS package follows the same liturgy, worth internalizing now:

cd $LFS/sources
tar -xf package-version.tar.xz
cd package-version
# (any required patches / build-dir mkdir / configure)
make
make install            # or DESTDIR=$LFS make install
cd .. && rm -rf package-version

Never build inside the source tree unless the book says to — several packages (gcc especially) require a separate build/ directory; stray artifacts from one configure poison the next. And always delete the tree after installing — the same tarball is re-extracted fresh for later passes. This ritual repeats ~90 times; memorizing it is the job of this chapter.

Package 1 — Binutils pass 1

Binutils — ld, as, ar, nm, objdump, strip — is first because nothing builds without a linker:

cd binutils-*/
mkdir -v build && cd build
../configure --prefix=$LFS/tools \
             --with-sysroot=$LFS \
             --target=$LFS_TGT   \
             --disable-nls       \
             --enable-gprofng=no \
             --disable-werror    \
             --enable-new-dtags  \
             --enable-default-hash-style=gnu
make
make install

Each flag earns its keep: --with-sysroot=$LFS tells the linker where the future system's headers/libraries will live; --target=$LFS_TGT makes it a cross-assembler/linker; --disable-nls skips translations (useless in a temp toolchain); --disable-werror stops host gcc warnings from killing the build; --enable-new-dtags uses RUNPATH over RPATH for better runtime behavior. Time this build — it's your SBU. Every other package's "x SBU" is a multiple of this number.

Package 2 — GCC pass 1

GCC pass 1 is a limited compiler — C only, no C++ library yet, statically linked where possible because it must not depend on host glibc internals:

cd gcc-*/
# gcc needs gmp/mpfr/mpc sources unpacked inside it
tar -xf ../mpfr-*.tar.xz && mv -v mpfr-* mpfr
tar -xf ../gmp-*.tar.xz  && mv -v gmp-*  gmp
tar -xf ../mpc-*.tar.gz  && mv -v mpc-*  mpc

# adjust gcc's default dynamic-linker path to the FUTURE system's
sed -e '/m64=/s/lib64/lib/' -i.orig gcc/config/i386/t-linux64

mkdir -v build && cd build
../configure --target=$LFS_TGT           \
             --prefix=$LFS/tools         \
             --with-glibc-version=2.41   \
             --with-sysroot=$LFS         \
             --with-newlib               \
             --without-headers           \
             --enable-default-pie        \
             --enable-default-ssp        \
             --disable-nls               \
             --disable-shared            \
             --disable-multilib          \
             --disable-threads           \
             --disable-libatomic         \
             --disable-libgomp           \
             --disable-libquadmath       \
             --disable-libssp            \
             --disable-libvtv            \
             --disable-libstdcxx         \
             --enable-languages=c,c++
make
make install
# create the 'full limits' header gcc needs
cd .. && cat gcc/limitx.h gcc/glimits.h gcc/limity.h > \
  `dirname $($LFS_TGT-gcc -print-libgcc-file-name)`/include-fixed/limits.h

The wall of --disable-* flags is the bootstrap bargain: a compiler built without threads, shared libs, or its own runtime can be built without already having them. --with-newlib --without-headers tells gcc there's no libc yet — because there isn't; glibc is next.

Package 3 — Linux API headers

Before glibc can compile, it needs the kernel's API headers — the stable interface every userspace program codes against:

cd linux-*/
make mrproper                    # pristine tree
make headers                     # generate the sanitized headers
find usr/include -type f ! -name '*.h' -delete
cp -rv usr/include $LFS/usr

The kernel's raw headers are unusable — this step produces the sanitized subset userspace links against.

Package 4 — glibc

The C library — the border between userspace and kernel — builds against those fresh headers:

cd glibc-*/
case $(uname -m) in
  i?86) ln -sfv ld-linux.so.2 $LFS/lib/ld-lsb.so.3 ;;
  x86_64) ln -sfv ../lib/ld-linux-x86-64.so.2 $LFS/lib64 ;;
esac
# (apply the book's FHS-compliance patch)
mkdir -v build && cd build
echo "rootsbindir=/usr/sbin" > configparms
../configure --prefix=/usr                      \
             --host=$LFS_TGT                    \
             --build=$(../scripts/config.guess) \
             --enable-kernel=5.4                \
             --with-headers=$LFS/usr/include    \
             --disable-nscd                     \
             libc_cv_slibdir=/usr/lib
make
make DESTDIR=$LFS install
# fix the ldd hardcode (it bakes the build path)
sed '/RTLDLIST=/s@/usr@@g' -i $LFS/usr/bin/ldd

DESTDIR=$LFS — glibc believes it's installing to /usr, but lands in $LFS/usr; inside the chroot, /usr is exactly where it wanted to be. The sanity check after glibc is the most important command in the book:

echo 'int main(){}' | $LFS_TGT-gcc -xc -
readelf -l a.out | grep ': /lib'
# must print: [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
# WRONG answer = stop everything — the toolchain targets the host, not LFS
rm -v a.out

If the interpreter path is the host's (/lib64/ld-linux-x86-64.so.2 exists on the host too — check it's the LFS one being baked via --with-sysroot), the toolchain is wrong and everything after fails. This echo-and-readelf is the build's first real proof of life.

Package 5 — libstdc++

GCC's C++ runtime library, built against the fresh glibc — the last piece of the cross-toolchain:

cd gcc-*/build  # REUSE the pass-1 source tree is wrong — re-extract fresh
# (book: re-enter a fresh gcc tree, build only libstdc++-v3)
cd libstdc++-v3
mkdir -v build && cd build
../configure --prefix=$LFS/tools   \
             --host=$LFS_TGT       \
             --build=$(../config.guess) \
             --disable-multilib    \
             --disable-nls         \
             --disable-libstdcxx-pch \
             --with-gxx-include-dir=/tools/$LFS_TGT/include/c++/<gcc-version>
make
make DESTDIR=$LFS install
rm -v $LFS/usr/lib/lib{stdc++{,exp,fs},supc++}.la   # libtool files break the cross-build

The pass-1 configure lines — the complete recipe

Every cross-toolchain package follows the same grammar — the book's chapter-5 set, with each flag's job:

Binutils pass 1 — the linker/assembler must exist first:

../configure --prefix=$LFS/tools \
             --with-sysroot=$LFS \
             --target=$LFS_TGT \
             --disable-nls \
             --enable-gprofng=no \
             --disable-werror \
             --enable-new-dtags \
             --enable-default-hash-style=gnu

--with-sysroot=$LFS tells it the future root; --target=$LFS_TGT makes it cross; --disable-werror because the host's gcc warns on code it didn't write; --enable-new-dtags uses RUNPATH over RPATH (the correct modern search behavior); --enable-default-hash-style=gnu the modern section format.

GCC pass 1 — the freestanding bootstrap:

tar -xf ../mpfr-*.tar.xz && mv mpfr-* mpfr
tar -xf ../gmp-*.tar.xz  && mv gmp-*  gmp
tar -xf ../mpc-*.tar.gz  && mv mpc-*  mpc
sed -e '/m64=/s/lib64/lib/' -i.orig gcc/config/i386/t-linux64
../configure --target=$LFS_TGT --prefix=$LFS/tools \
    --with-glibc-version=2.42 --with-sysroot=$LFS \
    --with-newlib --without-headers \
    --enable-default-pie --enable-default-ssp \
    --disable-nls --disable-shared --disable-multilib \
    --disable-threads --disable-libatomic --disable-libgomp \
    --disable-libquadmath --disable-libssp --disable-libvtv \
    --disable-libstdcxx --enable-languages=c,c++

The disables are the bootstrap bargain: no threads/shared/multilib because there's no libc to link them against. --enable-default-pie --enable-default-ssp make position-independent code + stack-smashing protection the defaults — modern security baseline baked in.

glibc — built BY the pass-1 compiler FOR the new system:

mkdir -v build && cd build
echo "rootsbindir=/usr/sbin" > configparms
../configure --prefix=/usr --host=$LFS_TGT \
    --build=$(../scripts/config.guess) \
    --enable-kernel=5.4 --with-headers=$LFS/usr/include \
    --disable-nscd libc_cv_slibdir=/usr/lib
make
make DESTDIR=$LFS install
sed '/RTLDLIST=/s@/usr@@g' -i $LFS/usr/bin/ldd

--host=$LFS_TGT — it runs on the new system; --build=$(config.guess) — it's compiled on the host; DESTDIR=$LFS — lands under the staging root believing it's /usr. The configparms and ldd sed are the book's FHS-compliance fix-ups.

libstdc++ — the C++ runtime, extracted from a fresh gcc tree, built only:

cd libstdc++-v3
../configure --prefix=$LFS/tools --host=$LFS_TGT \
    --build=$(../config.guess) --disable-multilib --disable-nls \
    --disable-libstdcxx-pch \
    --with-gxx-include-dir=/tools/$LFS_TGT/include/c++/$(gcc -dumpversion)
make && make DESTDIR=$LFS install
rm -v $LFS/usr/lib/lib{stdc++{,exp,fs},supc++}.la

The .la removal is libtool-archive hygiene — those files bake absolute build paths that poison later links.

What's in $LFS/tools now

A complete cross-environment: x86_64-lfs-linux-gnu-gcc, ld, as, headers, and a glibc-installed $LFS that knows its own name. Every binary there runs on the host but produces code for x86_64-lfs-linux-gnu. The host toolchain's job is done.

Where this chapter goes wrong

SymptomCause
The readelf sanity check shows the host's interpretersed step skipped or $LFS_TGT wrong — STOP and redo gcc pass 1
cannot find -lc / missing headersglibc's --with-headers or DESTDIR wrong
gcc builds host code, not target--target=$LFS_TGT missing or unset — check echo $LFS_TGT
"limits.h" missing errors in later buildsThe limits.h concatenation step skipped
Build ran as rootGo back — files must be lfs-owned or the host is at risk
make fails only in parallelRetry serial — a racy Makefile, not a broken system
Old toolchain binaries used instead of newset +h was missing — bash hashed the old path
configure says "C compiler cannot create executables"glibc/headers step skipped, or $LFS unset in this shell

The bootstrap — why it takes two passes

The two-pass build is the part that confuses everyone; it's worth understanding fully. A compiler is itself a program — it needs to be compiled. Whose compiler builds the new gcc? The host's. But the host's gcc links binaries against the host's glibc — the very thing we're severing from. So:

  • Pass 1 — the host's gcc builds a target-prefixed gcc (x86_64-lfs-linux-gnu-gcc) with --with-newlib --without-headers — a compiler that believes no libc exists yet. It can compile C/C++ but produces freestanding binaries only — perfect for building glibc.
  • glibc — built by pass-1 gcc against the kernel headers — now x86_64-lfs-linux-gnu has a libc.
  • Pass 2 — the host's gcc (or pass-1 gcc) builds a full gcc — threads, shared libs, libstdc++ — this time linked against the new glibc. This is the compiler the rest of the build uses.
  • Chapter 5's native gcc — the final compiler built by pass-2 gcc — the system re-compiling its own compiler, closing the bootstrap.

The chain: host-gcc → cross-gcc → new-glibc → full-cross-gcc → native-gcc. Each generation is built by the previous; each is freer of the host. By the end, every binary on the system descends from the toolchain you built — nothing remains of the host's except history.

Reading the flags — the configure deep-read

The flag walls in the builds aren't arbitrary — each answers a bootstrap question:

FlagAnswers
--target=$LFS_TGT"Build a toolchain that produces code for the new system"
--host=$LFS_TGT"This program will run on the new system" (pass 2)
--build=$(config.guess)"I'm compiling on the host"
--with-sysroot=$LFS"The new system's headers/libs live under $LFS"
--prefix=$LFS/tools"Install into the toolchain staging area"
--disable-nls"Skip translations — a temp toolchain doesn't need i18n"
--disable-werror"Host gcc warnings aren't fatal"
--enable-new-dtags"Use RUNPATH, not RPATH — the correct modern behavior"
--disable-shared"Pass-1 can't link shared — it would need a libc"
--disable-multilib"One ABI only — no 32-bit variants"
--without-headers --with-newlib"No libc exists yet — build freestanding"
--with-glibc-version=X"Tell gcc the future glibc's version — it configures its headers for it"

The configure --help output of binutils/gcc is a document worth reading — hundreds of these flags, each a fork in the toolchain's shape.

The sanity checks — the proofs of life

The book's two verification points are the build's most important commands — learn to read them:

# after glibc — the cross-toolchain check
echo 'int main(){}' | $LFS_TGT-gcc -xc -
readelf -l a.out | grep ': /lib'
# MUST show the LFS interpreter: /lib64/ld-linux-x86-64.so.2 (64-bit)
# — the dynamic linker baked into every binary the toolchain makes.
# If it shows the host's, the --with-sysroot/--target was wrong — STOP.

# and the deeper check — what crt files the linker finds
$LFS_TGT-gcc -print-sysroot      # should print $LFS
$LFS_TGT-gcc -print-file-name=crt1.o   # should resolve inside $LFS/usr/lib

The interpreter path is the whole story: every dynamically-linked binary records which ld-linux will run it. Cross = the LFS path baked in — the binaries literally know which system they belong to before it exists.

The packages in detail — the other nine

Beyond the big four, the cross-toolchain chapter quietly includes several more that make the temp system possible — worth knowing their roles:

  • gmp / mpfr / mpc — gcc's own math dependencies (arbitrary precision) — extracted inside the gcc tree so its build finds them
  • isl / zstd — further gcc/lib deps in some editions
  • m4 / ncurses / bash / coreutils / diffutils / file / findutils / gawk / grep / gzip / make / patch / sed / tar / xz — mostly deferred to the temp system proper (chapter 4), but the toolchain chapter's ordering matters: tools needed before chroot land here, tools needed inside land next

The full list lives in the book's chapter 5 — the point is the mechanism: each package cross-built with the same --build/--host/--target grammar, each landing in /tools, each severed from the host.

Common cross-build failures — the forensic table

SymptomRead
checking for $LFS_TGT-gcc... noPass-1 gcc didn't install — check --prefix and $LFS_TGT
crt1.o: no such fileglibc's DESTDIR wrong — it landed in the wrong root
Binaries link host's /lib--with-sysroot missing — the toolchain doesn't know $LFS
configure: error: cannot compute suffixThe triplet's wrong — x86_64-lfs-linux-gnu spelled wrong or unset
gcc says "no input files" on the sanity testecho | gcc -xc - syntax — the - is stdin
Limits.h errors in later packagesThe limitx.h/glimits.h/limity.h concatenation step was skipped

Exercises

  1. Explain --build, --host, --target for a package that isn't a compiler.
  2. Run config.guess and $LFS_TGT-gcc -dumpmachine; explain the difference.
  3. Corrupt the readelf sanity check deliberately (unset $LFS_TGT) and observe the failure — learn what it looks like.
  4. Read $LFS/tools/bin's contents; identify which are target-prefixed.
  5. Time your binutils build — record your SBU.

Timeline

StepResult
binutils pass 1cross linker/assembler in $LFS/tools (your SBU)
gcc pass 1bootstrap C/C++ compiler, target-prefixed
Linux headersthe API surface for glibc
glibcthe C library, installed under $LFS
libstdc++C++ runtime completes the toolchain
sanity checkreadelf proves the interpreter path
nextLFS II — the temporary system + the chroot

References

  • linuxfromscratch.org — LFS book chapter 5: constructing the cross-toolchain
  • GNU Autoconf manual — the build/host/target model
  • gcc configure documentation — bootstrap and cross builds

See also

Categories: Handbook · LFS · Linux · Linux Handbook
This page was last edited 1970-01-21 18:02:21 UTC by Devin. What links here