chroot,即 change root,用于改变当前进程及其子进程的根目录到指定的目录,创建一个隔离的环境。这是我们脱离宿主系统的第一步,后续所有的操作都在 chroot 环境下进行。从现在开始,所有的命令都要以 root 身份执行,所以必须要小心,否则可能对 LFS 系统造成损害。

chroot 前准备

第一步是改变所有者。目前,$LFS 目录树的所有者都是 lfs,这个用户只存在于宿主系统,并不存在于 LFS 系统。为此,我们要将 $LFS/* 目录的所有者都改为 root:

chown --from lfs -R root:root $LFS/{usr,var,etc,tools}
case $(uname -m) in
  x86_64) chown --from lfs -R root:root $LFS/lib64 ;;
esac

Linux 延续了 UNIX 一切皆文件的设计哲学,根目录下有一些目录是用来和内核进行通信的,比如 /dev/proc 等,它们被称作虚拟内核文件系统(Virtual Kernel File Systems)。在 chroot 环境中构建系统时,应用程序需要这些接口来获取硬件信息或向内核发送指令,所以必须先把它们挂载到 $LFS 目录树中。

先创建挂载点:

mkdir -pv $LFS/{dev,proc,sys,run}

对于/dev 的挂载,在正常的引导过程中,内核会自动挂载 devtmpfs/dev,LFS 官方书考虑到一些内核可能不支持 devtmpfs,选择绑定挂载宿主系统的 /dev 目录:

mount -v --bind /dev $LFS/dev

接下来挂载其余的虚拟内核文件系统:

mount -vt devpts devpts -o gid=5,mode=0620 $LFS/dev/pts

其中 gid=5 使所有通过 devpts 文件系统创建的设备节点属于编号为 5 的组,mode=0620 使得所有通过 devpts 创建的设备节点的权限模式为 0620,即所属用户可读写,所属组可写。这两个选项共同保证 devpts 创建的设备节点符合 grantpt() 函数的要求,这样就不需要 Glibc 的 pt_chown 辅助程序。

mount -vt proc proc $LFS/proc
mount -vt sysfs sysfs $LFS/sys
mount -vt tmpfs tmpfs $LFS/run

进入 chroot 环境

现在正式 chroot:

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
  • chroot "$LFS":将根目录切换至 $LFS 变量所指的路径,使该路径在当前会话中表现为系统的根目录 /
  • /usr/bin/env -i:运行 env 程序;-i 参数表示忽略当前宿主机的环境变量,创建一个全新的空白环境。
  • HOME=/root:在 chroot 环境中设置 root 用户的 HOME 目录为 /root
  • TERM="$TERM":将宿主机的终端类型(如 xterm-256color)传递进去,确保在 chroot 中使用 vimtop 等工具时能正确渲染显示。
  • PS1='(lfs chroot) \u:\w\$ ':定义 shell 的提示符格式,显示当前用户、当前路径。
  • PATH=/usr/bin:/usr/sbin:重置可执行文件的搜索路径,确保只使用新系统中已安装的工具。
  • MAKEFLAGS="-j$(nproc)":设置 make 编译时的并行作业数为宿主机的 CPU 核心数。
  • TESTSUITEFLAGS="-j$(nproc)":设置运行软件包的测试套件(Check/Test)时启用多线程并行测试。
  • /bin/bash --login:在 chroot 环境中启动 Bash;带 --login 参数确保读取 /etc/profile 等登录配置文件,使环境完整生效。

此时我们会进入 chroot 后的 Bash,其提示符为 I have no name!,由于我们还没有创建 /etc/passwd 文件,这是正常的。

接下来基于 FHS 创建一些必要的目录:

mkdir -pv /{boot,home,mnt,opt,srv}
mkdir -pv /etc/{opt,sysconfig}
mkdir -pv /lib/firmware
mkdir -pv /media/{floppy,cdrom}
mkdir -pv /usr/{,local/}{include,src}
mkdir -pv /usr/lib/locale
mkdir -pv /usr/local/{bin,lib,sbin}
mkdir -pv /usr/{,local/}share/{color,dict,doc,info,locale,man}
mkdir -pv /usr/{,local/}share/{misc,terminfo,zoneinfo}
mkdir -pv /usr/{,local/}share/man/man{1..8}
mkdir -pv /var/{cache,local,log,mail,opt,spool}
mkdir -pv /var/lib/{color,misc,locate}

ln -sfv /run /var/run
ln -sfv /run/lock /var/lock

install -dv -m 0750 /root
install -dv -m 1777 /tmp /var/tmp

默认情况下所有新建目录的权限都是 755,除了最后的 /root/tmp/var/tmp/root 被设置为了 0750,即仅限 root 及其组访问;/tmp/var/tmp 被设置为了 1777,即所有人可写,但只能删除自己的文件。

必要的文件和符号链接

Linux 过去在 /etc/mtab 维护挂载文件系统列表,现代 Linux 直接在内核中维护,为了满足一些仍然使用 /etc/mtab 的工具,就要创建符号链接:

ln -sv /proc/self/mounts /etc/mtab

接下来创建一个 hosts 文件,映射 localhost 到 127.0.0.1:

cat > /etc/hosts << EOF
127.0.0.1  localhost $(hostname)
::1        localhost
EOF

为了使 root 能正常登录,需要 /etc/passwd 文件:

cat > /etc/passwd << "EOF"
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/dev/null:/usr/bin/false
daemon:x:6:6:Daemon User:/dev/null:/usr/bin/false
messagebus:x:18:18:D-Bus Message Daemon User:/run/dbus:/usr/bin/false
systemd-journal-gateway:x:73:73:systemd Journal Gateway:/:/usr/bin/false
systemd-journal-remote:x:74:74:systemd Journal Remote:/:/usr/bin/false
systemd-journal-upload:x:75:75:systemd Journal Upload:/:/usr/bin/false
systemd-network:x:76:76:systemd Network Management:/:/usr/bin/false
systemd-resolve:x:77:77:systemd Resolver:/:/usr/bin/false
systemd-timesync:x:78:78:systemd Time Synchronization:/:/usr/bin/false
systemd-coredump:x:79:79:systemd Core Dumper:/:/usr/bin/false
uuidd:x:80:80:UUID Generation Daemon User:/dev/null:/usr/bin/false
systemd-oom:x:81:81:systemd Out Of Memory Daemon:/:/usr/bin/false
nobody:x:65534:65534:Unprivileged User:/dev/null:/usr/bin/false
EOF

其中每一行代表一个用户。拿第一行的 root 来说:

  1. 用户名 (root):登录时输入的名称。
  2. 密码占位符 (x):表示密码加密后存储在 /etc/shadow 文件中。
  3. 用户 ID (0):UID。系统识别用户的唯一数字。0 永远属于 root。
  4. 组 ID (0):GID。该用户所属主要组的 ID。
  5. 备注/全名 (root):对用户的描述信息。
  6. HOME 目录 (/root):用户登录后的起始目录。
  7. 登录 Shell (/bin/bash):用户登录后启动的程序。

至于密码,后续会设置。

除了 /etc/passwd 之外,Linux 用户管理需要的另一个重要文件就是定义组的 /etc/group

cat > /etc/group << "EOF"
root:x:0:
bin:x:1:daemon
sys:x:2:
kmem:x:3:
tape:x:4:
tty:x:5:
daemon:x:6:
floppy:x:7:
disk:x:8:
lp:x:9:
dialout:x:10:
audio:x:11:
video:x:12:
utmp:x:13:
clock:x:14:
cdrom:x:15:
adm:x:16:
messagebus:x:18:
systemd-journal:x:23:
input:x:24:
mail:x:34:
kvm:x:61:
systemd-journal-gateway:x:73:
systemd-journal-remote:x:74:
systemd-journal-upload:x:75:
systemd-network:x:76:
systemd-resolve:x:77:
systemd-timesync:x:78:
systemd-coredump:x:79:
uuidd:x:80:
systemd-oom:x:81:
wheel:x:97:
users:x:999:
nogroup:x:65534:
EOF

其中每一行代表一个组。拿第一行的 root 来说:

  1. 组名 (root): 组的名字
  2. 密码占位符 (x): 现代系统通常不会把组密码直接存放在 /etc/group 中,这里一般只是占位符。
  3. 组 ID (0): 组的 GID。
  4. 成员列表: 这里为空。对于第二行的 daemon 来说,表示 daemon 除了自己的初始组外,还属于 bin 组。

GID 65534 被内核用于 NFS 和用户命名空间,以表示未映射的用户或组。我们把它分配给 nobodynogroup,但是其他发行版可能用不同方式处理这个编号。

后续的一些测试需要用到一个非 root 用户,在这里创建一个,用完后再删除:

echo "tester:x:101:101::/home/tester:/bin/bash" >> /etc/passwd
echo "tester:x:101:" >> /etc/group
install -o tester -d /home/tester

重新启动 Bash,移除 I have no name! 提示符:

exec /usr/bin/bash --login

安装基本系统软件

接下来是漫长的编译软件包环节,由于编译过程都是完全一样的,这里就略过了。所有的软件包及其作用都可以在官方书的 Chapter 8 找到。

我在编译所有软件包时都用了 -march=native 参数,官方书说这样的行为是为定义的,但对于我的机器来说结果很成功。虽然性能提升就那么一点,但足以让人很兴奋了。

编译内核

现在进入最具挑战性的环节——编译内核。

内核配置有 12000 多个选项,非常复杂。LFS 官方的 Kernel configuration 介绍了内核配置的常识。

编译内核分为三步:配置、编译、安装

配置

在配置之前,先确保内核源码树是干净的:

make mrproper

在源码树 make 一个 menuconfig 即可进入配置页面:

make menuconfig

这会默认启动 ncurses 驱动的页面:

kernel

LFS 官方对于内核编译做如下配置:

General setup --->
  [ ] Compile the kernel with warnings as errors                        [WERROR]
  CPU/Task time and stats accounting --->
    [*] Pressure stall information tracking                                [PSI]
    [ ]   Require boot parameter to enable pressure stall information tracking
                                                     ...  [PSI_DEFAULT_DISABLED]
  < > Enable kernel headers through /sys/kernel/kheaders.tar.xz      [IKHEADERS]
  [*] Control Group support --->                                       [CGROUPS]
    [*]   Memory controller                                              [MEMCG]
    [ /*] CPU controller --->                                     [CGROUP_SCHED]
      # This may cause some systemd features malfunction:
      [ ] Group scheduling for SCHED_RR/FIFO                    [RT_GROUP_SCHED]
  [ ] Configure standard kernel features (expert users) --->            [EXPERT]

Processor type and features --->
  [*] Build a relocatable kernel                                   [RELOCATABLE]
  [*]   Randomize the address of the kernel image (KASLR)       [RANDOMIZE_BASE]

General architecture-dependent options --->
  [*] Stack Protector buffer overflow detection                 [STACKPROTECTOR]
  [*]   Strong Stack Protector                           [STACKPROTECTOR_STRONG]

[*] Networking support --->                                                [NET]
  Networking options --->
    [*] TCP/IP networking                                                 [INET]
    <*>   The IPv6 protocol --->                                          [IPV6]

Device Drivers --->
  Generic Driver Options --->
    [ ] Support for uevent helper                                [UEVENT_HELPER]
    [*] Maintain a devtmpfs filesystem to mount at /dev               [DEVTMPFS]
    [*]   Automount devtmpfs at /dev, after the kernel mounted the rootfs
                                                           ...  [DEVTMPFS_MOUNT]
    Firmware loader --->
      < /*> Firmware loading facility                                [FW_LOADER]
      [ ]   Enable the firmware sysfs fallback mechanism [FW_LOADER_USER_HELPER]
  Firmware Drivers --->
    [*] Export DMI identification via sysfs to userspace                 [DMIID]
    [*] Mark VGA/VBE/EFI FB as generic system framebuffer       [SYSFB_SIMPLEFB]
  Graphics support --->
    <*>    Direct Rendering Manager (XFree86 4.1.0 and higher DRI support) --->
                                                                      ...  [DRM]
    [*]    Display a user-friendly message when a kernel panic occurs
                                                                ...  [DRM_PANIC]
    (kmsg)   Panic screen formatter                           [DRM_PANIC_SCREEN]
    Supported DRM clients --->
      [*] Enable legacy fbdev support for your modesetting driver
                                                      ...  [DRM_FBDEV_EMULATION]
    Drivers for system framebuffers --->
      <*> Simple framebuffer driver                              [DRM_SIMPLEDRM]
    Console display driver support --->
      [*] Framebuffer Console support                      [FRAMEBUFFER_CONSOLE]

File systems --->
  [*] Inotify support for userspace                               [INOTIFY_USER]
  Pseudo filesystems --->
    [*] Tmpfs virtual memory file system support (former shm fs)         [TMPFS]
    [*]   Tmpfs POSIX Access Control Lists                     [TMPFS_POSIX_ACL]

对于 64 位系统,启用 CONFIG_PCI_MSICONFIG_IRQ_REMAP

Processor type and features --->
  [*] x2APIC interrupt controller architecture support              [X86_X2APIC]

Device Drivers --->
  [*] PCI support --->                                                     [PCI]
    [*] Message Signaled Interrupts (MSI and MSI-X)                    [PCI_MSI]
  [*] IOMMU Hardware Support --->                                [IOMMU_SUPPORT]
    [*] Support for Interrupt Remapping                              [IRQ_REMAP]

对于 NVME 的 SSD,启用 NVME 支持:

Device Drivers --->
  NVME Support --->
    <*> NVM Express block device                                  [BLK_DEV_NVME]

对于这些参数的作用,官方书 Kernel 章节做了详细的解释。

编译

第二步编译,就是简单的 make:

make

我花了 1 分 24 秒来编译内核。

安装

make 会编译出来一个叫做 bzImage 的内核二进制镜像,我们需要把它放到 Linux 启动文件目录 /boot 中,以便 Grub 找到内核:

cp -iv arch/x86/boot/bzImage /boot/vmlinuz-6.18.10-lfs-13.0-systemd

/boot 中镜像的名字可以改变,但开头一般都是 vmlinuz,其中 vm 表示 Virtual Memory(虚拟内存),linu 表示 Linux,z 表示Compressed(压缩)。

System.map 是内核符号文件,将内核 API 的每个函数入口点和运行时数据结构映射到它们的地址。也需要复制到 /boot 中:

cp -iv System.map /boot/System.map-6.18.10

安装 Linux 内核文档:

cp -r Documentation -T /usr/share/doc/linux-6.18.10

最后做一些收尾工作。比如创建一个 /etc/lsb-realse

cat > /etc/lsb-release << "EOF"
DISTRIB_ID="Linux From Scratch"
DISTRIB_RELEASE="13.0-systemd"
DISTRIB_CODENAME="lnvitesace"
DISTRIB_DESCRIPTION="Linux From Scratch"
EOF

当然还有 os-release

cat > /etc/os-release << "EOF"
NAME="Linux From Scratch"
VERSION="13.0-systemd"
ID=lfs
PRETTY_NAME="Linux From Scratch 13.0-systemd"
VERSION_CODENAME="lnvitesace"
HOME_URL="https://www.linuxfromscratch.org/lfs/"
RELEASE_TYPE="stable"
EOF

使 LFS 可引导

我的宿主机已经有 Grub,这里并不打算重新安装一遍,而是就用宿主机的 Grub 引导 LFS。

最简单的方式当然是 os-prober。在宿主机上更新 Grub:

sudo grub-mkconfig -o /boot/grub/grub.cfg                                                               
1
Generating grub configuration file ...
Found theme: /usr/share/grub/themes/vimix/theme.txt
Warning: os-prober will be executed to detect other bootable partitions.
Its output will be used to detect bootable binaries on them and create new boot entries.
Found Linux on /dev/nvme0n1p4
Found linux image: /boot/vmlinuz-linux
Found initrd image: /boot/intel-ucode.img /boot/initramfs-linux.img
Warning: os-prober will be executed to detect other bootable partitions.
Its output will be used to detect bootable binaries on them and create new boot entries.
Found Linux on /dev/nvme0n1p4
Adding boot menu entry for UEFI Firmware Settings ...
done

这说明 os-prober 成功识别到了位于 /dev/nvme0n1p4 的 Linux 系统,并为它生成了可供 Grub 使用的启动项。这里显示的是探测结果,不是直接在说 LFS 的内核镜像已经被精确匹配完成。接下来重启电脑,在宿主机的 Grub 中选择新加入的启动项即可。

lfs

启动成功!最后别忘记去官网的 lfscounter 注册一个自己的名字

lfs_count