1. 为什么要自己动手构建一个Linux系统构建一个属于自己的Linux系统听起来像是远古时代的操作实际上却是理解整个操作系统如何运转的最直接方式。这篇文章记录的是我最近一次从零开始、编译出一个基本可运行的Linux系统的全过程包括工具链怎么搭、根文件系统怎么落地、内核和引导器怎么接上。它不是一个可以拿来日常办公的发行版而是一个能开机进系统、能执行常见命令、能自己编译程序的最小Linux环境。这套思路在社区里通常被称作“从零构建Linux”核心不是“安装”而是“组装”把内核、工具链、基础命令这些部件一个个从源码编译出来亲眼看着系统从无到有。相比直接装一个现成的发行版这种方式的最大价值在于系统里的每一个组件你都知道它是干什么的、依赖什么东西、为什么被放进去。对于想深入理解操作系统的同学这是任何教科书都替代不了的实操体验。什么人适合跟着做我觉得至少要有这几个基础用过命令行了解ls、cd、mount、tar这些命令的含义会简单的环境变量操作更重要的是有耐心因为整个过程中有大量编译中途还会遇到各种报错。说实话前几次尝试很容易把人劝退但只要跑过一次完整的流程对系统的理解会上一个台阶。2. 开工前的准备分区、源码包与主机环境校验2.1 主机环境怎么选建议虚拟机加快照我的第一建议是不要在一台你每天都要用的物理机上直接做最好用虚拟机或者一台专门用来折腾的机器。为什么要这样因为整个构建过程会涉及分区、格式化、安装引导器一个不小心就可能把主机上原有的系统引导弄坏。虚拟机的好处是可以随便回滚快照分区表操作也更安全。我实际用的是当前主流Linux发行版做宿主机在虚拟机里开了一台x86_64架构的机器作为构建环境。虚拟机的磁盘我给了40GB内存给到4GBCPU给了4核。这个配置对于构建基本系统来说是够用的编译大型软件包时不会太煎熬。如果你是在树莓派这类ARM机器上做整个过程类似只是源码包需要下载对应架构的版本。创建好虚拟机后需要准备一个引导U盘或者ISO镜像方便待会启动到安装环境里。其实后面构建系统本身不需要图形界面终端就够了。2.2 磁盘分区与挂载规划三个分区足够开始构建之前先把目标磁盘的分区安排好。构建基本系统不需要很复杂的分区方案我采用的是三个分区根分区/、启动分区/boot、交换分区swap。对于一台准备日常使用的系统这样做确实有些简陋但探索阶段越简单越好排查问题。在虚拟机里我使用fdisk或者gdisk工具完成分区操作。具体分区参考如下fdisk /dev/sda在fdisk交互界面里依次删除原有分区然后新建/dev/sda1大小1GB类型设为EFI System或者Linux filesystem后面作为/boot。/dev/sda2剩余空间全部作为根分区/约30GB以上。/dev/sda3大小建议为内存的两倍或固定8GB类型为Linux swap。分区建好之后格式化并挂载mkfs.ext4 /dev/sda2 mkfs.ext4 /dev/sda1 mkswap /dev/sda3 swapon /dev/sda3 export LFS/mnt/lfs mkdir -pv $LFS mount /dev/sda2 $LFS然后把/boot分区挂载到$LFS/boot下面mkdir -pv $LFS/boot mount /dev/sda1 $LFS/boot这里将LFS设为环境变量值指向构建根目录。后面所有操作都会在这个目录下进行这样目标系统实际上位于$LFS的目录树里而不是直接写在主机的根文件系统上。注意分区操作极其危险一定确认你操作的是虚拟机的磁盘而不是宿主机硬盘。格式化之前用lsblk检查磁盘标识。2.3 下载源码包一组稳妥的版本组合构建基本系统涉及的源码包不算少但核心的就那么几个内核、GCC编译器、glibc标准库、binutils工具集、GRUB引导器以及一批基础命令工具coreutils、bash、grep、tar、make等。源码包的版本选择很关键我吃过不少版本搭配的亏。不同版本之间可能存在编译失败或者运行时行为不一致的问题尤其是GCC和glibc的组合。最稳妥的办法是参考你选择的那套源码仓库中记录的、经过测试的版本组合而不是随意搭配最新版。我这次用的是经过社区验证的一组稳定版本组件版本示意用途Linux Kernel6.x 长期支持版系统内核binutils2.4x汇编器、链接器gcc13.xC/C编译器glibc2.3xC标准库grub2.x启动引导器coreutils9.x基础命令bash5.x默认shell建议把所有源码包放在一个专门的目录例如$LFS/sources并且下载校验文件确保源码没被篡改或者下载不完整。下载完成后用sha256sum校验一遍这一步虽然繁琐但是能省掉后面大量莫名其妙的编译报错。同时检查宿主机的编译依赖是否齐全gcc、make、perl、python都可能用到。可以用下面这条命令快速检查关键工具是否存在which gcc make ld tar awk bison grep环境变量方面有个容易踩的坑在构建过程中需要让PATH里包含$LFS/tools/bin确保使用的是我们自己的工具链而不是宿主机上系统自带的工具。务务必在每次开启新终端时重新设置或者写进配置文件里。3. 构建临时工具链整个工程的地基3.1 为什么要先做临时工具链交叉编译思路这一步是“从零构建”里最容易劝退人的环节但也是核心所在。你要明白一个矛盾我们编译出的目标是给新系统用的但新系统现在还没跑起来连编译器都没有。那么谁来编译系统里的编译器只能是宿主机上的现有工具链。相当于用一台正在运行的系统为另一台还没安装操作系统的机器“代工”生成所有软件。然而这里有一个深层麻烦宿主机上的gcc和glibc是为宿主机的动态库路径和架构编译的。直接把宿主机gcc编出来的程序扔到新系统里它可能会去加载宿主机路径下的动态库或者依赖宿主机的特定头文件。为了避免目标系统被宿主机环境“污染”通常采用的方式是构建一套独立的临时工具链它位于$LFS/tools目录下并且尽量静态链接不依赖宿主机上的动态库。业界常把这种思路叫做临时工具链本质是交叉编译理念的一种应用用编译时的目标前缀让所有工具链组件在构建过程中指向我们自定义的$LFS/tools而不是/usr。这样新系统最终的二进制文件就不会和宿主机纠缠不清。3.2 第一遍binutils链接器的起点临时工具链的第一步是编译binutils。binutils包含了汇编器、链接器、以及一些处理目标文件的工具。为什么要先做它因为后续编译glibc和gcc时都会调用链接器来生成最终的二进制文件如果链接器本身不可用后续什么都做不了。第一遍binutils的编译配置很关键需要指定一个前缀和目标三元组cd $LFS/sources tar -xf binutils-2.4x.tar.xz cd binutils-2.4x mkdir -v build cd build ../configure --prefix$LFS/tools \ --with-sysroot$LFS \ --target$LFS_TGT \ --disable-nls \ --enable-gprofngno \ --disable-werror make -j$(nproc) make install这里的$LFS_TGT是一个形如“x86_64-lfs-linux-gnu”的目标三元组它告诉编译器“我们要生成的目标平台是一个新的Linux系统”。--with-sysroot让工具链知道去找系统头文件时要到$LFS目录下找而不是宿主机自带的/usr/include。--prefix指定安装到临时工具目录避免污染后续真正的系统目录。我在这步犯过一个很低级的错误没有设置环境变量导致configure直接找的是宿主机路径下的头文件结果后续工具链全部乱掉。建议开始前检查echo $LFS_TGT echo $LFS确认两者都非空再开始。3.3 第一遍gcc完整编译器背后还有三件套binutils装好以后就可以构建第一遍GCC。这一步我特别想强调GCC编译过程中需要GMP、MPFR、MPC这三个数学库它们是用来做高精度计算、浮点舍入等功能支持的。构建临时工具链时可以用gcc源码树内自带的一个脚本把这些库解压到gcc源码目录里这样就不需要单独安装到宿主系统上。我当时是这样操作进入gcc源码目录运行tar -xf ../mpfr-4.x.tar.xz mv mpfr-4.x mpfr tar -xf ../gmp-6.x.tar.xz mv gmp-6.x gmp tar -xf ../mpc-1.x.tar.xz mv mpc-1.x mpc然后编译gcc的第一遍版本。与binutils类似需要指定target、prefix和sysroot。另外要注意第一遍gcc有一个特殊操作把libgcc也静态编译进去因为此时目标系统的glibc还没构建没有可用的动态装载器需要让编译器生成的程序尽可能不依赖运行时动态库。第一遍GCC装完后可以做个快速自检编译一个空程序然后用链接器看依赖echo int main(){} dummy.c $LFS_TGT-gcc dummy.c readelf -l a.out | grep interpreter正常情况下输出里应该是“/tools/.../ld-linux.so.2”之类指向$LFS/tools下的解释器路径。如果你发现它指向宿主机的/lib/ld-linux说明前面的sysroot或者目标三元组配置有问题必须回头修正否则后面构建出的程序运行时找不到正确的动态库启动那一刻就会崩溃。3.4 glibc给系统装上C运行时临时工具链的另一个关键成员是glibc。这个库是所有命令程序运行的基础它负责文件操作、内存管理、进程创建、网络等底层接口。可以说只要程序用了C语言运行时就绝对离不开glibc。编译glibc前需要先安装Linux内核的头文件到目标系统的include目录里因为glibc的构建过程会用到内核提供的API头文件。这一步操作很简单解压内核源码后make mrproper make headers find usr/include -type f ! -name *\.h -delete cp -rv usr/include $LFS/usr/include注意make mrproper是为了清掉内核源码里可能存在的旧配置保证头文件干净。接下来编译glibc。配置项里有一个必须写的参数--host$LFS_TGT。还有两个额外步骤使用名为ldconfig的构建脚本生成正确的ld.so并手工修复一个可能的符号问题。这一步经常会碰到“找不到crt1.o”这类提示原因通常是工具链的sysroot还没有把glibc的启动文件放进去。遇到的解决办法是先安装头文件再编译glibc顺序不能反。4. chroot进入新系统构建完整的基本环境4.1 挂载内核虚拟文件系统并进入chroot当临时工具链准备好后我们需要进入一个新的“世界”通过chroot把当前进程的根目录切换到$LFS这样接下来所有的软件包安装都会直接安装到目标系统的目录树里就像在一个真实的Linux系统里工作一样只不过这个系统的根目录暂时是$LFS。chroot之前必须挂载一些内核提供给进程的特殊文件系统让新系统里的程序能访问到内核状态、进程信息、设备节点。我在这一步使用的命令是mount -v --bind /dev $LFS/dev mount -v --bind /dev/pts $LFS/dev/pts mount -v --bind /proc $LFS/proc mount -v --bind /sys $LFS/sys mount -v --bind /run $LFS/run这几个挂载点非常重要。如果没有挂载/proc安装某些包时可能因为无法读取内核信息而失败没有/dev很多设备节点会缺失启动后也可能出问题。建议把这几条命令保存成脚本后面调试时经常要用。然后进入chrootchroot $LFS /usr/bin/env -i \ HOME/root \ PATH/usr/bin:/usr/sbin:/bin:/sbin \ /bin/bash --login这里用env -i清空环境变量就是为了避免宿主机的环境变量传染到新系统里。这一步做完之后你的当前shell就已经“住”进新的系统了。4.2 调整工具链并编译第二遍binutils和gcc进入chroot后第一件事不是急着装各种软件而是把临时工具链中的binutils和gcc重装一遍去掉第一遍时的“临时”属性让它们安装到新系统的最终目录/usr/bin和/usr/lib下面。这一遍编译与第一遍最大的不同是此时已经有了glibc编译出来的程序就是真正给新系统用的了。第二遍binutils比较简单基本上是把第一遍的配置里的前缀改掉不再使用--target而是正常的native编译../configure --prefix/usr \ --sysconfdir/etc \ --libdir/usr/lib \ --disable-werror make -j$(nproc) make install然后第二遍gcc。同样在配置时指定tooldir和标准目录。在这步之后可以再次用readelf验证链接器的解释器路径确认已经是$/lib/ld-linux-x86-64.so.2而不是/tools开头的临时路径。这里有一个值得留意的细节新系统里还没有建立真正的运行时库目录所以某些安装脚本会去检查ldconfig缓存如果没有缓存文件可能会报错。我的经验是先用下面的命令建立一个基础的系统目录结构mkdir -pv /{bin,boot,etc/{opt,sysconfig},home,lib/firmware,media,opt,root,sbin,tmp} mkdir -pv /usr/{bin,include,lib,sbin,share,src} ln -sv /usr/bin /bin ln -sv /usr/lib /lib ln -sv /usr/sbin /sbin ln -sv /bin /usr/bin这堆目录看似琐碎实际上新系统在安装各种包时都会假设它们存在。缺失目录导致的报错常常很隐蔽值得提前处理。4.3 安装基础工具集让系统真正“能用”工具链和标准库就位后就可以开始安装系统里日常要用的命令工具了。这部分的清单较长我的选择遵循“最小可用”原则而不是完全复刻某个发行版。核心的包括coreutilsls、cat、cp、mv等核心文件命令。bash默认终端shell。tar解包工具。grep、gawk、sed、find文本与查找工具。make后续安装其他软件时编译依赖。pkg-config、perl、python很多构建脚本会调用到。每一个包都是标准的“解压、configure、make、install”流程例如coreutilstar -xf coreutils-9.x.tar.xz cd coreutils-9.x ./configure --prefix/usr make -j$(nproc) make install安装完这些基础工具之后最好立刻在chroot环境里测试几个命令比如ls、cat、grep确认它们都能正常工作。如果某个命令提示“segmentation fault”或者“cannot open shared object file”说明动态库依赖没有配好趁早排查别等到最后启动时才一起爆发。4.4 配置系统基础fstab、主机名与root用户软件装好了但系统还是“空”的需要配置文件告诉它怎么挂载磁盘它叫什么名字谁可以登录。这一步是系统启动的关键。fstab文件用于定义启动时的磁盘挂载关系我的最小化版本是这样的/dev/sda2 / ext4 defaults 1 1 /dev/sda1 /boot ext4 defaults 1 2 /dev/sda3 swap swap pri1 0 0然后是主机名和网络配置。网络这块有个很实际的经验如果内核没有编译进网卡驱动那么配置网络也没用所以这一步建议只做一个最小化的hostname和localhost解析。我建了/etc/hostname内容只有一行比如“mybox”。然后建/etc/hosts127.0.0.1 localhostroot用户的密码必须设置否则即使启动成功也登录不进去。在chroot里执行passwd root输入两遍密码即可。注意如果这里忘了设置密码后面进了新系统就只能通过单用户模式进入很麻烦。4.5 编译Linux内核最核心的一个大件内核是整个系统的心脏。没有它前面装的一切都是提篮打水。编译内核时第一步是清理源码然后配置cd /sources/linux-6.x make mrproper make menuconfigmenuconfig是一个图形化的配置界面基于文本菜单可以直接在里面选择需要的驱动和功能。对“基本可运行”来说一定要确保以下几个选项开启处理器类型和特性匹配你的虚拟机CPU通常选“Generic x86-64”。设备驱动必须包含你虚拟机的磁盘控制器驱动比如SATA/SCSI/AHCI。文件系统开启ext4如果你的根分区是ext4。块设备开启loop设备方便后续挂载镜像。内核支持模块开启module loading虽然最小系统可以不加载模块但留着更灵活。配置完成之后保存退出然后编译make -j$(nproc) make modules_install make installmake install会自动把内核镜像放到/boot目录同时生成System.map。如果编译时选了模块化驱动make modules_install会把模块安装到/lib/modules/目录。这里要特别注意模块目录的版本号必须和uname -r输出一致否则系统启动后加载模块时找不到对应目录。5. 引导器安装与首次启动调试5.1 安装GRUB让固件能找到内核内核编译好之后还需要一个引导器让计算机在开机时找到内核镜像并启动它。目前x86平台最常用的是GRUB。它的安装分两步第一步把引导代码写入磁盘第二步生成引导配置文件。由于当前在chroot环境里直接运行grub-install /dev/sda这一步会扫描磁盘并写入引导程序。如果你的分区表使用EFI方式则命令会稍有不同需要指定efi-system分区和安装到EFI目录。这里以传统BIOS引导为例。然后生成配置文件grub-mkconfig -o /boot/grub/grub.cfg生成的grub.cfg里会包含启动菜单指向/boot下的内核镜像。完成之后最好手动检查一下grub.cfg里是否出现了类似“menuentry ‘Linux’ --class gnu-linux”的段落并且启动参数里有“root/dev/sda2”。如果grub-mkconfig没有找到内核常见原因是/boot目录下没有vmlinuz文件或者内核编译失败导致没有安装内核镜像。5.2 首次启动最常见的三个故障一切准备完成后重启虚拟机并从硬盘启动。我第一次做的时候遇到的基本就是这三类问题第一种是“kernel panic - not syncing: VFS: Unable to mount root fs”。这种报错意味着内核在启动时找不到根文件系统。原因通常是内核缺少磁盘控制器或文件系统支持或者grub.cfg里的root参数不正确。解决办法是检查内核配置开启对应的AHCI和ext4支持并且确认root参数指向正确的分区。第二种是启动后卡在“switch_root: cant execute /sbin/init”。这说明内核找到了root分区但未能执行init进程。通常是基础工具没装完整或者init程序路径不对。可以进入单用户模式手动检查/sbin/init是否存在。第三种是进去之后发现网卡不显示或者ls /dev里只有一个console。这说明device manager如udev/systemd缺失或者内核驱动不足。我的解决方法是重新编译内核把所有需要的核心驱动都编译进来而不是依赖模块。5.3 从主机到chroot里的快捷排查技巧启动失败时调试的主要方式还是回到宿主机重新挂载分区并chroot进去修改。这时候会深刻体会到“chroot挂载虚拟文件系统”这套流程的好处它允许你在一个新系统完全不起的情况下像修车一样把引擎盖打开直接调整里面的零件。我的建议是把之前那几条mount命令和chroot命令做成一个shell脚本命名为enter.sh放在宿主机里每次调试时直接执行。#!/bin/bash mount /dev/sda2 $LFS mount /dev/sda1 $LFS/boot mount -v --bind /dev $LFS/dev mount -v --bind /proc $LFS/proc mount -v --bind /sys $LFS/sys mount -v --bind /run $LFS/run chroot $LFS /usr/bin/env -i \ HOME/root PATH/usr/bin:/usr/sbin:/bin:/sbin \ /bin/bash --login这个脚本能帮你快速进入新系统的“现场”修改配置、重新编译都方便得多。别嫌它麻烦后面每一次修bug都会感谢这个脚本。6. 常见问题速查与实操中的独家经验6.1 我能给你的问题速查表整个流程跑完我整理了几条高频问题。它们出现的概率一点也不低尤其是第一次做的时候现象原因处理方式configure报“cannot find crt1.o”glibc安装顺序错误或sysroot没配置确认头文件和库的路径指向$LFS/usrgcc编译时找不到mpfr/gmp数学库没解压进gcc源码目录严格按照前文步骤操作link时出现/usr/lib/ld-linux的引用工具链没有使用sysroot参数重新编译工具链检查编译前缀grub-mkconfig没有生成菜单/boot下没有内核镜像检查内核make install是否成功启动后无法加载模块模块目录版本与uname版本不一致重新make modules_installlsblk只看到极少的设备节点udev未启用或sysfs未挂载在内核开启devtmpfs并确保chroot时绑定/dev值得专门强调的是不要把宿主机的环境变量带进chroot里。我在第二次做的时候因为把HOSTNAME、PATH等变量放进chroot启动脚本导致构建出的工具链对宿主机的路径产生了依赖调试了很久才发现是环境变量污染。6.2 一些能少走弯路的经验第一次做这个项目时我犯过的错足够写满一页A4纸。总结下来有三个要点。第一所有源码包的版本尽量保持一致。不要默认“最新就是最好”GCC和glibc之间的兼容性尤其敏感。很多编译错误不是你的操作问题而是版本组合不合法。先把一套经过验证的组合跑通再考虑升级。第二每完成一个阶段就给虚拟机做一次快照。如果某一步编译失败了可以直接回滚到上一个更友好的阶段而不是从头再来。尤其当你已经连续编译了两三个小时只因一个配置项错误就要全部推倒重来的时候快照救命的感受会很强烈。第三每一条命令执行完都关注一下输出里有没有warning或者error。有些报错不会终止编译但会悄悄地在最终的二进制文件里留下隐患。比如我在编译glibc时忽略了一个与ldconfig相关的warning结果新系统启动后ls命令频繁崩溃最后重新编译才解决。6.3 这个项目做完了下一步干什么构建出一个基本可运行的系统后你就拥有了一张“白纸”。可以继续往上面添加更多软件包比如网络配置工具、SSH服务、文本编辑器也可以尝试换用musl libc或busybox来搭一套更轻量的系统用于嵌入式场景还可以研究如何把启动流程切换到systemd理解现代发行版复杂的init流程。如果对内核感兴趣可以在这个基础系统上玩内核模块开发——因为你的系统最精简添加一个新的模块几乎能看到全链路的影响。如果你对接下来的“构建Linux系统二”感兴趣我会继续记录如何在这个最小系统上搭建网络、安装桌面环境、以及配置软件包管理器让它从一个实验品变成真正顺手的日常环境。我在实际动手时最深的一个体会是这套流程其实没有多难难的是坚持不发散。一次只做一个组件做完就验证出问题就回溯最终总能得到那个能自由呼吸的、完全属于自己的Linux。你要是打算动手先记住一句话不要害怕出错错误本身就是最好的老师。至少在操作系统这件事上一次完整的失败比十次教科书式的成功教给你的东西多得多。
阅读完成 · 觉得有帮助?