前言
内核的构建系统 kbuild 基于GNU Make,是一套非常复杂的系统。
对于编译内核而言,一条 make 命令就足够了。因此,构建内核最困难的地方不是编译,而是编译前的配置。配置内核时,通常我们都能找到一些参考。比如,对于桌面系统,可以参考主流发行版的内核配置。但是,这些发行版为了能够在更多的机器上运行,几乎选择了全部的配置选项,编译了全部的驱动,不仅增加了内核的体积,还降低了内核的运行速度。再比如,对于嵌入式系统,BSP(Board Support Package)中通常也提供内核,但他们通常也仅是个可以工作的内核而已。显然,如果要一个占用空间更小、运行更快的内核,就需要开发人员手动配置内核。而且,也确实存在着在某些情况下,我们找不到任何合适的参考,这时我们只能以手动方式从零开始配置。
一、内核映像的组成
1、一级推进系统——setup.bin
在进行内核初始化时,需要一些信息,如显示信息、内存信息等。曾经,这些信息由工作在实模式下的 setup.bin 通过 BIOS 获取,保存在内核中的变量 boot_params 中,变量 boot_params 是结构体 boot_params 的一个实例。
2、二级推进系统——内核非压缩部分
内核的保护模式部分是经过压缩的,因此运行前需要解压缩,但是谁来负责内核映像的解压呢?解铃还须系铃人,既然内核在构建时自己压缩了自己,当然解压缩也要由内核映像自己完成。
内核在压缩的映像外包围了一部分非压缩的代码,Bootloader 在加载内核映像后跳转至外围的这段非压缩部分。这些没有经过解压缩的指令可以直接送给 CPU 执行,由这段 CPU 可执行的指令负责解压内核的压缩部分。
除了解压以外,非压缩部分还负责内核重定位。内核可以配置为可重定位的(relocatable),所谓可重定位即内核可以被 Bootloader 加载到内存任何位置。但是在链接内核时,链接器需要假定一个加载地址,然后以这个假定地址为参考,为各个符号分配运行时地址。显
然,如果加载地址和链接时假定的地址不同,那么需要对符号的地址进行重新修订,这就是内核重定位。
内核非压缩部分工作在保护模式下,其占用的内存在完成使命后将会被释放。
3、映像的格式
在 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 的构建也是同样的道理。因此,组成内核映像的各个部分的构建顺序如下:
- 构建有效载荷 vmlinux,并将其压缩为 vmlinux.bin.gz;
- 构建二级推进系统,并将二级推进系统装配到有效载荷上,组成 vmlinux.bin;
- 构建一级推进系统,即构建 setup.bin;
- 将 setup.bin 和 vmlinux.bin 组合为 bzImage。
三、配置内核
内核提供了 make menuconfig、make xconfig、make gconfig 等具有图形界面的配置方式。make menuconfig 是图形界面配置方式中最简陋的一种,但是却非常方便易用,依赖也最小。其他如 make xconfig、make gconfig 需要 QT、GTK+ 等库的支持。在本书中,我们使用 make menuconfig 配置内核,其简单地基于终端的图形界面是使用 ncurses 编写的,因此需要安装 libncurses5-dev 。
apt install libncurses5-dev
1、交叉编译内核设置
在默认情况下,内核构建系统默认内核是本地编译,即编译的内核是运行在与宿主系统相同的体系架构上。如果是为其他的架构编译内核,即交叉编译,我们需要设置两个变量: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-
2、基本内核配置
在很多情况下,我们都会有一个目标系统的老版本内核配置文件,而不必每次都从零开始。在此种情况下,首先将已有的内核配置文件复制到顶层目录下,并命名为 .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
3、内核启动的第一程序
如果用户没有通过内核命令行参数 “init” 指定第一个进程运行的用户空间的程序,则内核依次尝试执行目录 /sbin、/etc、/bin下的 init,最后尝试执行目录 /bin 下的 sh 。因此,我们在目录 /bin 下建立一个指向 bash 的符号链接 sh,而且,这个符号链接也是 FHS 标准要求的。
四、构建基本根文件系统
1、根文件系统的基本目录结构
Linux 的根文件系统的目录结构不是随意定义的,而是依照 Filesystem Hierarchy Standard Group 制定的 Filesystem Hierarchy Standard(FHS) 标准。从服务器、个人计算机到嵌入式系统,虽达不到完全符合,但大体上还是遵循这个标准的。
根文件系统中主要有四处存放可执行程序的目录:/bin、/sbin、/usr/bin 和 /usr/sbin 。系统管理员和普通用户都使用的重要命令保存在 /bin 目录下,而仅由系统管理员使用的重要命令则保存在 /sbin 目录下。相应的,不是很重要的命令则分别放置在 /usr/bin 和 /usr/sbin 目录下。
同样的道理,重要的系统库一般存放在 /lib 目录下,其他的库则存放在 /usr/lib 目录下。
2、安装C库
几乎所有程序都依赖 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/
3、安装shell
在安装 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