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.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
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 |
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
- 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