Linux内核的配置与编译

时间:2022-10-25 12:28:37

1 内核编译过程

学习Linux内核除了必备的基础知识、搭建Linux环境、下载内核源码和准备相关资源之外,第一件要做的事情应该就是编译自己的Linux内核,然后运行编译出来的内核。内核从配置,到编译,再到安装的命令非常简单,只要按顺序执行下面几个命令就可完成:

1.内核配置:make menuconfig

2.内核编译:make

3.安装模块:make modules_install

4.安装内核:make install

按顺序执行完上面的命令后,重启Linux系统,就是启动刚编译好的内核了。虽然Linux内核的编译很简单,但若想进一步的学习和定制Linux,就必须进一步了解内核编译过程及原理。下面详细的介绍Linux内核的编译过程。

虽然Linux内核按默认的配置和编译很简单,但若想进一步的学习、定制和开发内核,就必须进一步了解内核编译过程及原理。下面按内核的编译顺序进行详细的介绍。

1.1 内核配置

Linux内核在下载解压后需要先配置然后才能编译。源码树的每个目录下都有一个文件Kconfig,这个分布到各目录的Kconfig构成了一个分布式的内核配置数据库,每个Kconfig分别描述了所属目录源文档相关的内核配置菜单。在执行内核配置命令时,从Kconfig中读出菜单的信息供用户进行选择,配置的命令如下:

1)make config:基于文本的最为传统的一行一行的配置命令配置,不推荐使用。

2)make menuconfig:基于文本选单的配置界面,字符终端下推荐使用。

3)make xconfig:基于图形窗口模式的配置界面,Xwindow下推荐使用。

以上命令会在scripts/kconfig/目录下生成一个小工具,如:make menuconfig会生成mconf,然后用这个小工具根据各级目录的Kconfig文件在界面上展现选项供用户选择,执行完后生成一个.config文件保存用户的内核配置选项,选择相应的配置时,有三种选择,它们分别代表的含义如下:

Y--将该功能编译进内核。

N--不将该功能编译进内核。

M--将该功能编译成可以在需要时动态插入到内核中的模块。

make menuconfig命令运行界面就是由mconf小工具显示的,界面如下:
Linux内核的配置与编译


根据自己的需要选择适当的配置项,如果没有特别的目的,按默认值配置即可,配置程序结束将在源码主目录生成一个.config文件,在内核编译时,主Makefile调用这个.config就知道了用户的选择了。

1.2 编译内核

从Linux内核2.6开始,Linux内核的编译采用Kbuild系统,这和过去的编译系统有很大的不同,尤其对于Linux内核模块的编译。在新的系统下,Linux编译系统会两次扫描Linux的Makefile:首先编译系统会读取Linux内核顶层的Makefile,然后根据读到的内容第二次读取Kbuild的Makefile来编译Linux内核。

1.2.1 生成vmlinux

顶层 Makefile 有两个主要的目标,第一个目标就是生成vmlinux内核映像。顶层 Makefile 包含一个体系结构 Makefile,由 arch/$(ARCH)/Makefile 指定,为顶层 Makefile 提供了特定体系结构的信息。每个子目录各有一个 Kbuild文件和Makefile 文件用来执行从上层传递下来的命令。Kbuild和Makefile利用.config 文件中的信息来构造得到没有压缩的内核vmlinux。

vmlinux是未压缩的、原始的内核映像,编译出来后放在源码主目录里,此文件是ELF格式的文件,通过readelf -S vmlinux命令可以查看里面的各部分的细节。“vm”代表“Virtual Memory”,Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制,Linux能够使用硬盘空间作为虚拟内存,因此得名“vm”。

1.2.2 生成System.map

未压缩的vmlinux生成后,make程序调用nm命令生产System.map。System.map是内核符号表文件。Linux内核是一个很复杂的代码块,有许许多多的全局符号,Linux内核不使用符号名,而是通过变量或函数的地址来识别变量或函数名,比如像c0343f20这样引用一个变量。虽然内核本身并不真正使用System.map,对于进行程序设计来说,人们更喜欢使用那些像size_t BytesRead这样的名字,而不喜欢像c0343f20这样的地址,所以编译器/连接器允许我们编码时使用符号名,而内核运行时使用地址。在有的情况下,我们需要知道符号的地址,或者需要知道地址对应的符号,比如klogd,lsof和ps等软件需要一个正确的System.map,如果你使用错误的或没有System.map,klogd的输出将是不可靠的,这对于排除程序故障会带来困难。另外少数驱动需要System.map来解析符号,没有为你当前运行的特定内核创建的System.map它们就不能正常工作。所以System.map对需要知道内核符号与地址对应关系的程序来说是相当重要的。

1.3 生成可引导的内核映像

未压缩的vmlinux映像编译出来后可能有100+MB,这么大的文件是需要通过压缩才能生成能被Bootloader加载的内核映像。编译时会先压缩vmlinux映像,然后生成setup引导程序,最后把压缩的vmlinux映像和setup引导程序打包成可引导的内核映像文件。

1.3.1 压缩vmlinux

未压缩的vmlinux编译出来后放在源码的主目录下,压缩的vmlinux编译出来后放在arch/x86/boot/compressed/目录下,两个文件是不一样的,别搞混淆了。具体的压缩步骤如下图所示:
Linux内核的配置与编译

(1)通过objcopy命令把原始的vmlinux文件中的调试等信息去掉生产vmlinux.bin文件,此文件是还没有压缩的ELF文件,通过readelf命令可以比较出它和原始vmlinux文件的区别。

(2)编译一个小程序relocs,把原始的vmlinux中的需要重定位值的地址取出写入vmlinux.relocs文件,然后,将vmlinux.bin和vmlinux.relocs通过gzip压缩成vmlinux.bin.gz。

(3)编译一个小程序mkpiggy,计算vmlinux.bin.gz的压缩前大小,解压后大小,解压偏移地址等信息,生产一个小汇编文件piggy.S。此文件通过.incbin伪指令直接将vmlinux.bin.gz二进制文件插入到目标文件中的input_data下标处,编译此汇编文件将会生产一个包含有vmlinux.bin.gz二进制数据的piggy.o文件。

(4)把vmlinux.lds(链接描述文件,指明入口点为startup_32,各节的VMA等)、head_32.o(包含入口点startup_32的代码,然后调用解压程序)、misc.o(包含解压算法的代码)、piggy.o文件一起链接生成压缩后的vmlinux映像。

压缩后的vmlinux映像也是ELF文件通常只有几MB,里面包含了自解压的代码,用readelf -S vmlinux命令可以看到里面有一个.rodata.compressed节,存放着压缩的内核映像数据。

1.3.2 生成setup程序

光有压缩vmlinux内核映像是不够的,因为Bootloader运行在实模式下功能及寻址能力都很有限,所以需要加入一个程序用来接管Bootloader的控制权、初始化系统、打开保护模式等,这个程序就是内核setup程序。编译时make程序把setup.ld、及arch/x86/boot/目录下的main.c、pm.c、pmjump.S、video.c等源文件编译生成setup.elf程序。

1.3.3 生成bzImage

setup.elf程序和压缩的vmlinux都是ELF格式的文件。因为ELF文件附加了很多如文件头、符号表、重定位表等信息,这些信息对Bootloader加载内核是没有意义的,所以make程序通过objcopy命令提取setup.elf和压缩的vmlinux中有效的裸二进制数据生成setup.bin和vmlinux.bin两个文件,然后编译一个小程序build,把setup.bin和vmlinux.bin合在一起生成bzImage。

bzImage是可引导的、压缩的内核映像。虽然它里面包含了压缩的vmlinux,但不能通过gunzip 或 gzip解压,只能通过Bootloader引导和解压。bzImage是编译内核时通过命令make bzImage创建,需要注意,bzImage不是用bzip2压缩的,bzImage中的bz容易引起误解,bz表示“big zImage”的意思,它解压缩到高端内存(1M以上)。与bzImage对应的是zImage,不过现在PC中很少用到一般用于嵌入式系统,编译时通过“make zImage”创建的,适用于小内核的情况,它解压缩到低端内存(第一个640K)。bzImage和zImage两种方式引导的系统运行时是相同的,如果内核比较小,那么可以采用zImage 或bzImage之一,大的内核必须采用bzImage,不能采用zImage。

1.4 编译及安装模块

Linux内核是由一个个模块组成的一个集合,这些模块可以在编译内核时一起编译到内核中,也可以在运行时动态地加载到内核中,内核模块是Linux向外部提供的一个接口,其全称是:动态可加载内核模块(Loadable Kernel Module,LKM),简称模块。Linux之所以提供模块机制,是因为Linux内核本身是一个单内核(Monolithic Kernel)结构,最大的优点就是效率高,因为所有内容都集成在一起,但其缺点是可扩展性和可维护性较差,模块机制就是为了弥补这一缺陷而设计的。模块是具有独立功能的程序,它可以被单独编译,但是不能单独运行,因为它没有单独的main()函数,只有初始化函数,模块在运行时被链接到内核并作为内核的一部分在内核空间中运行,这与运行在用户空间下的进程是不同的,模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序、一个操作系统服务、增加一个新的系统调用,或其它内核上层的功能。总之,模块是一个为内核或其他内核模块提供使用功能的代码块。

顶层 Makefile 编译出vmlinux后将会编译 modules(所有模块文件),首先生成组成模块的对应 .o 文件和 .mod 文件,然后用 scripts/mod/modpost 来生成 .mod.c 文件,并将其编译成 .mod.o 对象文件,最后将 .mod.o 连同前面的 .o 一起链接成 .ko 模块文件。另外,在生成vmlinux的过程中,会在内核顶层目录中生成一个 Modules.symvers,里面存放基本内核导出的、供模块使用的符号以及CRC校验和。

编译完所以模块后,内核的编译任务就算完成了,接下来就是调用make  modules_install安装模块,安装时调用脚本/sbin/installkernel根据在内核配置、编译阶段生成的内核模块以及模块依赖关系/lib/modules/<version>/modules.dep制模块文件到/lib/modules/<version>目录下,安装完成。

1.5 安装内核

安装完模块后接着安装内核,make install命令调用了内核目录中的install.sh的shell脚本完成安装任务。

1.5.1 生成initramfs

当Linux内核启动系统时,它必须找到并执行第一个用户程序通常被称为init,方能成功开机。由于用户程序是保存在文件系统中的,因此Linux内核必须先找到并挂上第一个“根”文件系统。通常,可用的文件系统列在文件/etc/fstab里,以便mount能够找到它们。但/etc/fstab本身就是一个保存在文件系统中文件中。所以找到第一个文件系统成为鸡生蛋蛋生鸡的问题。

为了解决这个问题,内核开发者建立内核命令行选项 “root=”,用来指定root文件系统在哪个设备上。十五年前,“root=”所指的设备很容易找到,因为它不是在软盘上就是硬盘的分区上。如今root文件系统可以保存在大量各种不同类型的硬件(SCSI, SATA, flash MTD,USB)上,甚至是由不同类型硬件所建立的RAID上。root文件系统还可以存在主机外部的网络服务器上(内核找到“root”前得先取得DHCP地址,完成DNS lookup,并 使用帐号及密码 登入到远端服务器)。 root文件系统的位置很可能每次启动都在不同的位置。除了位置策略,root文件系统的保存方式也存在策略需要,比如,root文件系统也可能被压缩(如何?),被加密(用什么keys?),或 loopback挂接(哪里?)……不管处理root的策略如何,记住内核最终的目标还是找到根文件系统并执行其上的第一个init程序,完成开机。

面对如此多的动态策略,单一命令行参数(root=)远远不能满足了。即使将所有特殊案例的行为(设备列举,加密钥,或网络登入)都硬编码入进内核也无济于事,因为特殊的系统太多了。更糟的是,替内核加入这些复杂行为的工作,就像是用汇编语言写web软件:即便可以做到,但也不能很划算很容易的通过使用适当工具完成。随着内核的发展,为了解决这个无休止甚至在不断增加复杂度的问题,内核开发者决定收回现有解决方案,重新寻求更好的整体的解决方案。

Linux 2.6内核将一个小的ram-based initial root filesystem(initramfs)连接入内核,initramfs一个压缩过的cpio格式的打包文件。当内核启动时,会从这个打包文件中导出文件到内核的rootfs文件系统, 然后内核检查rootfs中是否包含有init文件,如果有则执行它,作为PID为1的第一个进程。这个init进程负责启动系统后续的工作,包括定位、 挂载“真正的”根文件系统设备(如果有的话)。如果内核没有在rootfs中找到init文件,则内核会按以前版本的方式定位、挂载根分区,然后执行 /sbin/init程序完成系统的后续初始化工作。

安装内核时make install命令调用了内核目录中的install.sh的shell脚本来完成安装任务。install.sh调用/sbin/installkernel脚本,/sbin/installkernel最终调用/sbin/dracut把initramfs安装到/boot目录下。

1.5.2 生成vmlinuz

vmlinuz是最终安装后的Linux内核,放在/boot目录下,并不会出现在源码目录中,文件名可能带有版本后缀等信息,它其实是bzImage或zImage安装后的别名。 安装内核命令调用了内核目录中的install.sh的shell脚本。该脚本首先将bzImage、System.map复制到/boot目录,并将这两个文件依次改名为vmlinuz-<version>,System.map-<version>。接着修改/boot/grub/grub.conf文件、添加新的引导菜单,安装完成。

内核编译安装完成之后,就可以重启系统了。