Linux From Scratch 13.0 刚刚于 3 月 5 日发布,我一直想尝试这个从零开始编译 Linux 的过程,这次正好碰到大版本更新,不如现在就行动起来。宿主机是 Arch,我们将按照 LFS 的官方书来构建,主要分为三个部分:
这一章主要完成第一部分。
宿主系统要求
首先,宿主机必须安装构建 LFS 需要的软件。LFS 第二章很贴心的给了一个 Shell 脚本来检查是否满足要求。在我的系统里,运行后的输出是这样的:
bash version-check.sh
OK: Coreutils 9.10 >= 8.1
OK: Bash 5.3.9 >= 3.2
OK: Binutils 2.46 >= 2.13.1
OK: Bison 3.8.2 >= 2.7
OK: Diffutils 3.12 >= 2.8.1
OK: Findutils 4.10.0 >= 4.2.31
OK: Gawk 5.4.0 >= 4.0.1
OK: GCC 15.2.1 >= 5.4
OK: GCC (C++) 15.2.1 >= 5.4
OK: Grep 3.12 >= 2.5.1a
OK: Gzip 1.14 >= 1.3.12
OK: M4 1.4.21 >= 1.4.10
OK: Make 4.4.1 >= 4.0
OK: Patch 2.8 >= 2.5.4
OK: Perl 5.42.0 >= 5.8.8
OK: Python 3.14.3 >= 3.4
OK: Sed 4.9 >= 4.1.5
OK: Tar 1.35 >= 1.22
OK: Texinfo 7.2 >= 5.0
OK: Xz 5.8.2 >= 5.0.0
OK: Linux Kernel 6.19.6 >= 5.4
OK: Linux Kernel supports UNIX 98 PTY
Aliases:
OK: awk is GNU
OK: yacc is Bison
OK: sh is Bash
Compiler check:
OK: g++ works
OK: nproc reports 20 logical cores are available
所有检查都通过了,现在系统已经做好了安装 LFS 的准备。
创建新分区
安装一个系统首先得在硬盘上创建分区。我的主系统安装在一块 512G 的 SSD 上,由于当时安装分配完了整个硬盘,现在没有空闲的空间给我装新系统了,必须先用 ISO 创建一个Live USB,在 Arch 安装介质中缩小现有分区后再创建。
Arch Linux ISO 目前最新的 Release 是 2026.03.01,先在 Arch Linux Downloads 将 ISO 下载下来,再用 pacman 验证签名:
pacman-key -v archlinux-2026.03.01-x86_64.iso.sig
==> Checking archlinux-2026.03.01-x86_64.iso.sig... (detached)
gpg: Signature made Sun 01 Mar 2026 06:46:18 PM CST
gpg: using EDDSA key 3E80CA1A8B89F69CBA57D98A76A5EF9054449A5C
gpg: issuer "pierre@archlinux.org"
gpg: Note: trustdb not writable
gpg: Good signature from "Pierre Schmitz <pierre@archlinux.org>" [full]
gpg: aka "Pierre Schmitz <pierre@archlinux.de>" [unknown]
签名正确,说明 ISO 没有被篡改。
过去在 Windows 上制作安装介质需要专门的刻录工具,这次有机会学习 Arch wiki 的 USB flash installation medium,发现原来直接将 ISO cat 进 USB 就可以:
cat path/to/archlinux-version-x86_64.iso > /dev/disk/by-id/usb-My_flash_drive
查资料发现,cat 绕过了文件系统,直接将 ISO 中的每一个字节按照从 0 扇区开始的顺序,原样填入 U 盘的对应物理位置,完整还原整个磁盘结构,包括引导扇区。这样逐字节的复制直接把 ISO 刻录进了 U 盘里。当 UEFI 启动时,会找到被 cat 写入的 .efi 文件,从而从 U 盘中的安装介质启动。同样常见的写法是用 dd,原理一样。
制作完成后重启进入 BIOS,修改启动选项为 USB,再重启就进入 Arch 的安装介质了。
我在安装宿主系统时,给硬盘分了三个区:nvme0n1p1,nvme0n1p2,和 nvme0n1p3。第三个分区是 root,目前还有 400 多 G 的空间。官方书说预留 20G 的大小足够了,这里我就取个整,留 400G 给 root,剩下的 73.9G 来安装 LFS。
注意:下面的操作顺序至关重要——必须先缩小文件系统,再缩小分区。如果反过来,分区缩小后文件系统的数据仍然占据着被截断的空间,会导致数据丢失。
先执行自检:
e2fsck -f /dev/nvme0n1p3
再缩小文件系统:
resize2fs /dev/nvme0n1p3 400G
现在文件系统只占用了前 400G 的空间,我们可以用 parted 来缩小分区了。
parted /dev/nvme0n1
先看看现在的分区:
(parted) unit GiB
(parted) p
Model: *my_ssd* (nvme)
Disk /dev/nvme0n1: 477GiB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:
Number Start End Size File system Name Flags
1 0.00GiB 1.00GiB 1.00GiB ext4 boot, esp
2 1.00GiB 3.00GiB 2.00GiB linux-swap(v1) swap
3 3.00GiB 477GiB 474GiB ext4
为了给第三分区留 400G 的空间,我们需要对它 resizepart 到 3 + 400 = 403GiB:
(parted) resizepart 3 403GiB
Warning: Shrinking a partition can cause data loss, are you sure you want to continue?
Yes/No? y
再来看看 resize 是否成功:
(parted) p
Model: *my_ssd* (nvme)
Disk /dev/nvme0n1: 477GiB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:
Number Start End Size File system Name Flags
1 0.00GiB 1.00GiB 1.00GiB ext4 boot, esp
2 1.00GiB 3.00GiB 2.00GiB linux-swap(v1) swap
3 3.00GiB 403GiB 400GiB ext4
拉伸文件系统填满分区:
resize2fs /dev/nvme0n1p3
resize2fs 1.47.3 (8-Jul-2025)
Resizing the filesystem on /dev/nvme0n1p3 to 104857344 (4k) blocks.
The filesystem on /dev/nvme0n1p3 is now 104857344 (4k) blocks long.
最后用 mkpart 创建新分区:
mkpart primary ext4 403GiB 100%
我们看看分区是否成功:
lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
nvme0n1 259:0 0 476.9G 0 disk
|-nvme0n1p1 259:1 0 1G 0 part /boot
|-nvme0n1p2 259:2 0 2G 0 part [SWAP]
|-nvme0n1p3 259:3 0 400G 0 part /
`-nvme0n1p4 259:4 0 73.9G 0 part /mnt/lfs
新分区出现在了 nvme0n1p4。现在已经完成分区了,退出安装介质,重启进 BIOS,进入宿主机去 mkfs:
mkfs -v -t ext4 /dev/nvme0n1p4
mke2fs 1.47.3 (8-Jul-2025)
fs_types for mke2fs.conf resolution: 'ext4'
Discarding device blocks: done
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
4849664 inodes, 19382784 blocks
969139 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=2168455168
592 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Filesystem UUID: 8f690333-33d5-4d18-af87-7bcf27ff8eed
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624, 11239424
Allocating group tables: done
Writing inode tables: done
Creating journal (131072 blocks): done
Writing superblocks and filesystem accounting information: done
我们给 LFS 的空间已经初始化完成了,后续所有的操作都会在这个分区中进行。
挂载文件系统
现在为了从宿主机操作这个文件系统,我们需要在宿主机创建一个挂载点,把文件系统挂载上去。我们和书上一样选择 /mnt/lfs:
export LFS=/mnt/lfs
接着设置访问权限模式掩码 (umask) 为 022,保证只有文件所有者可以写新创建的文件和目录:
umask 022
创建挂载点:
mkdir -pv $LFS
把文件系统挂载上去:
mount -v -t ext4 /dev/nvme0n1p4 $LFS
$LFS 是根目录,其核心文件必须归 root 所有,且 root 有所有权限,而其他用户可以读取和执行根目录,但不能写入。为此,我们先把根目录的所有权设置为 root,再把权限改为 755 (rwxr-xr-x):
chown root:root $LFS
chmod 755 $LFS
OK,官方书第二章准备宿主系统已经完成了。
软件包和补丁
这一节我们会下载构建整个 Linux 系统所必须的软件包,它们只有 603MB 大小——原来 600MB 就足以构建一个完整的 Linux!
首先需要一个目录保存这些软件包,它们自然应该被放在 $LFS 里,我们取名为 sources:
mkdir -v $LFS/sources
目前 $LFS 的所有者是 root,而为了安全,实际的编译工作通常要切换到一个非特权用户。所以要给予所有用户对 sources 的写入权限。但仍然只允许 root 删除其中的文件,所以还要添加一个 sticky bit 来实现这一点:
chmod -v a+wt $LFS/sources
现在已经准备好下载软件包了。我们总共需要 78 个软件包来完成 LFS 的构建,具体有哪些、为什么需要,可以在官方书的 Rationale for Packages in the Book 这一节中找到,而他们的下载地址在 All Packages 中。
其中有几个最核心的软件包:
- Binutils,包含汇编器
as和链接器ld,它们是一切编译的基础,除此之外还有一些非常重要的底层工具。它将是我们第一个编译的包,因为 GCC 和 Glibc 的configure脚本需要测试链接器和汇编器的功能,来判断某些特性是否可用。如果先编译 GCC,它会去找宿主机的链接器,导致污染。 - GCC,对于一个完整的系统,我们需要能独立的在系统中完成编译工作,而不依赖宿主机。没有 GCC 的时候,编译工作都是由宿主机完成的,这些编译出来的程序隐式依赖宿主机的库和路径,放在新系统中可能跑不起来,LFS 上的 GCC 才真正给了我们独立编译的条件。第一次编译出的 GCC 是残缺版,由于没有标准库,它不知道
size_t是什么类型、malloc如何分配内存、系统调用是什么东西,功能非常受限。后续编译完 glibc 和 libstdc++ 后,我们会用这个残缺版来编译完整的 GCC,用完整的 GCC 再重新编译 Binutils、Glibc、Libstdc++ 和后续所有包,这就是自举(Bootstrapping)。 - Glibc,它实现了 C 标准库,我们在 C 语言中写
printf、malloc、open等标准库函数时会调用 Glibc,把它们翻译成write、brk、openat等内核实现的系统调用。除此之外, POSIX 扩展也由它实现,包括线程库 pthreads 如pthread_create和mutex、BSD socket API 封装如socket和bindconnect、 resolver 如getaddrinfo等。它是 Linux 最核心的库,几乎所有程序在底层都在调用 Glibc。编译完后的 glibc 在系统中叫做libc.so.6,几乎任何涉及到底层的 debug 操作都会遇到它。 - Linux kernel,内核本体,无需多言。
官方已经提供了一个 wget-list-systemd,我们可以直接用 wget 下载所有软件包:
curl -o wget-list-systemd https://www.linuxfromscratch.org/lfs/view/13.0-systemd/wget-list-systemd
wget --input-file=wget-list-systemd --continue --directory-prefix=$LFS/sources
--2026-03-06 22:49:04-- https://download.savannah.gnu.org/releases/acl/acl-2.3.2.tar.xz
Loaded CA certificate '/etc/ssl/certs/ca-certificates.crt'
Connecting to 127.0.0.1:7890... connected.
Proxy request sent, awaiting response... 302 Moved Temporarily
Location: https://nongnu.niranjan.co/acl/acl-2.3.2.tar.xz [following]
--2026-03-06 22:49:06-- https://nongnu.niranjan.co/acl/acl-2.3.2.tar.xz
Connecting to 127.0.0.1:7890... connected.
Proxy request sent, awaiting response... 200 OK
Length: 371680 (363K) [application/octet-stream]
Saving to: ‘/mnt/lfs/sources/acl-2.3.2.tar.xz’
acl-2.3.2.tar.xz 100%[===============================================================================>] 362.97K 533KB/s in 0.7s
2026-03-06 22:49:08 (533 KB/s) - ‘/mnt/lfs/sources/acl-2.3.2.tar.xz’ saved [371680/371680]
...
等待所有软件包完成下载。
接着用 md5sum 来验证正确性:
pushd $LFS/sources
md5sum -c md5sums
acl-2.3.2.tar.xz: OK
attr-2.5.2.tar.gz: OK
autoconf-2.72.tar.xz: OK
automake-1.18.1.tar.xz: OK
bash-5.3.tar.gz: OK
bc-7.0.3.tar.xz: OK
binutils-2.46.0.tar.xz: OK
...
popd
所有软件包都验证完毕。
最后的准备工作
我们熟悉 /bin、/etc、/var 等 Linux 根目录下的文件夹,它们几乎都是基于 Filesystem Hierarchy Standard (FHS) 建立的目录树。LFS 的目录布局也基于 FHS,但我们目前只用创建一些交叉工具链所需要的目录:
mkdir -pv $LFS/{etc,var} $LFS/usr/{bin,lib,sbin}
此时根目录的分布为:
$LFS
├── etc
├── usr
│ ├── bin
│ ├── lib
│ └── sbin
└── var
现代 Linux 标准倾向于将所有文件放在 /usr/bin、/usr/lib、/usr/sbin,但对应的 /bin、/lib、/sbin 在根目录中依然存在,为了合并它们,我们需要给它们建立链接:
for i in bin lib sbin;
do ln -sv usr/$i $LFS/$i
done
在 x86 架构上,64 位程序的动态链接器的标准路径通常 hardcode 为 /lib64/ld-linux-x86-64.so.2,所以对于 64 位系统,我们需要显式的创建 /lib64:
case $(uname -m) in x86_64)
mkdir -pv $LFS/lib64 ;;
esac
后面章节的交叉编译程序会被安装到一个专门的目录 $LFS/tools 中。创建该目录:
mkdir -pv $LFS/tools
目前我们的大部分工作都是作为 root 完成的。但是以 root 身份运行系统有风险,微小的错误可能损害整个系统,所以我们需要创建一个非 root 的新用户 lfs 来完成后续的操作:
groupadd lfs
useradd -s /bin/bash -g lfs -m -k /dev/null lfs
其中 -s /bin/bash 设置 bash 为用户 lfs 的默认 shell,-g lfs 添加 lfs 到组 lfs,-m 为 lfs 创建一个主目录,-k /dev/null 将模板目录设置为空设备文件 /dev/null,不从宿主系统拷贝配置文件,隔离 LFS 与宿主机。
现在将 lfs 设为 $LFS 中所有目录的所有者:
chown -v lfs $LFS/{usr{,/*},var,etc,tools}
case $(uname -m) in x86_64)
chown -v lfs $LFS/lib64 ;;
esac
启动一个以 lfs 身份运行的 shell:
su - lfs
新用户就创建完成了。
Linux 编译过程中,很多工具比如 GCC、make 会自动读取环境变量,如果宿主系统安装了一些特殊的库,编译器可能会引用宿主系统的文件而非 $LFS 下的文件,造成污染。解决方案是为 lfs 创建一个纯净的隔离环境:
cat > ~/.bash_profile << "EOF"
exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash
EOF
这条命令创建一个 ~/.bash_profile 文件,内容为 exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash。由于此指令比较重要,在此逐行解释:
exec是一个替换命令,启动一个新进程来替换当前的 Shell 进程env -i代表 ”ignore-environment“,清除所有宿主系统的环境变量HOME=$HOME TERM=$TERM PS1='\u:\w\$ '清除变量后,手动重新定义这三个变量,保证 Shell 可以正常运行。/bin/bash启动一个新的 bash
新的 Shell 并非登录 Shell,它不会执行 /etc/profile 或者 .bash_profile,而只会执行 .bashrc。现在创建 .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
具体参数的作用可以在 Setting Up the Environment 中找到。
对于多核 CPU,可以通过并行编译来减少编译的时间,为此我们需要在 make 后加上 -j$(nproc),其中$(nproc) 是 CPU 逻辑核心的数量。我的这颗 12700KF 有 8 个 P 核,每个 P 核能同时跑两个线程,总共可以抽象出 16 个线程。除此之外还有 4 个 E 核,每个 E 核能同时跑一个线程,总共 4 个线程。所以整个 CPU 共有 16 + 4 = 20 个线程,这就是 $(nproc) 的值。我们在 .bashrc 后加上此环境变量使得 make 自动使用 20 线程并行编译:
cat >> ~/.bashrc << "EOF"
export MAKEFLAGS=-j$(nproc)
EOF
最后 source 一下 .bash_profile 使环境就绪:
source ~/.bash_profile
至此,所有的准备工作都完成了。下一章我们将进入 构建交叉工具链 的章节,开始真正的编译工作。