Skip to content
ALB Wiki

Diff — LFS Cross-Toolchain

Comparing rev 6 → rev 7

{{infobox
| title = LFS: The Cross-Toolchain
| chapter = Linux Handbook, chapter 3 — binutils → gcc → glibc → libstdc++
| stage | Chapter 5 of the LFS book — the compiler that builds for the future
| output = $LFS/tools — a toolchain that targets x86_64-lfs-linux-gnu
| hardest_idea = Bootstrapping: building a compiler that builds compilers
| needs = Chapter 2 complete — partition, sources, lfs user, sterile env
| next = LFS II — the temporary system and the chroot
}}
**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:
```sh
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:
```sh
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:
```sh
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:
```sh
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:
```sh
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:**
```sh
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:
```sh
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:
```sh
../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:
```sh
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:
```sh
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:
```sh
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-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:
| 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:
```sh
# 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
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
| 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/](/dist/books/index.html) — 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
[[Category:Linux]]
[[Category:LFS]]
[[Category:Handbook]]