Ashok G

Linux From the Metal Up

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 -kernel loader.
  • The kernel unpacked a newc cpio into rootfs and ran rdinit=/init as PID 1.
  • /init mounted the pseudo-filesystems that every later tool depends on.
  • Then it exec’d a shell. Because exec replaces 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 /init mount it and switch_root into it.
  • Add -netdev user,id=n0 -device virtio-net,netdev=n0 and run udhcpc.
  • Rebuild the kernel from tinyconfig and 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 +h turns off bash’s command hashing, so a newly built tool in $LFS/tools/bin is used as soon as it exists.
  • LFS_TGT=x86_64-lfs-linux-gnu is a fake vendor triplet. Because it differs from the host’s x86_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_SITE stops 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 DESTDIR staging plus tarballs, porg, or a simple find timestamp 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-networkd files in /etc/systemd/network/ (for example 10-eth-dhcp.network with [Match] Name=enp1s0 and [Network] DHCP=ipv4), plus /etc/hostname, /etc/hosts and systemd-resolved.
  • Devices. udev rules, plus persistent naming for NICs and disks.
  • Clock, console, locale. /etc/adjtime, /etc/vconsole.conf, /etc/locale.conf, generated with localedef. Also /etc/inputrc and /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_CGROUPS and CONFIG_MEMCG: systemd places every service in a cgroup (2.4).
  • CONFIG_PSI: pressure-stall information, used by systemd-oomd.
  • CONFIG_DEVTMPFS and CONFIG_DEVTMPFS_MOUNT: /dev exists before udev starts (2.8).
  • CONFIG_INOTIFY_USER, CONFIG_TMPFS and CONFIG_TMPFS_POSIX_ACL.
  • CONFIG_RELOCATABLE and CONFIG_RANDOMIZE_BASE (KASLR); CONFIG_STACKPROTECTOR_STRONG (2.11).
  • CONFIG_INET and CONFIG_IPV6. IPv6 is described as “highly recommended by the systemd developers”.
  • UEVENT_HELPER disabled, because udev handles uevents. RT_GROUP_SCHED disabled, because it can break systemd features.
  • On UEFI: CONFIG_EFI, CONFIG_EFI_STUB, CONFIG_EFI_PARTITION, CONFIG_VFAT_FS and CONFIG_EFIVAR_FS. With NVMe disks, add CONFIG_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 stepKernel/OS concept made concrete
LFS_TGT fake tripletToolchain identity, sysroots, host/target separation
Linux API headersThe UAPI boundary: the kernel’s stable contract
glibc --enable-kernel=The libc–kernel ABI and syscall feature detection
Sanity check with readelfELF interpreter, dynamic loading (execve → ld-linux)
chroot + virtual filesystemsPath-resolution root vs. namespaces; procfs/sysfs/devtmpfs
--enable-default-pie/sspToolchain-enforced exploit mitigations
systemd kernel optionscgroups, PSI, devtmpfs, inotify — kernel mechanisms consumed by userspace policy
GRUB + no initramfsThe 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 aports build recipes.

4.4 Building a Linux OS vs. writing a kernel

These are different projects:

Building a Linux OSWriting a kernel
You writeConfiguration, build scripts, init scripts, integrationBoot code, paging, interrupts, scheduler, drivers, syscalls
You reuseLinux kernel, GCC, glibc, systemd…Usually nothing but a compiler and maybe a bootloader (Limine, GRUB multiboot)
ResultA usable, POSIX-compatible systemA toy (or research) kernel that boots and does a few things
TeachesIntegration, ABI boundaries, toolchains, bootHardware 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.