一、GNU工具链组成
编译过程分为4个阶段,分别是:编译预处理、编译、汇编以及链接。每个阶段都涉及了若干工具,GNU将这些工具分别包含在3个软件包中:Binutils、GCC、Glibc。
Binutils:GNU将凡是与二进制文件相关的工具,都包括在软件包Binutils中。Binutils就是 Binary utilities 的简写,其中主要包括生成目标文件的汇编器(as),链接目标文件的链接器(ld)以及若干处理二进制文件的工具,如objdump、strip等。但是也不是Binutils中的所有的工具都是处理二进制文件的,比如处理文本文件的预编器(cpp)也包含在其中。
GCC:GNU将编译器包含在GCC中,包括C编译器、C++编译器、Fortran编译器、Ada编译器等。GCC中还提供了C++的启动文件。
Glibc:C库包含在Glibc中。除了C库外,动态链接器(dynamic loader/linker)也包含在这个包中。另外这个包中还提供C的启动文件。事实上,有很多C库的实现,比如适用于Linux桌面系统的Glibc、EGlibc、uClibc;在嵌入式系统上,可以使用EGlibc或者uClibc;对于没有操作系统的系统,也就是所说的 freestanding enviroment ,可以选择newlib、dietlibc,或者根本就不用C库。
除了这三个软件包外,工具链中还需要包括内核头文件。用户空间中的很多操作需要借助内核来完成,但是通常用户程序不必直接和内核打交道,而是通过更易用的C库。C库中的很大一部分函数是对内核服务的封装。在某种意义上,内核头文件可以看作是内核与C库之间的协议。因此,构建C库之前,需要首先在工具链中安装内核头文件。
二、构建工具链的过程
C99标准定义了两种运行环境,一种是 “hosted environment” ,针对的是具有操作系统的环境,程序一般是运行在操作系统之上的,因此这个操作系统不仅是内核,还包括外围的C库,对于程序来说,就是一个"hosted environment"。另外一种是 “freestanding environment”,就是程序不需要额外环境的支持,直接运行在裸机(bare metal)上,比如Linux内核,以及一些运行在没有操作系统的裸板上的程序,不再依赖操作系统内核和C库,所有的功能都在单个程序的内部实现。
通常 “hosted implementation” 的实现包含编译器(比如 GCC)和 C 库(比如 Glibc)。而 “freestanding implementation” 的实现通常只包含编译器,如 GCC,最多再加上一个简单的库,比如典型的 newlib。但是如果没有 newlib 的支持,GCC 自己也可以自给自足。
“freestanding implementation” 的实现,恰恰解决了我们提到的 GCC 和 Glibc 的循环依赖问题。我们可以先编译一个仅支持 “freestanding implementation” 的 GCC,因为在这种情况下,不需要 C 库的支持。但是 “freestanding implementation” 的 GCC 却可以编译 Glibc,因为 Glibc 也是一个自包含的,完全自给自足。事实上,Glibc 中也有小部分地方使用了 GCC 的代码,但是这不会带来依赖的麻烦,因为 GCC 一定是在 Glibc 之前编译的。
在编译目标系统的 C 库,甚至是编译 GCC 中包含的目标系统上的库时,都需要链接器,因此,Binutils 是编译器和 C 库共同依赖的。索性 Binutils 几乎没有任何依赖,只需要利用宿主系统的工具链构建一套交叉 Binutils 即可。
另外值得一提的是内核头文件。在 Linux 系统上,在编译 C 库前需要安装目标系统的内核头文件,从某种意义上讲,内核头文件就是 C 库和内核之间的一个协议(Protocol)。而且,C 库会根据内核头文件检查内核提供了哪些特性,以及需要在 C 库层面模拟哪些内核没有提供的服务。
综上所述,构建工具链步骤如下:
- 构建交叉 Binutils,包括汇编器as、链接器ld等。
- 构建临时的交叉编译器(仅支持 freestanding)。
- 安装目标系统的内核头文件。
- 构建目标系统的 C 库。
- 构建完整的交叉编译器(支持 hosted 和 freestanding )。
三、准备工作
1、环境变量
i686
unset LANG export HOST=i686-pc-linux-gnu export BUILD=$HOST export TARGET=i686-none-linux-gnu export CROSS_TOOL=/vita/cross-tool export CROSS_GCC_TMP=/vita/cross-gcc-tmp export SYSROOT=/vita/sysroot export PATH=$CROSS_TOOL/bin:$CROSS_GCC_TMP/bin:/sbin:/usr/sbin:$PATH
x86_64
unset LANG export HOST=x86_64-pc-linux-gnu export BUILD=$HOST export TARGET=x86_64-none-linux-gnu export CROSS_TOOL=/vita/cross-tool export CROSS_GCC_TMP=/vita/cross-gcc-tmp export SYSROOT=/vita/sysroot export PATH=$CROSS_TOOL/bin:$CROSS_GCC_TMP/bin:/sbin:/usr/sbin:$PATH
2、Binutils
wget https://ftp.gnu.org/gnu/binutils/binutils-2.23.tar.gz wget https://ftp.gnu.org/gnu/binutils/binutils-2.39.tar.xz
Linux 编写 configure.ac 和 Makefile.am 示例
Linux命令详解./configure、make、make install 命令
../binutils-2.23/configure --prefix=$CROSS_TOOL --target=$TARGET --with-sysroot=$SYSROOT ../configure --prefix=$CROSS_TOOL --target=$TARGET --with-sysroot=$SYSROOT make && make install
c: error: this statement may fall through [-Werror=implicit-fallthrough=]
报错解决:error: this statement may fall through [-Werror=implicit-fallthrough=]
make CFLAGS='-Wno-implicit-fallthrough' make CFLAGS='-Wno-error'
Binutils 将二进制工具安装在 $CROSS_TOOL/bin 目录下,链接脚本安装在 $CROSS_TOOL/i686-none-linux-gnu/lib/ldscripts 目录下。
其中 elf_i386.x 用于IA32上ELF文件的链接,elf_i386.xbn、elf_i386.xc 等分别对应ld使用不同的链接参数时使用的链接脚本,如果使用了 “-N” 参数,那么ld使用链接脚本elf_i386.xbn。
Binutils 在 $CROSS_TOOL/i686-none-linux-gnu/bin 目录下也安装了一些二进制工具,这些是编译器内部使用的,我们不必关心,其实这个目录下的工具与 $CROSS_TOOL/bin 目录下的工具完全相同,只是名称不同而已。
四、编译freestanding的交叉编译器
1、GCC 下载
gcc configure: error: Building GCC requires GMP…
wget https://ftp.gnu.org/gnu/gcc/gcc-4.7.2/gcc-4.7.2.tar.gz wget https://ftp.gnu.org/gnu/gcc/gcc-4.7.2/gcc-4.7.2.tar.bz2 wget https://ftp.gnu.org/gnu/gmp/gmp-5.0.5.tar.xz wget https://ftp.gnu.org/gnu/mpfr/mpfr-3.1.1.tar.xz wget https://ftp.gnu.org/gnu/mpc/mpc-1.0.1.tar.gz
2、编译
tar -xf gmp-5.0.5.tar.xz -C ../build/ mv gmp-5.0.5/ gmp tar -xf mpc-1.0.1.tar.gz -C ../build/ mv mpfr-3.1.1/ mpfr tar -xf mpfr-3.1.1.tar.xz -C ../build/ mv mpc-1.0.1/ mpc
# gcc-4.7.2 ../gcc-4.7.2/configure --prefix=$CROSS_GCC_TMP --target=$TARGET --with-sysroot=$SYSROOT --with-newlib --enable-languages=c --with-mpfr-include=/vita/build/gcc-4.7.2/mpfr/src --with-mpfr-lib=/vita/build/gcc-build/mpfr/src/.libs --disable-shared --disable-threads --disable-decimal-float --disable-libquadmath --disable-libmudflap --disable-libgomp --disable-nls --disable-libssp ../configure --prefix=$CROSS_GCC_TMP --target=$TARGET --with-sysroot=$SYSROOT --with-newlib --enable-languages=c --with-mpfr-include=/vita/build/gcc-4.7.2/mpfr/src --with-mpfr-lib=/vita/build/gcc-4.7.2/build/mpfr/src/.libs --disable-shared --disable-threads --disable-decimal-float --disable-libquadmath --disable-libmudflap --disable-libgomp --disable-nls --disable-libssp # gcc-11.3.0 ../configure --prefix=$CROSS_GCC_TMP --target=$TARGET --with-sysroot=$SYSROOT --with-newlib --enable-languages=c --with-mpfr-include=/vita2/build/11.3.0/mpfr/src --with-mpfr-lib=/vita2/build/gcc-11.3.0/build/mpfr/src/.libs --disable-shared --disable-threads --disable-decimal-float --disable-libquadmath --disable-libmudflap --disable-libgomp --disable-nls --disable-libssp
五、安装内核头文件
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/linux-3.7.4.tar.xz tar xf ../source/linux-3.7.4.tar.xz make mrproper make ARCH=i386 headers_check make ARCH=i386 INSTALL_HDR_PATH=$SYSROOT/usr/headers_install make ARCH=x86_64 headers_check make ARCH=x86_64 INSTALL_HDR_PATH=$SYSROOT/usr/headers_install
六、编译目标系统的C库
apt-get install gawk tar xvf ../source/glibc-2.15.tar.xz ../glibc-2.15/configure --prefix=/usr --host=$TARGET \ --enable-kernel=3.7.4 --enable-add-ons \ --with-headers=$SYSROOT/usr/include \ libc_cv_forced_unwind=yes libc_cv_c_cleanup=yes \ libc_cv_ctors_header=yes make make install_root=$SYSROOT install
七、构建完整的交叉编译器
# gcc-4.7.2 ../gcc-4.7.2/configure --prefix=$CROSS_TOOL --target=$TARGET --with-sysroot=$SYSROOT --enable-languages=c,c++ --with-mpfr-include=/vita/build/gcc-4.7.2/mpfr/src --with-mpfr-lib=/vita/build/gcc-4.7.2/build/mpfr/src/.libs --enable-threads=posix # gcc-4.9.4 ../configure --prefix=$CROSS_TOOL --target=$TARGET --with-sysroot=$SYSROOT --enable-languages=c,c++ --with-mpfr-include=/vita/build/gcc-4.9.4/mpfr/src --with-mpfr-lib=/vita/build/gcc-4.9.4/build/mpfr/src/.libs --enable-threads=posix # gcc-11.3.0 ../configure --prefix=/vita2/cross-tool --target=x86_64-none-linux-gnu --with-sysroot=/vita2/sysroot --enable-languages=c,c++ --with-mpfr-include=/vita/build/gcc-11.3.0/mpfr/src --with-mpfr-lib=/vita/build/gcc-11.3.0/build/mpfr/src/.libs --enable-threads=posix
最终的交叉编译器安装的主要文件如下:
- 驱动程序
GCC 安装的最主要的是交叉编译器的驱动程序,包括 i686-none-linux-gnu-gcc、i686-none-linux-gnu-g++ 等。
2.目标系统的库和头文件
GCC 中也包含了一些用于目标系统的运行时库及头文件,它们安装在 $CROSS_TOOL/i686-none-linux-gnu 目录下。在该目录下,子目录 lib 存放包括目标系统的运行时库以及供目标系统编译程序使用的静态库,子目录 include 下包含开发目标系统上的程序需要的 C++ 头文件。
3.helper program
前面我们提到,gcc 仅仅是一个驱动程序,它将调用具体的程序完成具体的任务,这些程序被 GCC 安装在 libexec 目录下,典型的有编译器 cc1,链接过程调用的 collect2 等。
libexec 与 sbin/bin 目录下存放的可执行文件的一个区别是:sbin/bin 目录下的可执行文件一般是用户使用的;而 libexec 目录下的可执行文件一般是由某个程序或工具使用的,所以一般称为 “helper program”。
4.freestanding 实现文件
前面我们提到,C99 标准定义了两种实现方式:一种称为 “hosted implementation”,支持全部 C 标准,包括语言标准以及库标准;另外一种是 “freestanding implementation” 。在 lib 目录下的头文件即为 "freestanding implementation"实现标准要求的头文件。
5.启动文件
与 C++ 相关的启动文件在 GCC 中,包括 crtbegin.o、crtend.o 等。
讨论完 C 库和编译器后,我们看到,无论是 C 库,还是 GCC 都各自安装了头文件、运行库,GCC 还安装了一些内部使用的可执行程序。那么在编译程序时,GCC 是怎么找到这些文件的呢?答案就是 GCC 内部定义的两个环境变量 LIBRARY_PATH 和 COMPILER_PATH。GCC 会根据用户的一些配置参数,包括 –target、–with-sysroot 等设置这些环境变量的值。我们可以在编译程序时,使用参数 “-v” 查看这两个变量的值。
八、定义工具链相关的环境变量
export CC="$TARGET-gcc" export CXX="$TARGET-g++" export AR="$TARGET-ar" export AS="$TARGET-as" export RANLIB="$TARGET-ranlib" export LD="$TARGET-ld" export STRIP="$TARGET-strip"
九、封装“交叉”pkg-config
“轻松搞定 CMake”系列之 find_package 用法详解
在 GNU 中大部分的软件都使用 Autoconf 配置,Autoconf 通常借助工具 pkg-config 去获取将要编译的程序依赖的共享库的一些信息,比如库的头文件存放在哪个目录下,共享库存放在哪个目录下以及链接哪些共享库等,我们将其称为库的元信息。通常,这些信息都被保存在一个以软件包的名称命名,并以 “.pc” 作为扩展名的文件中。而 pkg-config 会到特定的目录下寻找这些 pc 文件,一般而言,其首先搜索环境变量 PKG_CONFIG_PATH 指定的目录,然后搜索默认路径,一般是 /usr/lib/pkgconfig、/usr/share/pkgconfig、/usr/local/lib/pkgconfig 等。显然,使用环境变量 PKG_CONFIG_PATH 不能满足我们的要求。因为在交叉编译环境中,我们是不能允许正在编译的程序链接到宿主系统的库上的,也就是说,我们除了告诉 pkg-config 到目标系统的文件系统中寻找外,还要禁止它搜索默认的宿主系统的路径。而另外一个环境变量 PKG_CONFIG_LIBDIR 可以满足我们这个需求,一旦设置了 PKG_CONFIG_LIBDIR ,其将取代 pkg-config 默认的搜索路径。因此,在交叉编译时,这两个变量的设置如下:
unset PKG_CONFIG_PATH export PKG_CONFIG_LIBDIR=$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig
10、关于使用libtool链接库的讨论
GNU 中的大部分软件包都使用 libtool 处理库的链接。通常,大部分的软件在包发布时都已经包含了 libtool 所需的脚本工具等。但是如果一旦准备使用 autoconf、automake 重新生成编译脚本,且这些脚本中包含了 libtool 提供的 M4 宏,则需要安装 libtool 。
11、启动代码
启动代码是工具链中 C 库和编译器都提供了的重要部分之一,但是由于应用程序员很少接触它们,因此非常容易引起程序员的困惑,所以我们特将其单独列出,使用一点篇幅加以讨论。
不知读者是否留意过这个问题:无论是在 DOS 下、Windows 下,还是在 Linux 操作系统下,程序员使用 C 语言编程时,几乎所有程序的入口函数都是 main,这是因为启动代码的存在。在 “hosted environment” 下,应用程序运行在操作系统之上,程序启动前和退出前需要进行一些初始化和善后工作,而这些工作与 “hosted environment” 密切相关,并且是公共的,不属于应用程序范畴的事情,这些应用程序员无需关心。更重要的一点是,有些初始化动作需要在 main 函数运行前完成,比如 C++ 全局对象的构造。有些操作是不能使用 C 语言完成的,必须要使用汇编指令,比如栈的初始化。于是编译器和 C 库将它们抽取出来,放在了公共的代码中。
这些公共代码被称为启动代码,其实不只是程序启动时,也包括在程序退出时执行的一些代码,我们统称它们为启动代码,并将启动代码所在的文件称为启动文件。对于 C 语言来说,Glibc 提供启动文件。显然,对于 C++ 语言来说,因为启动代码是和语言密切相关的,所以其启动代码不在 C 库中,而由 GCC 提供。这些启动文件以 “crt”(可以理解为 C RunTime 的缩写)开头、以 “.o” 结尾。