深度探索Linux操作系统 —— 构建内核

简介: 深度探索Linux操作系统 —— 构建内核

前言

    内核的构建系统 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.binvmlinux.bin,所以在构建 bzImage 前,make 将自动先去构建它们,以此类推,vmlinux 的构建也是同样的道理。因此,组成内核映像的各个部分的构建顺序如下:

  1. 构建有效载荷 vmlinux,并将其压缩为 vmlinux.bin.gz;
  2. 构建二级推进系统,并将二级推进系统装配到有效载荷上,组成 vmlinux.bin;
  3. 构建一级推进系统,即构建 setup.bin;
  4. 将 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、交叉编译内核设置

    在默认情况下,内核构建系统默认内核是本地编译,即编译的内核是运行在与宿主系统相同的体系架构上。如果是为其他的架构编译内核,即交叉编译,我们需要设置两个变量:ARCHCROSS_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 initSystemd 等都提供一个二进制程序作为第一个进程执行的用户空间的程序,但是为简单起见,我们使用 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
目录
相关文章
|
11天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
34 1
|
4天前
|
安全 Anolis
龙蜥社区落地开源生态发展合作倡议,构建开放兼容的操作系统生态
通过共同努力,三个社区基于服务器操作系统场景,在操作系统内核等关键共性技术链统一方面达成了一致。
|
1天前
|
人工智能 安全 Android开发
移动应用开发与操作系统的深度协同:构建高效、安全的移动生态####
【10月更文挑战第21天】 本文深入探讨了移动应用开发与移动操作系统之间的内在联系与相互影响,强调了两者在构建高效、安全移动生态系统中的关键作用。通过分析当前主流移动操作系统(如Android、iOS)的特性及发展趋势,结合移动应用开发的最新技术与挑战,本文旨在为开发者提供一套全面的理解框架,以促进更加协同高效的应用开发实践。 ####
31 18
|
13天前
|
安全 Linux 数据安全/隐私保护
深入Linux操作系统:文件系统和权限管理
在数字世界的海洋中,操作系统是连接用户与硬件的桥梁,而Linux作为其中的佼佼者,其文件系统和权限管理则是这座桥梁上不可或缺的结构。本文将带你探索Linux的文件系统结构,理解文件权限的重要性,并通过实际案例揭示如何有效地管理和控制这些权限。我们将一起航行在Linux的命令行海洋中,解锁文件系统的奥秘,并学习如何保护你的数据免受不必要的访问。
|
11天前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
11天前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
12天前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
12天前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
7月前
|
Ubuntu Linux Shell
【Linux操作系统】探秘Linux奥秘:shell 编程的解密与实战
【Linux操作系统】探秘Linux奥秘:shell 编程的解密与实战
112 0
|
7月前
|
Ubuntu Linux Shell
【Linux操作系统】探秘Linux奥秘:用户、组、密码及权限管理的解密与实战
【Linux操作系统】探秘Linux奥秘:用户、组、密码及权限管理的解密与实战
148 0