Part 4 — Building your own Linux OS
4.1 Step zero: a minimal bootable Linux in about an hour (kernel + BusyBox + QEMU)
Do this before LFS. It shows you the irreducible core: a kernel, an initramfs and one statically linked binary acting as PID 1. With those three pieces you have a working Linux system.
# Host packages (Debian/Ubuntu names): build-essential flex bison bc libelf-dev libssl-dev
# qemu-system-x86 cpio
mkdir -p ~/minilinux && cd ~/minilinux
# 1. Kernel (use the minimal recipe from Part 3.5, or simply defconfig)
curl -LO https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.3.tar.xz
tar xf linux-7.3.tar.xz
make -C linux-7.3 O=$PWD/kbuild defconfig
make -C linux-7.3 O=$PWD/kbuild -j"$(nproc)" bzImage
# 2. BusyBox, statically linked
curl -LO https://busybox.net/downloads/busybox-1.38.0.tar.bz2
tar xf busybox-1.38.0.tar.bz2
cd busybox-1.38.0
make defconfig
sed -i 's/^# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
# If the build fails in networking/tc.c against new kernel headers, disable tc:
sed -i 's/^CONFIG_TC=y/# CONFIG_TC is not set/' .config
make -j"$(nproc)"
make CONFIG_PREFIX=$HOME/minilinux/rootfs install
cd ..
# 3. The initramfs: directory skeleton + /init
cd rootfs
mkdir -p proc sys dev tmp etc
cat > init <<'EOF'
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
echo
echo "Booted $(uname -sr) in $(cut -d' ' -f1 /proc/uptime)s — PID $$"
echo "Try: ps, cat /proc/cpuinfo, ls /sys/bus, dmesg | head"
exec setsid cttyhack /bin/sh
EOF
chmod +x init
find . -print0 | cpio --null -o -H newc | gzip -9 > ../initramfs.cpio.gz
cd ..
# 4. Boot it
qemu-system-x86_64 -m 512M -nographic -enable-kvm \
-kernel kbuild/arch/x86/boot/bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0 rdinit=/init"
# Exit QEMU: Ctrl-A then X
What you just exercised:
- The bootloader role is played by QEMU’s
-kernelloader. - The kernel unpacked a
newccpio into rootfs and ranrdinit=/initas PID 1. /initmounted the pseudo-filesystems that every later tool depends on.- Then it
exec’d a shell. Becauseexecreplaces the process, the shell is now PID 1. If it exits, you will see the “Attempted to kill init!” panic from Part 2.2.
Run ls -l /bin and you will see that every command is a symlink to one BusyBox binary. That is a whole userland in about 1–2 MiB.
Next exercises:
- Add a virtio disk (
-drive file=disk.img,if=virtio) and an ext4 image. Make/initmount it andswitch_rootinto it. - Add
-netdev user,id=n0 -device virtio-net,netdev=n0and runudhcpc. - Rebuild the kernel from
tinyconfigand count how many options you actually need.
4.2 Linux From Scratch: the full method
LFS 13.1-systemd (published 1 September 2026, managing editor Bruce Dubbs) is the current release; it updated or added 46 packages across more than 200 commits since the previous stable release. Since LFS 13.0 (March 2026), the book is systemd-only. The SysV edition is frozen at 12.4 and no longer developed. Core toolchain versions in 13.1: binutils 2.47, GCC 16.2.0, glibc 2.44, Linux 7.1.8 (API headers and kernel), GRUB 2.14, systemd 261.2.
LFS is built around one idea: build a toolchain isolated from the host, use it to build a minimal temporary system, chroot into that, and build the final system using only itself. Each stage reduces how much of the host leaks into the result.
4.2.1 Host prerequisites
The book’s hardware recommendation is at least 4 CPU cores and 8 GB of RAM. Minimum host software includes bash 3.2, binutils 2.13.1, bison 2.7, coreutils 8.1, gawk 4.0.1, GCC 5.4 with g++, make 4.0, Python 3.4, texinfo 5.0, xz 5.0.0 and a Linux 5.10 or later host kernel with UNIX98 PTY support.37 /bin/sh must be bash, awk must be gawk and yacc must be bison.
There is also an upper bound: GCC newer than 16.2.0 or binutils newer than 2.47 is untested with this release and may break the build. A rolling-release host might be too new. The book provides a version-check.sh script, so run it first.
4.2.2 Partitioning, the filesystem, and $LFS
# As root on the host. Example: /dev/sdb is a dedicated disk.
fdisk /dev/sdb # create e.g. sdb1 (ESP, 512M, if UEFI), sdb2 (root, ≥30G), sdb3 (swap)
mkfs -v -t ext4 /dev/sdb2
mkswap /dev/sdb3
export LFS=/mnt/lfs
umask 022
mkdir -pv $LFS
mount -v -t ext4 /dev/sdb2 $LFS
chown root:root $LFS && chmod 755 $LFS
/sbin/swapon -v /dev/sdb3
The book says about 10 GB is enough for a minimal build and suggests around 30 GB to leave room to grow.38 $LFS must be set in every shell you use, including root’s. A mistyped or unset $LFS combined with make install writes into your host’s /usr. That is the classic LFS disaster.
4.2.3 The lfs user and a sterile environment
Build as an unprivileged lfs user whose environment is wiped clean. That way host settings (aliases, CFLAGS, a host config.site) cannot leak in, and a mistake cannot overwrite host files.
# ~lfs/.bash_profile — replace the login shell with a clean one
cat > ~/.bash_profile << "EOF"
exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash
EOF
# ~lfs/.bashrc
cat > ~/.bashrc << "EOF"
set +h
umask 022
LFS=/mnt/lfs
LC_ALL=POSIX
LFS_TGT=$(uname -m)-lfs-linux-gnu
PATH=/usr/bin
if [ ! -L /bin ]; then PATH=/bin:$PATH; fi
PATH=$LFS/tools/bin:$PATH
CONFIG_SITE=$LFS/usr/share/config.site
export LFS LC_ALL LFS_TGT PATH CONFIG_SITE
EOF
cat >> ~/.bashrc << "EOF"
export MAKEFLAGS=-j$(nproc)
EOF
source ~/.bash_profile
Why each line is there:
set +hturns off bash’s command hashing, so a newly built tool in$LFS/tools/binis used as soon as it exists.LFS_TGT=x86_64-lfs-linux-gnuis a fake vendor triplet. Because it differs from the host’sx86_64-pc-linux-gnu, every configure script believes it is cross-compiling. That forces true isolation even though host and target are the same architecture.CONFIG_SITEstops the host’s site defaults from being picked up.
The book defines one SBU (Standard Build Unit) as the time to build binutils pass 1 on one core. All other build times are given in SBUs measured with four cores.
4.2.4 Chapter 5: the cross-toolchain
The build order is dictated by dependencies. Each package needs the previous ones:
binutils pass 1 → gcc pass 1 (C/C++ only, no libc) → Linux API headers → glibc → libstdc++
Binutils pass 1. Build the cross assembler and linker first, because GCC and glibc both probe the linker and assembler to decide which features to enable:
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
GCC pass 1. This is a cross compiler with no libc yet, so it is built as a “freestanding” compiler. --with-newlib --without-headers stops it from depending on libc headers that do not exist. All runtime libraries that need libc are disabled. GMP, MPFR and MPC are unpacked into the source tree so they are built along with 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
mkdir -v build && cd build
../configure \
--target=$LFS_TGT \
--prefix=$LFS/tools \
--with-glibc-version=2.44 \
--with-sysroot=$LFS \
--with-newlib \
--without-headers \
--enable-default-pie \
--enable-default-ssp \
--disable-fixincludes \
--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
--enable-default-pie and --enable-default-ssp make every program built later a position-independent executable with stack protection by default. That is the toolchain applying security policy to everything it builds.
Linux API headers. glibc needs the kernel’s userspace ABI headers (include/uapi) for syscall numbers, structure layouts and ioctl definitions:
make mrproper
make headers
find usr/include -type f ! -name '*.h' -delete
cp -rv usr/include $LFS/usr
Glibc. This is the first real target library, cross-compiled with --host=$LFS_TGT. --enable-kernel=5.10 drops compatibility code for older kernels.
case $(uname -m) in
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
patch -Np1 -i ../glibc-fhs-1.patch
mkdir -v build && cd build
echo "rootsbindir=/usr/sbin" > configparms
../configure \
--prefix=/usr \
--host=$LFS_TGT \
--build=$(../scripts/config.guess) \
--disable-nscd \
libc_cv_slibdir=/usr/lib \
--enable-kernel=5.10
make
make DESTDIR=$LFS install
sed '/RTLDLIST=/s@/usr@@g' -i $LFS/usr/bin/ldd
Then the sanity check, which you must not skip. Compile an empty program and confirm that the toolchain is using $LFS, not the host:
echo 'int main(){}' | $LFS_TGT-gcc -x c - -v -Wl,--verbose &> dummy.log
$LFS_TGT-readelf -l a.out | grep ': /lib'
# [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
grep -E -o "$LFS/lib.*/S?crt[1in].*succeeded" dummy.log
grep "/lib.*/libc.so.6 " dummy.log # must be /mnt/lfs/usr/lib/libc.so.6
grep found dummy.log # ld-linux found under /mnt/lfs
rm -v a.out dummy.log
Libstdc++. Now that libc exists, GCC’s C++ runtime can be built against it.
4.2.5 Chapter 6: cross-compiling temporary tools
Using the cross toolchain, build a minimal set of tools into $LFS: M4, Ncurses, Bash, Coreutils, Diffutils, File, Findutils, Gawk, Grep, Gzip, Make, Patch, Sed, Tar and Xz, followed by binutils pass 2 and GCC pass 2. These tools are cross-built, so they cannot accidentally link against host libraries. Pass 2 produces a native compiler for the target that will run inside the chroot.
4.2.6 Chapter 7: entering the chroot
# As root: hand ownership to root, prepare virtual kernel filesystems
chown --from lfs -R root:root $LFS/{usr,lib,var,etc,bin,sbin,tools}
case $(uname -m) in x86_64) chown --from lfs -R root:root $LFS/lib64 ;; esac
mkdir -pv $LFS/{dev,proc,sys,run}
mount -v --bind /dev $LFS/dev
mount -vt devpts devpts -o gid=5,mode=0620 $LFS/dev/pts
mount -vt proc proc $LFS/proc
mount -vt sysfs sysfs $LFS/sys
mount -vt tmpfs tmpfs $LFS/run
if [ -h $LFS/dev/shm ]; then
install -v -d -m 1777 $LFS$(realpath /dev/shm)
else
mount -vt tmpfs -o nosuid,nodev tmpfs $LFS/dev/shm
fi
chroot "$LFS" /usr/bin/env -i \
HOME=/root \
TERM="$TERM" \
PS1='(lfs chroot) \u:\w\$ ' \
PATH=/usr/bin:/usr/sbin \
MAKEFLAGS="-j$(nproc)" \
TESTSUITEFLAGS="-j$(nproc)" \
/bin/bash --login
This is where the kernel concepts from Part 2 become concrete. The chroot shares the host kernel: you bind-mount /dev and mount procfs, sysfs and tmpfs because the tools inside need the kernel’s pseudo-filesystems. chroot changes only the path-resolution root. It is not a security boundary, and it is a weaker relative of the mount namespaces from Part 2.4. Inside, create the FHS directory tree and the essential files (/etc/passwd, /etc/group, /var/log/wtmp), then build Gettext, Bison, Perl, Python, Texinfo and Util-linux. Before Chapter 8, the book recommends unmounting and backing up the temporary system as a tarball.
4.2.7 Chapter 8: the final system
Chapter 8 builds roughly 80 packages natively, in dependency order: man-pages, Iana-Etc, glibc (final), the compression libraries, readline, m4, bc, flex, the Tcl/Expect/DejaGNU test harness, pkgconf, binutils (final), GMP/MPFR/MPC, attr/acl/libcap, libxcrypt, shadow, GCC (final), ncurses, sed, bash, perl, autotools, OpenSSL, libelf, Python, Meson/Ninja, kmod, coreutils, GRUB, iproute2, kbd, vim, systemd (with udev), D-Bus, procps-ng, util-linux and e2fsprogs.
Run the test suites for glibc, binutils and GCC. They are the evidence that your toolchain works. The chapter ends with stripping debug symbols and cleanup.
Two practical points the book stresses:
- Package management. LFS has none. Track installed files somehow, using
DESTDIRstaging plus tarballs,porg, or a simplefindtimestamp approach. You will need this to upgrade. - Debugging symbols. Keep separate debug info for glibc and GCC (
objcopy --only-keep-debug) if you plan to debug later.
4.2.8 Chapter 9: system configuration (systemd)
- Networking. Use
systemd-networkdfiles in/etc/systemd/network/(for example10-eth-dhcp.networkwith[Match] Name=enp1s0and[Network] DHCP=ipv4), plus/etc/hostname,/etc/hostsandsystemd-resolved. - Devices. udev rules, plus persistent naming for NICs and disks.
- Clock, console, locale.
/etc/adjtime,/etc/vconsole.conf,/etc/locale.conf, generated withlocaledef. Also/etc/inputrcand/etc/shells. - systemd customisation. Disable screen clearing at boot, configure core dumps and journald limits, and set defaults through
systemctl preset-all.
4.2.9 Chapter 10: fstab, the kernel, and GRUB
cat > /etc/fstab << "EOF"
# file system mount-point type options dump fsck
/dev/sda2 / ext4 defaults 1 1
/dev/sda3 swap swap pri=1 0 0
EOF
Building the kernel for the new system (Linux 7.1.8 in LFS 13.1):
cd /sources/linux-7.1.8
make mrproper
make defconfig # a sane starting point for x86_64
make menuconfig # then apply the systemd-required options below
make
make modules_install
cp -iv arch/x86/boot/bzImage /boot/vmlinuz-7.1.8-lfs-13.1-systemd
cp -iv System.map /boot/System.map-7.1.8
cp -iv .config /boot/config-7.1.8
The book lists kernel options that systemd needs, and each one ties back to a kernel concept from Part 2:
CONFIG_CGROUPSandCONFIG_MEMCG: systemd places every service in a cgroup (2.4).CONFIG_PSI: pressure-stall information, used by systemd-oomd.CONFIG_DEVTMPFSandCONFIG_DEVTMPFS_MOUNT:/devexists before udev starts (2.8).CONFIG_INOTIFY_USER,CONFIG_TMPFSandCONFIG_TMPFS_POSIX_ACL.CONFIG_RELOCATABLEandCONFIG_RANDOMIZE_BASE(KASLR);CONFIG_STACKPROTECTOR_STRONG(2.11).CONFIG_INETandCONFIG_IPV6. IPv6 is described as “highly recommended by the systemd developers”.UEVENT_HELPERdisabled, because udev handles uevents.RT_GROUP_SCHEDdisabled, because it can break systemd features.- On UEFI:
CONFIG_EFI,CONFIG_EFI_STUB,CONFIG_EFI_PARTITION,CONFIG_VFAT_FSandCONFIG_EFIVAR_FS. With NVMe disks, addCONFIG_BLK_DEV_NVME.
Build the root filesystem driver and the disk controller driver into the kernel (=y). Then you do not need an initramfs, which is part of LFS’s minimalism.
GRUB:
# BIOS:
grub-install /dev/sda --target=i386-pc
# UEFI (ESP mounted at /boot/efi):
grub-install --target=x86_64-efi --removable
cat > /boot/grub/grub.cfg << "EOF"
set default=0
set timeout=5
insmod part_gpt
insmod ext2
set root=(hd0,2)
menuentry "GNU/Linux, Linux 7.1.8-lfs-13.1-systemd" {
linux /boot/vmlinuz-7.1.8-lfs-13.1-systemd root=/dev/sda2 ro
}
EOF
(hd0,2) is GRUB’s own disk numbering (disks from 0, partitions from 1), and it is unrelated to the kernel’s /dev/sda2. When device names are unstable, use search --set=root --fs-uuid <UUID> and root=PARTUUID=<UUID>. Then set /etc/os-release and /etc/lfs-release, leave the chroot, unmount everything in reverse order, and reboot into your system.
4.2.10 What LFS teaches, mapped to Part 2
| LFS step | Kernel/OS concept made concrete |
|---|---|
LFS_TGT fake triplet | Toolchain identity, sysroots, host/target separation |
| Linux API headers | The UAPI boundary: the kernel’s stable contract |
glibc --enable-kernel= | The libc–kernel ABI and syscall feature detection |
Sanity check with readelf | ELF interpreter, dynamic loading (execve → ld-linux) |
| chroot + virtual filesystems | Path-resolution root vs. namespaces; procfs/sysfs/devtmpfs |
--enable-default-pie/ssp | Toolchain-enforced exploit mitigations |
| systemd kernel options | cgroups, PSI, devtmpfs, inotify — kernel mechanisms consumed by userspace policy |
| GRUB + no initramfs | The boot chain without early userspace |
4.3 Beyond LFS
-
BLFS (Beyond Linux From Scratch) 13.1 (released 3 September 2026) adds over 1,000 further packages, including 212 security updates over the previous release: Xorg/Wayland, desktops, servers, and an initramfs if you need one. ALFS/jhalfs automates the LFS book when you want to rebuild repeatedly.
-
Buildroot (current: 2026.08, which supports Linux 7.1.x, GCC 16.2.0 and glibc 2.44; LTS releases now get 3 years of support) uses Kconfig and Make to produce a cross-toolchain, kernel, bootloader and root filesystem image for an embedded board from a single
make.3940 It teaches the same stack as LFS, but declaratively. It does not do package management on the target, by design.git clone https://gitlab.com/buildroot.org/buildroot.git && cd buildroot make qemu_x86_64_defconfig && make -j"$(nproc)" ./output/images/start-qemu.sh -
Yocto Project / OpenEmbedded (current LTS 6.0 “Wrynose”, released May 2026 and supported to April 2030; latest stable 6.1 “Blacksail”, October 2026) is a layered metadata system.941 BitBake recipes, layers, machine/distro configs, sstate caching and SBOM generation together make up a distribution factory. It has a steeper learning curve and is the industrial standard for long-lived products. Yocto 6.0 can enable Rust in the kernel via
KERNEL_FEATURES. -
Alpine Linux / musl + BusyBox is the production version of the quickstart above. It is a real, security-focused distribution built on a minimal userland, and it is worth reading its
aportsbuild recipes.
4.4 Building a Linux OS vs. writing a kernel
These are different projects:
| Building a Linux OS | Writing a kernel | |
|---|---|---|
| You write | Configuration, build scripts, init scripts, integration | Boot code, paging, interrupts, scheduler, drivers, syscalls |
| You reuse | Linux kernel, GCC, glibc, systemd… | Usually nothing but a compiler and maybe a bootloader (Limine, GRUB multiboot) |
| Result | A usable, POSIX-compatible system | A toy (or research) kernel that boots and does a few things |
| Teaches | Integration, ABI boundaries, toolchains, boot | Hardware mechanism at the lowest level |
If you want to write a kernel:
- OSDev wiki: x86 bare-metal reference.
- xv6: MIT’s teaching Unix, on RISC-V, around 10k lines, with a book. It is the best way to see a whole kernel at once.
- Philipp Oppermann’s “Writing an OS in Rust”.
- “Operating Systems: Three Easy Pieces”: for the theory.
The two paths reinforce each other. After xv6, Linux’s kernel/sched/ and mm/ read as engineered versions of ideas you have already implemented.