Skip to content
ALB Wiki

LFS Cross-Toolchain

This is an old revision of this page — revision 1 (2026-10-25 14:48:41 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

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

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 2026-10-25 14:48:41 UTC by ALB Team. What links here