【linux】Linux Kernel Makefile简析 之 make zImage

本文将简要分析Linux Kernel编译zImage的过程。读者需具备GNU Make、Bash Shell、Python脚

本、编译器、链接器等方面的基础知识。虽然重点是分析kernel的构建过程,但是也会顺带的分析一些

其他的小的知识点。我们坐车去远行,欣赏沿途的风景,并不会妨碍我们最终抵达我们的目的地,不是

吗?j_0061.gif

先描述一下具体开发环境:

. host os: ubuntu 14.04 server LTS

. cross toolchain: crosstool-ng生成的交叉编译器

. target: tegra, jeston-tk1

. kernel: 3.10.40



在开始分析之前,先做一些必要的说明:

. 分析过程中,所有文件路径,都是相对于kernel源码根目录的;

. 除顶层Makefile文件外,所有Makefile代码片段都会在第一行注释表明路径。



好了,接下来开始分析zImage的编译过程。

启动终端,在kernel源码根目录下运行命令. envsetup.sh设置编译环境。envsetup.sh的内容如

下:

#!/bin/bash

filename: envsetup.sh

export ARCH=arm
export CROSS_COMPILE=~/tegra/tk1/crosstool/crosstool-ng/install/bin/arm-cortex_a9-linux-gnueabi-
然后运行命令make zImage O=output编译kernel。make读取kernel源码根目录下的Makefile,看到

如下代码片断:

wKiom1mD4wCgi3kiAADVybJtaiQ952.png

图1

这里会先判断KBUILD_SRC的值是否为空,因为此时 KBUILD_SRC 尚未定义,所以这个判断是成的。

接着将KBUILD_OUTPUT赋为从命令行传过来的变量O的值,即:

KBUILD_OUTPUT := $(O) # KBUILD_OUTPUT := output
附带的变量saved-output也被赋成了同样的值output,然后将KBUILD_OUTPUT的值更新为output目录的完

整路径。KBUILD_OUTPUT是用来存储编译产生的输出的目录。

wKiom1mD50HjdmQmAAArOjHGcZ8930.png

图2

这里是很重要的片段,是进入构建最终目标的开端。上面图2中的代码片段此时被展开为:

wKiom1mD6ZDxg6TgAAAiFxKN4yQ160.png

图3

请注意,其中/path/to/output和/current/path都不是真实的目录名,具体的值依赖于kernel源码根目录的实际路径。/current/path在此时的值为kernel源码根目录所在的路径。

终于,我们看到了zImage这个我们编译的终极目标,它是依赖于sub-make的。但请记住,make在看

到zImage的产生规则以及其依赖目标的规则后,并不会马上去执行这两条规则,而只是先建立zImage和

sub-make的依赖关系。为什么会这样呢?原来make的执行过程是一个2遍扫描过程,这类似于一些编译器

的编译过程,并不是一次到位,而是分为几个阶段。make在第一遍扫描的时候,只是会去创建变量(并

展开某些类型变量)、建立目标依赖关系树,而只有在第二遍的时候才会去跟根据规则去进行目标的创

建过程。所以在这里zImage以及sub-make规则都不会被马上执行,而只是在make内部作了相应记录,而

后继续往后扫描Makefile剩下的内容。

这里有一个很重要的变量skip-makefile,被赋值为1,这个变量和我们接下来的分析密切相关。我

们继续看这个Makefile剩下的内容:

wKioL1mD7kWAqEh6AAAu6dsVt64465.png

图4

上图中代码用…省略了很多内容,中间的内容非常重要且核心,但对我们当前的第1遍是会被跳过的,

因为此时skip-makefile的值为1,所以ifeq ($(skip-makefile),)是不成立的,其间包含的内容自然也

被跳过。

FORCE这条特殊的空规则,在kernel所有的Makefile中,随时可以看到它的身影,所以有必要简单的

解释一下它的用途。FORCE常见于一条规则依赖列表的末尾,作用是保证依赖它的目标总是被更新。原因

是FORCE不对应任何实际文件(.PHONY伪目标),且不依赖任何其标,make会认为这类目标总是要被更新

的,相应依赖它的目标自然也要被更新。

打起精神,我们的重头戏马上要登场了。make完成对kernel顶层Makefile的第1编扫描后,接下来进

入make对Makefile处理的第2阶段,根据阶段1构建的依赖关系树开始一步步地构建我们的终极目标

zImage。起点是图2所示的位置,我们看图2内容展开后的图3,zImage依赖于sub-make,所以sub-make这

条规则。我们来看sub-make规则的展开后的内容:

wKioL1mEEnvRuhI_AAAOFnVV2Po492.png

图5

这条规则到底做了什么呢?这条规则完成了4个动作:

. make将自己的当前目录切换到了/path/to/output

. 设置KBUILD_SRC为当前目录(即kernel源码根目录)

. make将重新读取kernel的顶层Makefile(也就是我们当前正在执行的Makefile)

. 构造目标依旧是zImage

到这里你可能产生了疑问,怎么又回到了我们正在执行的Makefile呢?这不是死循环了,没完没了

了吗?事实并非如此。我们一步步来解释为什么。执行sub-make规则的语句后,make重新从头开始读取

kernel的顶层Makefile,时光流逝,make的脚步又重新走到了图1所示的代码片段处,这里的条件分支判

断KBUILD_SRC的值是否为空。记得吗?就在刚刚,我们重新进入Makefile之前,已经设置了KBUILD_SRC

的值为kernel顶层Makefile所在的路径(也是kernel源码根目录所在的路径),所以这个对KBUILD_SRC判空的分支是不成立的,自然分支里面的语句也不会被执行,因此和第1遍执行这个Makefile时有了不同的执行路径。

make继续前进到了图4所示的代码片段处,要根据skip-makefile的值决定我们接下来要去向哪里。

而这一次,skip-makefile并没有被定义,所以ifeq ($(skip-makefile),)的判定成立,因此make将执行

被该条件分支包含的代码片段,这将我们整个顶层Makefile分析的核心内容。这里面的内容很多,我们

将逐段进行分析,对代码片段截取也是逐段截图。

wKioL1mEG8iS9E_CAACQLCVfCvw612.png

图6

这段代码工作很简单,完成了几个变量的设置。在我的开发环境下,这些变量分别得到了如下的值(至

于如何得到的,就要看官们自己分析了):

srctree = /path/to/kernel/source/base
objtree = /path/to/kernel/source/base/output
src = /path/to/kernel/source/base
obj = /path/to/kernel/source/base/output
VPATH = /path/to/kernel/source/base
SUBARCH = x86
值得注意的是,srctree objtree VPATH被export了。继续往下看:

wKioL1mEH8HBaE3HAAC-Pc2z7RU326.png

图7(上接图6)

这段代码定义了一些变量,将得到如下所列的值:

ARCH = arm
CROSS_COMPILE = ~/tegra/tk1/crosstool/crosstool-ng/install/bin/arm-cortex_a9-linux-gnueabi-
UTS_MACHINE = arm
SRCARCH = arm
hdr-arch = arm
KCONFIG_CONFIG = .config
CONFIG_SHELL = /bin/bash
这些变量的重要性,我想也不用我多说了吧。路漫漫其修远兮,革命尚未成功,我们继续赶路吧。

wKioL1mEJpDyCyTMAAAMCLlnavo274.png

wKioL1mEJu6QSiNZAAB47JgyDmw161.png

wKiom1mEJ2-SpFFiAACOYi-rPXE986.png

wKioL1mEJ9yhOXV7AACNTYTjCYg654.png

wKioL1mHzMKQHcWhAADTJ14atfA246.png

wKioL1mH0C6RMs3CAAB4VXrdDvo178.png

wKiom1mEJreyY7C8AAADf0elG3w556.png

图8(上接图7)

上面的代码看起来内容很多,但仔细看起来,也只是做了一些变量的定义而已,如交叉编译工具链等。而

包含进来的文件Kbuild.include定义了一些常用的功能集合。

Kbuild.include中有一个变量build是出现频率也很重要的一个变量定义:

build := -f $(if ( K B U I L D S R C ) , (KBUILD_SRC), (KBUILDSRC),(srctree)/)scripts/Makefile.build obj
它用来调用Makefile.build来进行模块工具等的编译,调用方式为make $(build)=xxx。

接下来的内容要打起精神仔细看了:

wKiom1mH3ZrTfw2iAAAqhNeepdQ967.png

图9(上接图8)

这段代码判断最终的编译目标:是进行kernel配置,还是编译kernel和(或)module。上面的代码运行

后,变量的值在我们的make目标下,会得到如下值:

config-targets=0, mixed-targets=0, dot-config=1
接下来的代码条件判断分支有点多,我们先理清楚分支逻辑结构,然后再深入到符合条件的分支里

面分析细节。

wKiom1mH353gPZyfAACWXekRCkg445.png

图10(上接图9)

根据上面得到变量mixed-targets,config-targets的值,ifeq ($(mixed-targets),1)的条件分支测试

是不满足条件的,所以make进入到上面代码中的第11行及后面的代码。接着第12行又有条件分支测试

ifeq ($(config-targets),1),这里是判断我们是否要进行kernel的配置,即make *config(make

config, make menuconfig…)等。如果我们的目标是进行kernel配置,需要从这里开始分析代码。不过

我们当前的make目标是zImage,同时config-targets的值目前是0,所以make进入第33行省略号部分的代

码,这将是我们分析代码的终极目标。

继续看代码:

wKiom1mIAILiOgI0AADPbIHhhVs237.png

图11(上接图10中…部分)

上图代码上接图10中的…部分,逻辑也简单,不多解释。看接下来的代码:

wKiom1mIBOzwiFieAABbM_XZ3qg003.png

图12(上接图11)

看到了吗?上图中第21行的代码,进入了架构相关的Makefile,即arch/arm/Makefile。看代码:

wKiom1mICbnC-5voAAC6upGXE04819.pngwKiom1mICeeC9wCOAABlxBOV59g862.png

图13(ARCH Makefile)

上面的代码定义了一些平台相关的编译选项配置、代码目录、目标规则等。第39~46确定了KBUILD_IMAGE

的值为zImage。第63行规则描述zImage依赖于vmlinux,那我们就要看vmlinux是怎样生成的,这样又回

到了我们顶层的Makefile:

wKiom1mIEL6xbhggAAEQIz3VLFA346.png

wKiom1mIESWhCoCCAAEKUtXbL5I382.png

wKioL1mIEXzhSsxlAADofRJ2Kn8821.png

wKiom1mIEdHy1A8DAADBsjwqdzE691.png

wKiom1mIEivx09YyAABoNsKLqa0497.png

图14(上接图12)

上图中第62行给出了vmlinux的生成规则。该规则的最后1行:

+$(call if_changed,link-vmlinux)
展开为:

/bin/bash link-vmlinux $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux)
也就是调用scripts/link-vmlinux.sh链接生成vmlinux文件,当然这是生成vmlinux的最后一步。我们来

逐步vmlinux所依赖的文件的生成过程。

可以看到vmlinux依赖于vmlinux-deps以及其他2个目标,其他2个目标的生成过程比较简单,我们着

重分析vmlinux-deps的依赖树。vmlinux-deps是顶层代码目录的列表,从上面代码第76行可以看到它又

依赖于vmlinux-dirs,依次的vmlinux-dirs又依赖于prepare scripts(见上面代码第85行),对于这些

vmlinux-dirs依赖的目标,比较简单,在此不做分析,而且后面的分析过程和这这些目标的生成规则也

有重叠的地方,理解了我们将要展开的分析,也不难理解这些。

我们将场景切换到vmlinux-dirs所有依赖树已经展开完成,开始执行vmlinux-dirs规则的时刻:

wKioL1mJaASy9dmHAAAoq18Zea0318.png

图15

这里将从kernel源码顶层的各个子目录,逐级递归的、深度优先的执行各级Kbuild文件或Makefile。我

们来看这一过程是如何展开的。

以drivers目录为例,vmlinux-dirs规则展开为:

drivers: prepare scripts
@make -f ( s r c t r e e ) / s c r i p t s / M a k e f i l e . b u i l d o b j = d r i v e r s 其 中 (srctree)/scripts/Makefile.build obj=drivers 其中 (srctree)/scripts/Makefile.buildobj=drivers(srctree)当前为kernel源码根目录。也就是make切换到scripts/Makefile.build文件:

wKioL1mJcfXSTfaUAACz2Xhs5sM384.pngwKioL1mJci_Q9P_NAAC6o_qZsrA997.png

wKiom1mJcmOSRxjHAABOoxvG6Ug438.png

图16

上面是截取自文件的scripts/Makefile.build的代码片段。上面代码39-42的4行,包含了当前进入的子

目录的Kbuild或者Makefile,在我们drivers目录的没有Kbuild文件,所以包含进来的是

drivers/Makefile文件:

wKioL1mJc-mRllbvAAApxokCc-c266.png

图17

可以看到,定义了obj-y变量(和)可能的obj-m变量。是否定义了obj-m变量,取决于CONFIG_XXX是否定

义为模块编译方式。接下来回到scripts/Makefile.build的第46行,它包含了scripts/Makfile.lib文

件:

wKioL1mJdjrzTFixAADnt3WMoQY062.png

wKioL1mJdnzhp0MwAAD8wE_3Skk230.pngwKiom1mJdrig9iLPAACnE32a_H8839.png

图18

我们主要看第33~42,74对包含当前目录Makefile obj-y,obj-m变量的收集整理,最终汇集到变量

subdir-ym等之中。执行流程继续回到scripts/Makefile.build(图16)中的第62行,目标__build是make

当前目录执行时的最终目标。它依赖于builtin-target、subdir-ym等其他目标,其中builtin-target是

将当前目录下的所有代码编译整合成的一个名为built-in.o文件,这里完成了当前目录下的编译过程;

而subdir-ym是进一步递归编译的子目录列表,scripts/Makefile.build(图16)中的第95的规则是递归进

入子目录的地方,于是,又一次的执行scripts/Makefile.build,重复我们前面分析的过程,直到…直

到…所有的目录都编译完成,或生成编译所需工具,或生成built-in.o,等等。最终make的执行流程回

到图14第62行的vmlinux的规则处,并执行该规则,链接生成vmlinux,也就是我们的kernel。

千辛万苦,似乎已经走完了我们要走的路。可是,我们还有一些收尾的工作还没有完成,再坚持一

下吧。还记得吗?zImage才是我们的最中目标,从顶层Makefile继续推到架构特定的Makefile,即图13

中的第63行,这条规则在我们的执行情境下展开为:

zImage: vmlinux
@make -f $(srctree)/scripts/Makefile.build obj=arch/arm/boot MACHINE=tegra
arch/arm/boot/zImage
又进入了scripts/Makefile.build,重复前面的分析过程,包含了arch/arm/boot/Makefile:

wKioL1mJf-Tx4k6nAACWSAT0uzY064.png

图19

此时我们make的目标为arch/arm/boot/zImage,顺着依赖树,在一次objcopy动作后,然后执行第34的规

则,然后又一次的进入了Makefile.build,然后包含文件arch/arm/boot/compressed/Makefile编译我们

kernel的解压程序。看代码:

wKiom1mJgpzycgYBAAC4xTpr36E391.png

wKioL1mJgtmQQxsVAABZG***nXQ857.png

图20

注意上图中阴影部分代码,我用一个简单的加减公式来总结一下它们的执行过程:

arch/arm/boot/compressed/vmlinux = head.o + piggy.gzip.o + misc.o + decompress.o + 其他.o
vmlinux -> …/Image -> piggy.gzip -> piggy.gzip.o
注意,其中的两个vmlinux不是同一个文件,下面一行的vmlinux(kernel)是做为上面一行vmlinux的一部

分。在compressed目录下完成vmlinux对解压程序的打包以及kernel解压代码的编译后,生成了另一个

vmlinux文件,它的作用是完成对它包含的kernel进行解压并执行的任务。回到我们图19的Makfile文件

arch/arm/boot/Makefile,对arch/arm/boot/compressed/vmlinux进行一次objcopy操作,生成目标文件

arch/arm/boot/zImage。make再一次回退到图13中第62行的zImage规则处,终于我们走到了这次分析之

旅的终点,一切尘埃落定。

后记

这篇文章,写起来很累。一方面,是自己功力不足,未能对Kernel的Makefile做到举重若轻,游刃

有余;另一方面,不可否认,Kernel的Makefile也确实有些复杂。能坚持看完的,自己也能分析出来这

些东西,而且相信大部分人比我会做得更好。对于你们这样的观众来讲,这篇文章可谓鸡肋。所以,写

这篇文章主要还是我自己的对所得的一次梳理,也具备备忘录的功能。如果浪费了您的时间,只能说一

句抱歉了j_0057.gif。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值