深度探索Linux操作系统 —— 编译过程分析
深度探索Linux操作系统 —— 构建工具链
深度探索Linux操作系统 —— 构建内核
深度探索Linux操作系统 —— 构建initramfs
深度探索Linux操作系统 —— 从内核空间到用户空间
深度探索Linux操作系统 —— 构建根文件系统
深度探索Linux操作系统 —— 构建桌面环境
深度探索Linux操作系统 —— Linux图形原理探讨
内核的构建系统 kbuild 基于GNU Make,是一套非常复杂的系统。
对于编译内核而言,一条 make 命令就足够了。因此,构建内核最困难的地方不是编译,而是编译前的配置。配置内核时,通常我们都能找到一些参考。比如,对于桌面系统,可以参考主流发行版的内核配置。但是,这些发行版为了能够在更多的机器上运行,几乎选择了全部的配置选项,编译了全部的驱动,不仅增加了内核的体积,还降低了内核的运行速度。再比如,对于嵌入式系统,BSP(Board Support Package)中通常也提供内核,但他们通常也仅是个可以工作的内核而已。显然,如果要一个占用空间更小、运行更快的内核,就需要开发人员手动配置内核。而且,也确实存在着在某些情况下,我们找不到任何合适的参考,这时我们只能以手动方式从零开始配置。
在进行内核初始化时,需要一些信息,如显示信息、内存信息等。曾经,这些信息由工作在实模式下的 setup.bin 通过 BIOS 获取,保存在内核中的变量 boot_params 中,变量 boot_params 是结构体 boot_params 的一个实例。
内核的保护模式部分是经过压缩的,因此运行前需要解压缩,但是谁来负责内核映像的解压呢?解铃还须系铃人,既然内核在构建时自己压缩了自己,当然解压缩也要由内核映像自己完成。
内核在压缩的映像外包围了一部分非压缩的代码,Bootloader 在加载内核映像后跳转至外围的这段非压缩部分。这些没有经过解压缩的指令可以直接送给 CPU 执行,由这段 CPU 可执行的指令负责解压内核的压缩部分。
除了解压以外,非压缩部分还负责内核重定位。内核可以配置为可重定位的(relocatable),所谓可重定位即内核可以被 Bootloader 加载到内存任何位置。但是在链接内核时,链接器需要假定一个加载地址,然后以这个假定地址为参考,为各个符号分配运行时地址。显
然,如果加载地址和链接时假定的地址不同,那么需要对符号的地址进行重新修订,这就是内核重定位。
内核非压缩部分工作在保护模式下,其占用的内存在完成使命后将会被释放。
在 Linux 作为操作系统的 hosted environment 环境下,二进制文件使用 ELF 格式,操作系统也提供 ELF 文件的加载器。但是,操作系统本身确是工作在 freestanding environment 环境下。操作系统显然不能强制要求 Bootloader 也提供 ELF 加载器。而且,操作系统映像也没有必要使用 ELF 格式来组织,将代码和数据顺次存放即可,即所谓的裸二进制格式。所以,内核映像都采用裸二进制格式进行组织。
但是,从 Linux 2.6.26 版本开始,内核的压缩部分,即有效载荷部分,采用了 ELF 格式。这样做可以支持 “the Xen domain builder” 的 Bootloader。
我们知道,在解压内核映像后,将会跳转到解压映像的开头执行。但是,ELF 文件的开头并不是代码段的开始,而是 ELF 文件头,也就是说,并不是 CPU 可执行的机器指令。显然,当内核映像不是裸二进制格式时,我们需要有一个 ELF 加载器来将 ELF 格式的内核映像转化为裸二进制格式。那么谁来充当这个 ELF 加载器呢?
正所谓“螳螂捕蝉,黄雀在后”。内核的非压缩部分调用函数 decompress 解压内核后,紧接着就调用了函数 parse_elf 来处理ELF格式的内核映像。
事实上,如果 Bootloader 不是所谓的 “the Xen domain builder” ,我们完全没有必要保留内核的压缩部分为 ELF 格式,并略去启动时进行的 “parse_elf” 。
在编译内核时,通常我们只需要执行 “make bzImage” ,或者 make 后面不接任何目标。在没有接目标时,构建的内核映像也是 bzImage。
# linux-3.7.4/arch/x86/boot/Makefile:
$(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin \
$(obj)/tools/build FORCE
根据构建规则可见,bzImage 依赖于 setup.bin 和 vmlinux.bin,所以在构建 bzImage 前,make 将自动先去构建它们,以此类推,vmlinux 的构建也是同样的道理。因此,组成内核映像的各个部分的构建顺序如下:
内核提供了 make menuconfig、make xconfig、make gconfig 等具有图形界面的配置方式。make menuconfig 是图形界面配置方式中最简陋的一种,但是却非常方便易用,依赖也最小。其他如 make xconfig、make gconfig 需要 QT、GTK+ 等库的支持。在本书中,我们使用 make menuconfig 配置内核,其简单地基于终端的图形界面是使用 ncurses 编写的,因此需要安装 libncurses5-dev 。
apt install libncurses5-dev
在默认情况下,内核构建系统默认内核是本地编译,即编译的内核是运行在与宿主系统相同的体系架构上。如果是为其他的架构编译内核,即交叉编译,我们需要设置两个变量:ARCH 和 CROSS_COMPILE 。其中:
ARCH 指明目标体系架构,即编译好的内核运行在什么平台上,如 x86、arm 或 mips 等。
CROSS_COMPILE 指定使用的交叉编译器的前缀。对于我们的交叉工具链来说,其前缀是 i686-none-linux-gnu- 。
在顶层的 Makefile 中,我们可以看到工具链中的编译器、链接器等均以 $(CROSS_COMPILE) 作为前缀:
# linux-3.7.4/Makefile:
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
可以使用多种方式定义这两个变量,比如通过在环境变量中定义 ARCH、CROSS_COMPILE ;或者每次执行 make 时,通过命名行为这两个变量的赋值,如:
make ARCH=i386 CROSS_COMPILE=i686-none-linux-gnu-
也可以直接更改顶层 Makefile 。这种方法比较方便,但是要小心,以免破坏 Makefile 文件。本书中我们采用这种方式,将顶层 Makefile 中的如下脚本:
# linux-3.7.4/Makefile:
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?= $(CONFIG_CROSS_COMPILE:"%"=%)
更改为:
# linux-3.7.4/Makefile:
ARCH ?= i386
CROSS_COMPILE ?= i686-none-linux-gnu-
在很多情况下,我们都会有一个目标系统的老版本内核配置文件,而不必每次都从零开始。在此种情况下,首先将已有的内核配置文件复制到顶层目录下,并命名为 .config;然后运行 make oldconfig,其将会询问用户如何处理变动的内核配置;最后用户可以使用 make menuconfig 进行微调。虽然内核提供 make oldconfig 的方法,但是这些方法并不是完美的,读者需要小心处理新内核中新增或改变的配置项。
但是也有很多情况,已有配置并不理想,我们需要进行更彻底定制,或者我们根本找不到一个合适的已有配置。难道我们就别无选择,只能从零开始了吗?当然不是,内核构建系统已经为开发者考虑了这些。
一方面内核为很多平台附带了默认配置文件,保存在 arch/<arch>/configs 目录下,其中 <arch> 对应具体的架构,如 x86、arm 或者 mips 等。比如,对于 x86 架构,内核分别提供了 32 位和 64 位的配置文件,即 i386_defconfig 和 x86_64_defconfig;对于 arm 架构,内核提供了如 NVIDA 的 Tegra 平台的默认配置 tegra_defconfig,Samsung 的 S5PV210 平台的默认配置 s5pv210_defconfig 等。
如果我们打算使用x86的32位的默认配置,执行下面命令即可:
make i386_defconfig
如果想使用Samsung的S5PV210平台的默认配置,则使用如下命令:
make ARCH=arm s5pv210_defconfig
如果对这些内核内置的默认配置依然不满意,kbuild 还提供了创建一个最小配置的方法,从某种意义上讲,这是最彻底的定制方式了,命令如下:
make allnoconfig
如果用户没有通过内核命令行参数 “init” 指定第一个进程运行的用户空间的程序,则内核依次尝试执行目录 /sbin、/etc、/bin下的 init,最后尝试执行目录 /bin 下的 sh 。因此,我们在目录 /bin 下建立一个指向 bash 的符号链接 sh,而且,这个符号链接也是 FHS 标准要求的。
Linux 的根文件系统的目录结构不是随意定义的,而是依照 Filesystem Hierarchy Standard Group 制定的 Filesystem Hierarchy Standard(FHS) 标准。从服务器、个人计算机到嵌入式系统,虽达不到完全符合,但大体上还是遵循这个标准的。
根文件系统中主要有四处存放可执行程序的目录:/bin、/sbin、/usr/bin 和 /usr/sbin 。系统管理员和普通用户都使用的重要命令保存在 /bin 目录下,而仅由系统管理员使用的重要命令则保存在 /sbin 目录下。相应的,不是很重要的命令则分别放置在 /usr/bin 和 /usr/sbin 目录下。
同样的道理,重要的系统库一般存放在 /lib 目录下,其他的库则存放在 /usr/lib 目录下。
几乎所有程序都依赖 C 库,它是整个系统的基础,因此,我们首先安装 C 库到根文件系统。在前面讨论编译构建系统的 C 库时,我们看到,C 库包含函数库、各种工具程序,以及开发所需的头文件等。而这里的文件系统只是个临时系统,所以 C 库中的各种实用工具及 $SYSROOT/usr/share 目录下的数据文件,都不需要安装。而且这个临时根文件系统亦不需要支撑开发,所以凡是开发时所需要的文件,包括头文件、静态库、启动文件等,也不需要安装。因此,最终我们只需要安装 $SYSROOT/lib 目录下的动态库及相应的动态链接/加载器需要的符号链接。
我们新建一个保存目标系统的根文件系统的 rootfs 目录,并且按照 FHS 标准的规定,将 C 库安装在 rootfs/lib 目录下,命令如下:
mkdir rootfs
mkdir rootfs/lib
cp -d sysroot/lib/* rootfs/lib/
除了 Glibc 中包含的 C 库外,在前面编译 GCC 时,我们也看到,GCC 也将部分底层函数封装到库中,有些程序会使用 GCC 的这些库,因此,我们也将这部分程序安装到 rootfs/lib 目录中。同样,我们也只安装动态库及其对应的运行时符号链接,命令如下:
cp -d cross-tool/i686-none-linux-gnu/lib/lib*.so.*[0-9] rootfs/lib/
在安装 C 库后,构建基本的应用程序的基础已经具备了,接下来我们需要为内核准备用户空间的程序了。在 Linux 中,专门负责启动的软件包,如 System V init 和 Systemd 等都提供一个二进制程序作为第一个进程执行的用户空间的程序,但是为简单起见,我们使用 bash shell 。安装 bash 的命令如下:
wget https://ftp.gnu.org/gnu/bash/bash-4.2.tar.gz
tar -xf ../source/bash-4.2.tar.gz
./configure --prefix=/usr --bindir=/bin --without-bash-malloc
make
make install DESTDIR=$SYSROOT
# /vita/cross-tool/bin/ldd:
#!/bin/bash
LIBDIR="${SYSROOT}/lib $(SYSROOT}/usr/lib ${CROSS_TOOL}/${TARGET}/lib"
find() {
for d in $LIBDIR; do
found=""
if [ -f "${d}/$1" ]; then
found="${d}/$1"
break
fi
done
if [ -n "$found" ]; then
printf "%8s%s => %s\n" "" $1 $found
else
printf "%8s%s => (not found)\n" "" $1
fi
}
readelf -d $1 | grep NEEDED \
| sed -r -e 's/.*Shared library:[ ]+\[(.*)\]/\1/;' \
| while read lib; do
find $lib
done