LFS Cross-Toolchain
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.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++
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
ln -sfv ../lib/ld-linux-x86-64.so.2 $LFS/lib64/ld-lsb-x86-64.so.3 ;;
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-version>
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
| Symptom | Cause |
|---|---|
| The readelf sanity check shows the host's interpreter | sed step skipped or $LFS_TGT wrong — STOP and redo gcc pass 1 |
cannot find -lc / missing headers | glibc'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 builds | The limits.h concatenation step skipped |
| Build ran as root | Go back — files must be lfs-owned or the host is at risk |
make fails only in parallel | Retry serial — a racy Makefile, not a broken system |
| Old toolchain binaries used instead of new | set +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-gnuhas 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:
| Flag | Answers |
|---|---|
--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
| Symptom | Read |
|---|---|
checking for $LFS_TGT-gcc... no | Pass-1 gcc didn't install — check --prefix and $LFS_TGT |
crt1.o: no such file | glibc'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 suffix | The triplet's wrong — x86_64-lfs-linux-gnu spelled wrong or unset |
| gcc says "no input files" on the sanity test | echo | gcc -xc - syntax — the - is stdin |
| Limits.h errors in later packages | The limitx.h/glimits.h/limity.h concatenation step was skipped |
Exercises
- Explain
--build,--host,--targetfor a package that isn't a compiler. - Run
config.guessand$LFS_TGT-gcc -dumpmachine; explain the difference. - Corrupt the readelf sanity check deliberately (unset
$LFS_TGT) and observe the failure — learn what it looks like. - Read
$LFS/tools/bin's contents; identify which are target-prefixed. - Time your binutils build — record your SBU.
Timeline
| Step | Result |
|---|---|
| binutils pass 1 | cross linker/assembler in $LFS/tools (your SBU) |
| gcc pass 1 | bootstrap C/C++ compiler, target-prefixed |
| Linux headers | the API surface for glibc |
| glibc | the C library, installed under $LFS |
| libstdc++ | C++ runtime completes the toolchain |
| sanity check | readelf proves the interpreter path |
| next | LFS II — the temporary system + the chroot |
References
- linuxfromscratch.org — LFS book chapter 5: constructing the cross-toolchain
- Hosted mirror: /dist/books/ — the official LFS book mirrored on this site (v10.0–13.1, SysV + systemd editions; CC BY-NC-SA 2.0 / MIT, hosted verbatim with attribution)
- GNU Autoconf manual — the build/host/target model
- gcc configure documentation — bootstrap and cross builds
See also
- LFS Preparation — the staging ground this builds on
- Linux — chapter 1
- C toolchain — the compiler's own anatomy