LFS 0: LFS 项目介绍

概述

  LFS(Linux From Scratch)项目由Gerard Beekmans于1999年发起,它的目的是指导用户一步步从源代码构建整个Linux系统的所有组件,直到得到一个完全属于自己的 Working Linux 系统。LFS 项目充分体现了一种自己动手探索和学习的精神,也体现了 Linux 及相关开源社区的精神。

  在 LFS 的作者看来,充分理解 Linux 系统的内部的各个组件以及它们的相互关系,可以使用户获得能力来定制化自己的操作系统;用户可以不依赖于一些现成的Linux 系统,自己可以更加灵活地掌握控制系统的各个层面。另外,用户可以获得更加小巧紧凑的系统,不用安装一些不需要或者不理解的软件,这样可以节省资源,特别对于一些资源敏感的嵌入式系统。最后,这样构建的系统更加安全,用户可以随时从源代码编译、测试和安装安全补丁,而不是依赖他人。

  LFS 项目的主体是一份手册,现在最新版本是11.3(2023年3月1日),任何人都可以从官方网站 LinuxFromScratch 获得,另外注意下载软件包时选择合适的镜像可以缩短下载时间。
  BLFS、CLFS 和 ALFS 项目是由 LFS 项目扩展衍生出来的。

标准

  LFS 尽可能遵循下面的标准(但软件包的最终选择还是由用户决定):

  LFS 中包含的属于 LSB 的软件包:

  • LSB Core: Bash, BC, Binutils,Coreutils,Diffutils,File,Findutils,Gawk,Grep,Gzip,M4, Man-DB,Ncureses,Procps,Psmisc,Sed,Shasow,Tar,Util-Linux,Zlib
  • LSB Runtime Languages: Perl,Python

交叉编译

  一个系统可以复制它自己,这如何做到呢?对于操作系统来说,如果它拥有关于自己的所有源代码,它如何一步步构建可以移植到不同处理器架构的机器上的自己呢?

  Linux 系统与 Unix 传统相同,它的内核及大部分组件(一些来源于 GNU 项目)由 C 语言编写( Linux 内核中会部分包含一些汇编代码),即它可以通过 C 编译器移植到任何硬件平台上。

  交叉编译(Cross Compilation)是指我们在一个系统平台上(如 x86 )编译可以在其他系统平台(如 ARM )上运行的可执行代码,那么它在什么情况下最经常使用呢?

  首先,在很多情况下,目标平台由于资源限制,没有现成的编译器及开发测试环境,甚至没有操作系统,而用于开发的系统要使用更快速性能更高的完全不同的平台。

  另外,交叉编译可以使用一个编译环境来生成针对不同目标平台的系统,(例如 Launchpad平台 就提供了帮助开发者交叉编译、打包和发布的功能)。

  最后,交叉编译可以针对系统模拟器生成可执行代码,这些模拟器所模拟的硬件平台可能是非常老的系统,原来的编译环境(native compiler)已经很难搭建。

  LFS Book 中介绍了 Canadian Cross 交叉编译模式,它假设了三个机器A,B 和 C。慢机器 A 安装有 ccA 原生编译器,快机器 B 没有编译器,目标是生成在慢机器 C 上运行的软件。

第一步:使用 ccA 生成目标 (Target) 为机器 B 的交叉编译器 cc1,注意 cc1 运行在(Host)机器 A 上,但它生成的是针对机器 B 的可执行代码。

第二步:使用 cc1 生成目标为机器 C 的交叉编译器 cc2,注意 cc2 运行在(Host)在机器 B上,但它生成的是针对机器 C 的可执行代码。

第三步:在机器 B 上运行 cc2,使用它来生成 ccC 原生编译器,它将运行在(Host)机器 C上,机器 C 上的其他程序也可以在机器 B 上通过 cc2 编译生成。

  LFS Book 中使用伪交叉编译(Fake Cross Compilation),因为对于大多数读者来说,构建LFS的系统和运行LFS的是同一系统。LFS首先使用原生编译器 cc-pc 构建交叉编译器 cc1,cc1 的 Host 系统为 pc,Target 系统是 lfs ;第二步是使用 cc1 生成原生编译器 cc-lfs,cc-lfs 则用在 chroot 环境构建整个 lfs 系统。

准备工作

分区

a. 创建新分区

  将 LFS 安装到单独的分区是一种良好的实践,我们可以简单使用 fdiskparted 分配一个10G-30G(根据需要增加)容量的新分区。除了主分区,其他分区(如swap分区)可以简单使用 Host 上已经有的分区。关于分区的具体知识,参考 关于计算机操作系统启动引导的总结

b. 格式化文件系统

mkfs -v -t ext4 /dev/xxx

**c. 设置 $LFS 环境变量 **

export LFS=/mnt/lfs
另外也可以将上述命令写入 ~/.bash_profile~/.bashrc 文件。

d. 挂载分区

1
2
mkdir -pv $LFS
mount -v -t ext4 /dev/xxx $LFS

如果有其他新建分区,也要在 $LFS 中创建挂载点并进行挂载。
为了在系统重启后不需要再次手动执行挂载,可以在 /etc/fstab 中添加:

/dev/xxx /mnt/lfs ext4 default

如果使用 swap 分区,请确定它已经激活:
/sbin/swapon -v /dev/zzz

获取软件包

下载软件包:中国境内的镜像地址

解压到 $LFS/source目录, 并设置 sticky 标志:

1
2
mkdir -v $LFS/source
chmod -v a+wt $LFS/source

如果上诉步骤使用非 root 用户操作,则所有文件的 UID 和 GID 都是该用户的 UID 和 GID。当后面 chroot 进入没有创建该用户的 lfs 系统,GID 和 UID就无效了,所以我们提前将所有文件的 owner 和 group 设置为 root:

chown root:root $LFS/source/*

最后的准备工作

a. 以 root 用户创建根目录结构

1
2
3
4
5
6
7
8
mkdir -pv $LFS/{etc,var} $LFS/usr/{bin,lib,sbin}
for i in bin lib sbin; do
ln -sv usr/$i $LFS/$i
done

case $(uname -m) in
x86_64) mkdir -pv $LFS/lib64 ;;
esac

中间过程会生成一些交互编译工具链,这些工具应该与最后的根目录中的程序隔离,因此创建 tool 目录:
mkdir -pv $LFS/tool

**b.创建 lfs 用户和 lfs用户组 **
为了获得更加干净的工作环境,建议新建 lfs 用户和用户组:

1
2
groupadd lfs
useradd -s /bin/bash -g lfs -m -k /dev/null lfs

设置口令(可选):
passwd lfs

c. 将lfs设置为 $LFS 根目录的 owner:

1
2
3
4
5
chown -v lfs $LFS/{usr{,/*},lib,var,etc,bin,sbin,tools}

case $(uname -m) in
x86_64) chown -v lfs $LFS/lib64 ;;
esac

注意不包括 $LFS/source

d. 切换到 lfs 用户:
su - lfs

e. 设置环境

1
2
3
cat > ~/.bash_profile << "EOF"
exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash
EOF

注意目前的 ~ 现在代表 lfs 的home 目录。

1
2
3
4
5
6
7
8
9
10
11
12
cat > ~/.bashrc << "EOF"
set +h ## 关闭 bash 的 Hash 功能
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

生效:
source ~/.bash_profile

构建工具链和临时工具

编译交叉工具链 Pass 1

  该节所生成的程序和库都会安装到特定的目录$LFS/tool中,以便将交叉编译工具与其他程序分开。

  首先是 Binutils 包的第一次编译(Pass1),它会生成交叉汇编器 (cross-as)、交叉链接器(cross-ld)等,这些工具在对 gcc 和 glibc 编译时使用。

  然后是 gcc 包的第一次编译(Pass1),特别注意 configure 脚本的配置选项。因为 GCC 依赖一个内部库 libgcc,它需要链接 glibc 才能具有全部的功能,而此时我们还没有 glibc,另外 C++ 库(libstdc++)也依赖 glibc。对这个“蛋->鸡->蛋”问题的解决方法是生成退化的(degraded) libgcc(当然此时生成的 cc1 也是退化的),它缺少一些功能(例如线程和异常处理)。然后使用 cc1 编译 glibc(glibc 不是退化的)。接着编译libstdc++(缺少一些 libgcc 的功能)。最后当我们获得 cc-lfs 后再生成完全的 libstdc++ 库(Pass 2)。这个阶段的配置选项要避免对 libc 和 libstdc++ 的依赖,以及避免编译一些不需要的组件。

  安装Linux API 头文件,它们在编译 glibc 时会使用。

  编译 glibc,configure 脚本中的 –host 和 –build 选项要特别注意:要保证使用我们上述生成的交叉工具链而不是 Host 系统中的工具。注意 glibc 库不是交叉工具链,所以其安装目录是 /usr,通过 --prefix=/usr 设置。另外,这里的 /usr 是相对路径。(因为 make DESTDIR=$LFS install,glibc 被安装到 $LFS/usr。)

  libstdc++ 第一次编译(Pass1),生成 C++ 库。安装路径同样是 $LFS/usr

交叉编译临时工具和 Pass 2

  这些临时工具安装到最后的位置,但还不能使用,因为它们还依赖于 Host 系统上的一些工具,但它们可以链接已经安装的库,比如 glibc。

  在进入 chroot 环境时,这些临时工具是必要的,而不是在 chroot 之后再编译它们,比如 bash。它们的依赖关系是怎样的以及为什么选择它们呢?

  M4 包被用于编译工具,Ncurses 包被 Bash 包依赖,Bash 包是进入 chroot 的必要工具。Coreutils 包含一些基本工具,例如操作文件和获得系统基本特性的工具。Diffutils、File、Findutils、Gawk、Grep、Gzip、Make、Patch、Sed、Tar、Xz包提供另外的常用基本工具。

  Binutils包第二次编译(Pass2),这次编译生成原生汇编器、原生链接器等,它们被原生编译器 cc-lfs 所依赖。注意在 configure 配置选项上和 Pass1 的区别,此时我们不需要设置 --target,因为我们要获得的是原生工具链,只需要设置 --host=$LFS_TGT

   gcc 包第二次编译(Pass2),这次编译生成 cc-lfs。注意--with-build-sysroot=$LFS选项;--target=$LFS_TGT选项确保此次构建使用 Pass1 中的库(如 glibc),因此生成的 libgcc 库也不再是退化的。LDFLAGS_FOR_TARGET=-L$PWD/$LFS_TGT/libgcc选项允许 libstdc++ 使用此次构建的(共享的) libgcc 而不是 Pass1 中静态的(退化的) libgcc

进入 chroot 环境原生编译其他临时工具

  在进入 chroot 环境前要更改整个 LFS 文件目录的权限:设置为 root。然后,挂载虚拟内核文件系统(Virtual Kernel File System),包括/dev、/proc、/run、/sys。内核文件系统不占用任何磁盘空间,它可以使得用户空间与内核空间通信以及用于内核自身的通信。在进入 chroot 环境前为什么要这么做呢?以 procps 包为例,它包含获取当前运行进程信息的工具,如果没有挂载 /proc 这些工具就无法测试和使用。

1
2
3
4
5
6
7
8
9
10
11
chown -R root:root $LFS/{usr,lib,var,etc,bin,sbin,tools}
case $(uname -m) in
x86_64) chown -R root:root $LFS/lib64 ;;
esac

mkdir -pv $LFS/{dev,proc,sys,run}
mount -v --bind /dev $LFS/dev
mount -v --bind /dev/pts $LFS/dev/pts
mount -vt proc proc $LFS/proc
mount -vt sysfs sysfs $LFS/sys
mount -vt tmpfs tmpfs $LFS/run

进入 chroot 环境:

1
2
3
4
5
6
chroot "$LFS" /usr/bin/env -i \
HOME=/root \
TERM="$TERM" \
PS1='(lfs chroot) \u:\w\$ ' \
PATH=/usr/bin:/usr/sbin \
/bin/bash --login

进入 chroot 环境后,生成一些目录和文件,例如/boot、/home、/mnt、/opt和/srv等。

1
mkdir -pv /{boot,home,mnt,opt,srv}

新建基本的配置文件(如/etc/passwd/etc/group/etc/mtab/etc.host等)和文件符号链接。

  libstdc++ 第二次编译(Pass2),此次编译将生成完全的 libstdc++ 库,从而可以编译使用 C++ 编写或者部分编写的软件。(11.3版本中无此步骤,10.1版本中包括该步骤。)

  对 gettext,bison,perl,python,texinfo,util-linux包的编译。

构建 LFS 的基本系统软件

  我们已经有了编译环境和足够的临时工具,因此可以开始构建LFS系统。
  前面的一些临时工具也会被重新编译并被覆盖(override),这些系统软件会被测试,然后安装,并且兼容 FHS 和 LSB 标准。每一个系统软件都值得去亲自编译、测试和安装:The key to learning what makes a Linux system work is to know what each package is used for and why you (or the system) may need it.

  Bash、Python 等包的编译和第一次编译有些不同,这次编译会设置一些依赖,这些依赖的包已经安装到系统上,而不是像第一次编译那样使用自己内部的包。

系统配置

启动引导

/etc/fstab文件

编译内核

  内核编译命令比想象的要简单一些,如下:

1
2
3
4
$ make mrproper          # Clean the kernel tree
$ make menuconfig # Configure kernel options
$ make # Compile kernel image and modules
$ make modules_install # install modules

  编译内核需要一些软件工具,这些工具前面几节已经安装到指定的最后位置,但弄清楚需要依赖哪些软件包可以帮助我们在其他平台构造编译环境。

  除了 make、gcc,还主要包括bash、coreutils、diffutils、e2fsprogs、findutils、grep、gzip、hostname、glibc、ncurses、perl、sed、sysvinit、tar和 util-linux。在 Ubuntu 平台这些软件包和工具列表由 build-essential 包维护。另外,Ubuntu 使用 Debian 包管理系统,因此它会使用一些额外的包,如 kernel-package、fakeroot、initramfs-tools和 module-init-tools。

设置 GRUB 引导

##过程记录
对于多次编译的包,一定要重新解压源码包,然后进入新解压的包目录进入编译,不要在先前编译的基础上重新配置、编译和安装。

M4编译错误?为什么在chroot环境下会由getopt中的变量名冲突?

为什么没有自动创建$LFS/usr/lib目录?

为什么出现””Assumed value of MB_LEN_MAX wrong”错误? Answer: 修改tools/gcc/x86_64-lfs-linux-gnu/10.2.0/include-fixed/limits.h 中的MB_LEN_MAX

Patch包中PATH_MAX变量
无法进入chroot的问题,bash无法找到libncursesw.so.6? Answer: 将libncursesw.so.6复制到lib64。对于libopcodes.so、libbfd.so、libctf.so也是同样的解决方法。

编辑/etc/ld.so.conf