构建交叉编译工具链

本章节,将安装交叉编译器也就是 Binutils、GCC 这两个包到独立的的$LFS/tools 目录,然后将系统 C 库和标准 C++ 库安装至$LFS/usr,构成整个工具链。

3.2.1 安装 Binutils:二进制工具的集合

Binutils,其完整的名字应该是 GNU Binutils,是一个二进制工具集。其中主要包含 GNU 的链接器 ld 和汇编器 as,以及 addr2line、gold、objdump 等二进制工具。

  • 大致构建用时: 1 SBU
  • 所需磁盘空间: 617 MB

首先进入$LFS/sources:

cd $LFS/sources

解压并进入软件包:

tar -xf binutils-2.35.tar.xz
cd	binutils-2.35

Binutils 手册中建议,在源码目录之外一个专门的编译目录里面编译 Binutils:

mkdir	-v build
cd        build

现在准备编译 Binutils:

../configure		\
    --prefix=/tools		\
    --with-sysroot=$LFS		\
    --target=$LFS_TGT		\
    --disable-nls		\
    --disable-werror

配置选项的含义如表 3-1 所示。

表 3-1 Binutils 配置选项
参数描述
--prefix=/tools告诉配置脚本将 Binutils 程序安装到 /tools 文件夹
--with-sysroot=$LFS用于交叉编译,告诉编译系统在 $LFS 中查找所需的目标系统库
--target=$LFS_TGT仅在建立交叉编译环境的时候使用,构建一个新的用于编译运行在其他 target 指定系统上的程序的编译器。
--disable-nls禁止国际化(i18n),因为国际化对临时工具来说没有必要
--disable-werror防止来自宿主编译器的警告事件导致编译停止

构建软件包:

make

安装软件包:

make install

在 3.1.2 软件包构建时间单位 SBU 已经介绍了作为衡量软件包构建速度度量值的 SBU,该值的正是以当前软件包 我们要测量一下这个软件包从配置到包括第一次安装在内的编译时间。为了轻松的做到这点,会用类似time { ./configure ... && ... && make install; } 的方式将命令包裹在 time 命令中,具体如下:

time {
    ../configure		\
        --prefix=/tools		\
        --with-sysroot=$LFS		\
        --target=$LFS_TGT		\
        --disable-nls		\
        --disable-werror		\
    && make			\
    && make install
}

如果运行了上述指令,那后续到 make install 为止的指令就不必执行了,那些只是以上指令的拆解。为了方便在不需要执行的命令的描述前加了*作为区别。

real    4m38.087s
user    3m47.317s
sys     0m38.048s

退出并清理软件包:

cd ../..
rm -rf binutils-2.35

按照 3.1 节构建顺序中的第 4、5 步,在构建完成后我们需要回到/mnt/lfs/sources/并删除构建完的软件包目录。在整个构建过程中很多在本章出现的软件包需要构建两次,Binutils 更是要构建四次,而我们每次构建的指令都不尽相同,不能直接在上次构建的目录里重新构建。

3.2.2 安装 GCC:GNU 编译器套件

GCC 是 GNU Compiler Collection 的首字母缩写,是一套由 GNU 开发的编程语言编译器,包含 C、C++、Objective-C、Fortran、Ada、Go 和 D 的前端和库。GCC 最初是作为 GNU 操作系统的编译器编写的,目前在类 Linux 系统上使用最为广泛的编译器套件。

  • 大致构建用时: 11 SBU
  • 所需磁盘空间: 3.8 GB

首先,解压并进入软件包:

tar -xf gcc-10.2.0.tar.xz
cd	gcc-10.2.0

然后,解压 GMP、MPFR 和 MPC 这三个软件包,并将解压后的目录重命名。属主系统中未必包含这三个软件包,而 GCC 又依赖它们的精度运算函数。在构建临时系统时,我们没有必要花时间构建这三个软件包,只需要将它们解压到 GCC 的目录下,GCC 在构建的过程中即可直接使用。

tar -xf ../mpfr-4.1.0.tar.xz
mv -v mpfr-4.1.0 mpfr
tar -xf ../gmp-6.2.0.tar.xz
mv -v gmp-6.2.0 gmp
tar -xf ../mpc-1.1.0.tar.gz
mv -v mpc-1.1.0 mpc
将 x86_64 的主机上,设置 64 位的库的默认目录为 lib:
case $(uname -m) in
  x86_64)
    sed -e '/m64=/s/lib64/lib/' \
        -i.orig gcc/config/i386/t-linux64
 ;;
esac

GCC 安装手册建议在软件包目录之外另外创建一个专门用于构建 GCC 的目录。其原因是 GCC 的开发者使用的是这种方式,测试也以此为标准,所以构建的过程有保证。直接在软件包目录或者在软件包目录创建子目录构建,其实也无不可。这里采用的就是在软件包中创建子目录构建的方式,经笔者测试构建过程中没有发生问题。有一点需要注意,我们执行的下一个命令是在软件包的子目录中运行的,如果你试图按照 GCC 的推荐在软件包的同级创建目录,需要对稍后的命令稍加更改。

mkdir -v build
cd	build

准备编译 GCC:

../configure		\
    --target=$LFS_TGT		\
    --prefix=$LFS/tools		\
    --with-glibc-version=2.11	\
    --with-sysroot=$LFS		\
    --with-newlib		\
    --without-headers		\
    --enable-initfini-array	\
    --disable-nls		\
    --disable-shared		\
    --disable-multilib		\
    --disable-decimal-float	\
    --disable-threads		\
    --disable-libatomic		\
    --disable-libgomp		\
    --disable-libquadmath	\
    --disable-libssp		\
    --disable-libvtv		\
    --disable-libstdcxx		\
    --enable-languages=c,c++

下面来分析下上面 GCC 编译时的配置选项的含义:

表 3-2 GCC 配置选项
参数描述
--with-glibc-version=2.11这里填写的 glibc 版本与宿主机系统需求相同。设置该参数的理由是为了让 GCC 与宿主机中的 glibc 兼容保持兼容。
--with-newlib将“newlib” 指定为目标 C 库。
该参数确保编译 libgcc 时定义了常数 inhibit_libc。由于还没有可用的 C 库,这样做可以防止编译任何需要 libc 支持的代码
--without-headers防止 GCC 查找不需要的头文件
在创建完整的交叉编译器时,GCC 要求标准头文件和目标系统兼容。对于我们的目的来说,不需要这些头文件。
--enable-initfini-array一些必须的内部数据结构无法被交叉编译器识别,所以需要手动开启
--disable-shared这个选项强制 GCC 静态链接到它的内部库。我们这样做是为了避免主机系统可能出现的问题
--disable-multilib不去生成编译为其他平台可执行代码的交叉编译器
在 x86_64 机器上,LFS 还不支持 multilib 配置,使用 64 位主机的读者不应该输入这个选项。这个选项对 x86 来说无害
--disable-decimal-float
--disable-threads
--disable-libatomic
--disable-libgomp
--disable-libquadmath
--disable-libssp
--disable-libvtv
--disable-libstdcxx
这些选项取消了对十进制浮点数扩展、线程化、libatomic、libgomp、libquadmath、libssp、libvtv、libcilkrts 和 C++ 标准库的支持。这些功能在编译交叉编译器的时候会导致编译失败,对于交叉编译临时的 libc 来说也没有必要
--enable-languages=c,c++这个选项用于指定构建的编译器和运行时库,我们只选择目前我们需要的 C 和 C++

运行命令编译 GCC:

make

在 make 完成后,通常需要运行测试套件来检查 make 是否出错,但正如前面提到的,测试套件框架还没有准备好。在此进行测试的并没有太多好处,因为第一遍编译的程序很快会被取代。所以可以直接安装刚才 make 的软件包:

make install

构建 GCC 的过程中会安装若干系统头文件,其中就包含 limits.h。在构建 GCC 的时候,使用的是 GCC 内部的 limits.h,虽然缺少系统头文件的扩展特性,但对于构建临时的 libc 已经足够,不过在后续的构建工作中需要的是完整的 limits.h,所以需要运行以下命令生成该文件:

cd ..
cat gcc/limitx.h gcc/glimits.h gcc/limity.h > \
`dirname $($LFS_TGT-gcc-print-libgcc-file-name)`/install-tools/include/limits.h

安装后用以下命令退出并清理软件包:

cd ../..
rm -rf gcc-10.2.0

如果你不是在软件包内部创建的构建目录,请自行删除构建目录。清楚构建过的软件包的作用在之前已经说明了,之后将不再说明。

3.2.3 安装 Linux API 头文件

Linux 内核是最后安装的一个关键软件包,但下一章的 Glibc 需要 Linux 内核提供的 API,所以需要预先安装内核的 API 头文件。

  • 大致构建用时: 0.2 SBU
  • 所需磁盘空间: 3.8 GB 头文件也包含在 Linux 内核的软件包内,所以解压 linux-5.8.3.tar.xz 并进入目录 linux-5.8.3:
tar -xf linux-5.8.3.tar.xz
cd	linux-5.8.3

编译内核之前养成习惯,运行以下命令确保没有陈旧的文件影响本次构建。其实解压后第一次使用无需执行此命令,如果是多次使用,务必在使用前运行此命令。

make mrproper

从源代码中提取用户可见的内核头文件。头文件起初会被存放在./usr 中,然后再复制到相应的位置。不推荐使用 header_install,那样需要依赖 rsync,在当前环境中可能无法使用。

make	headers
find	usr/include -name '.*' -delete
rm    usr/include/Makefile
cp 	-rv usr/include $LFS/usr

之后退出并清理软件包:

cd ..
rm -rf linux-5.8.3

3.2.4 安装 Glibc:GNU C 库

Glibc 便是 GNU C 库项目为 GNU 系统和 GNU / Linux 系统以及许多其他使用 Linux 作为内核的系统提供了核心 C 库。任何 Unix-like 操作系统都会需要一个 C 库。该库需要涵盖系统调用和基础功能等一系列的函数,例如常用的 printf、malloc 等。LFS 也将安装其作为使用的系统 C 库。

  • 大致构建用时: 4.6 SBU
  • 所需磁盘空间: 762 MB 解压并进入软件包:
tar -xf glibc-2.32.tar.xz
cd 	glibc-2.32

首先,创建 lsb 的符号链接,对于 x86_64

case $(uname -m) in
    i?86)   ln -sfv ld-linux.so.2 $LFS/lib/ld-lsb.so.3
    ;;
    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-2.32-fhs-1.patch

与 GCC 一样,根据 Glibc 手册建议,需要在源文件夹之外的一个专用文件夹中编译 Glibc:

mkdir -v build
cd	build

下一步,准备编译 Glibc:

../configure			\
    --prefix=/tools			\
    --host=$LFS_TGT			\
    --build=$(../scripts/config.guess)	\
    --enable-kernel=3.2                	\
    --with-headers=/tools/include    	\
    libc_cv_slibdir=/lib

配置选项的含义如表 3-3 所示。

参数描述
--host=$LFS_TGT
--build=$(../scripts/config.guess)
这些选项的组合效果是让 Glibc 的构建系统配置它自己使用 /tools 里面中的交叉链接器和交叉编译器交叉编译自己其自身。
--enable-kernel=3.23.2这告诉 Glibc 编译能支持 3.23.2 以及之以后的内核库。更早的内核版本不受支持。
--with-headers=/tools/include告诉 Glibc 利用刚刚安装在 tools 文件夹中的头文件编译自身,此这样能够根据内核的具体特性提供更好的优化。
libc_cv_slibdir=/lib启用/lib 中的库,以替代/lib64 中默认的 64 位库

运行本指令,可能会产生以下警告:

configure: WARNING:
*** These auxiliary programs are missing or
*** incompatible versions: msgfmt
*** some features will be disabled.
*** Check the INSTALL file for required versions.

警告意为 msgfmt 缺失,或版本不兼容。Gettext 软件包中包含 msgfmt 程序,不过当前该程序应该由宿主机提供,用于将文本翻译描述生成为二进制消息目录。不过不必担心,此警告是可以忽略的。

在 make 并行构建这个软件包时,可能失败。为避免该情况发生,使用-j1 参数强制使用单核构建。

编译软件包:

make -j1

安装软件包:

在安装前需要注意命令中的 LFS 变量是否已经正确设置。倘若此时您使用的是 root 用户,或者 LFS 变量没有正确设置,以下命令极有可能将新构建的 Glibc 安装至宿主系统中,如此一来,可能会导致宿主机无法正常使用。

make DESTDIR=$LFS install

DESTDIR=$LFS

DESTDIR 参数在大多数软件包中用于指定安装位置。如果此处不加指定,那么安装位置竟会是默认的/目录。这里将其设置为 LFS,也就是在 Chroot 环境后的/目录。

在安装完之后,接下来必须要做的便是确认新工具链的基本功能,即编译和链接功能,是否都如预期的那样可以正常工作。执行以下命令进行检查:

echo 'int main(){}' > dummy.c
$LFS_TGT-gcc dummy.c
readelf -l a.out | grep ': /tools'

如果一切工作正常的话,应该不会出现错误消息,输出形式将如下所示:

[Requesting program interpreter: /tools/lib64/ld-linux-x86-64.so.2]

此处实例的输出结果是 64 位的解释器,32 位机器上对应的解释器,应为/tools/lib/ld-linux.so.2。

如果输出结果错误,或者没有输出结果,那便表明在先前构建的某些地方应该出错了。请回溯并查看构建步骤,找出问题所在,并加以改正。在继续构建工作前,务必解决问题,使得此处能够输出正确结果。

在后续的 4.3.6 节编译 Binutils 时会再一次检查工具链是否正确编译。如果 binutils 编译失败,说明之前安装 binutils、GCC,或者 Glibc 时某些地方出现了错误。

退出并清理软件包:

cd ../..
rm -rf glibc-2.32

3.2.5 安装 Libstdc++-标准的 C++ 库

Libstdc++ 是 GCC 的一部分,是 GNU 标准 C++ 库,许多软件包需要用到 C++ 库。

  • 大致构建用时: 0.5 SBU
  • 所需磁盘空间: 954 MB Libstdc++ 包含在 GCC 的软件包中。所以解压 gcc-10.2.0.tar.xz 并进入目录 gcc-10.2.0。
tar -xf gcc-10.2.0.tar.xz
cd	gcc-10.2.0

为 Libstdc ++ 另外创建一个用于构建的文件夹并进入该文件夹:

mkdir -v build
cd	build

准备编译 Libstdc++:

../libstdc++-v3/configure		\
    --host=$LFS_TGT			\
    --build=$(../config.guess)		\
    --prefix=/usr			\
    --disable-multilib			\
    --disable-nls			\
    --disable-libstdcxx-pch		\
    --with-gxx-include-dir=/tools/$LFS_TGT/include/c++/10.2.0

配置选项的含义如表 3-4 所示。

表 3-4 Libstdc 配置选项
参数作用描述
--host=...指示使用我们刚才编译的交叉编译器,而不是/usr/bin 中的。
--host=$LFS_TGT替代/usr/bin 中默认的交叉编译器,将其指定为刚才构建的交叉编译器。
--disable-libstdcxx-pch此选项防止预编译 include 文件的安装。,在当前阶段并不需安装这些。
--with-gxx-include-dir=/tools/$LFS_TGT/include/c++/10.2.0指定的是 C++ 编译器搜索标准 include 文件的位置。在一般的编译中默认情况,这个该位置信息会从顶层目录自动传入 libstdc++ 的 configure 选项,在但目前我们还没有完善的目录结构,所以的例子中,必须明确指定该信息。

之后编译 Libstdc ++:

make

编译后进行库的安装:

make DESTDIR=$LFS install

退出并清理软件包:

cd ../..
rm -rf gcc-10.2.0