一、既熟悉又陌生的内核模块Makefile
内核开发者刚刚入门时,都会学习写一个Hello World内核模块。这个内核模块中一定会包含一个Makefile文件。对于这个Makefile文件的内容和格式,几乎每个内核开发者都应该已经熟稔于心。
下面结合一个小例子,和大家一起回顾下这个再熟悉不过的内核模块Makefile。从git上获取示例。
$ git clone https://gitee.com/os-newbie/module_makefile.git
其中主要包含如下几个文件内容。
$ cd module_makefile/ $ ls libbie.c libbie.h Makefile newbie.c
下面是这个内核模块Makefile部分的内容。
$ cat Makefile ifneq ($(KERNELRELEASE),) obj-m+=newbs.o newbs-objs:=newbie.o libbie.o else KERNELVER:=$(shell uname -r) KERNELDIR:=/usr/src/kernels/$(KERNELVER)/ PWD:=$(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean endif
进入到module_makefile目录,执行make命令进行编译。
$ cd module_makefile/ $ make make -C /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/ M=/root/os-newbie/module_makefile modules make[1]: Entering directory `/usr/src/kernels/3.10.0-693.2.2.el7.x86_64' CC [M] /root/os-newbie/module_makefile/newbie.o CC [M] /root/os-newbie/module_makefile/libbie.o LD [M] /root/os-newbie/module_makefile/newbs.o Building modules, stage 2. MODPOST 1 modules CC /root/os-newbie/module_makefile/newbs.mod.o LD [M] /root/os-newbie/module_makefile/newbs.ko make[1]: Leaving directory `/usr/src/kernels/3.10.0-693.2.2.el7.x86_64'
执行过后,即可在当前目录下看到一个newbs.ko的文件。
$ ls libbie.c libbie.o modules.order newbie.c newbs.ko newbs.mod.o libbie.h Makefile Module.symvers newbie.o newbs.mod.c newbs.o
再接着就是是insmod(插入内核模块)、rmmod(卸载内核模块)和dmesg(查看内核日志)等步骤,整个过程轻车熟路。但是对于Makefile文件中很多使用细节以及整个内核模块的编译过程(make),即使多年的内核老鸟往往也不知所以然。
上文在执行make命令时,也会输出一些信息CC、LD、Entering directory、Leaving directory、Building modules和MODPOST 1 modules,这些信息是什么含义?Makefile文件中obj-m是什么?这个变量名newbs-objs为什么把模块名包含进去?内核经典入门教材《Linux设备驱动程序》(LDD3)的第29页中对此问题有过一些讲解。但是限于篇幅,作者并没有深入展开说明。
工欲善其事,必先利其器。恰好在几年前我研究procps时,比较深入的学习了一下makefile的相关知识。现在把当初积累的一些高效技巧拿出来分析了一下内核模块Makefile文件和make过程,获得了对这个编译过程很多意想不到的收获。
下面就先从我曾经收藏的三大技巧开始,给大家分享一下我的个人经验成果。
二、makefile调试技巧
2.1、实验小例子
为了能准确说明这三个makefile调试技巧,我们首先准备一个极简的Makefile实例。实例从git上获取。
$ git clone https://gitee.com/os-newbie/makefile_debug.git $ cd makefile_debug/ $ ls cfg_makefile Makefile sub_makefile $ cat Makefile aa:=11 bb:=$(aa) cc:=$(bb) bb:=9999 export bb include cfg_makefile default: prqst @echo "Makefile default start" @echo "bb is $(bb)" @echo "cccc is $(cccc) in main makefile before" make -f sub_makefile @echo "cccc is $(cccc) in main makefile after" @echo "Makefile default end" prqst: @echo "zz is $(zz)" $ cat cfg_makefile xx:=4444 yy:=$(xx) zz:=$(yy) $ cat sub_makefile aaaa:=222222 bbbb:=$(aaaa) cccc:=$(bbbb) all: @echo "sub_makefile start" @echo "cccc is $(cccc)" @echo "bb is $(bb) in sub_makefile" @echo "sub_makefile end"
执行make命令,显示如下输出内容。
$ make zz is 4444 Makefile default start bb is 9999 cccc is in main makefile before make -f sub_makefile make[1]: Entering directory `/root/os-newbie/makefile_debug' sub_makefile start cccc is 222222 bb is 9999 in sub_makefile sub_makefile end make[1]: Leaving directory `/root/os-newbie/makefile_debug' cccc is in main makefile after Makefile default end
如果对makefile比较熟悉的同学估计很快就看明白了。不熟悉也没关系,下面简要介绍一下关键点。
- 首先,大家要明白make命令执行时,隐含的调用了Makefile文件,即make -f Makefile。
- 其次,在Makefile文件中,其中如下2行是最关键的。其中通过include包含进其他makefile文件,或者在Makefile文件中再次调用make命令执行其他makefile文件。正是通过这2行代码,将整个这个小例子中的所有3个makefile文件关联了起来。
...... include cfg_makefile ...... make -f sub_makefile ......
- 最后,我们还可以通过三个makefile中的@echo输出,大致了解到makefile的工作机制执行顺序,最先生效了被include的子文件cfg_makefile变量替换,然后生效了主文件的Makefile变量替换,最后生效的是经过make -f调用的sub_makefile子文件。
2.2、递归式make与非递归式make
一些大型的开源项目,往往有很多Makefile,分别控制不同目录层级的编译动作。以我们的linux内核为例,就有1800多个makefile文件。
$ find /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/ -name Makefile | wc -l 1806
这么多makefile文件必须通过一些方法将它们组织起来。《递归式make》和《非递归式make》就是makefile语法中两种组织makefile文件的途径方法。如下3个例子所示。
include cfg_makefile # 通过include包含的就是《非递归式make》 make -f sub_makefile # 再次调用make的就是《递归式make》 make -C newdir -f sub_makefile # 递归式make还可通过-C参数指定不在同一目录的makefile目标文件
事实上,linux kernel主要采用的就是递归式make的方式组织makefile文件,而安卓系统主要采用的就是非递归式make方式。这两种递归方式各有优缺点,没有明显的孰优孰劣。
2.3、makefile调试的技巧1
2.3.1、make命令选项说明
make命令有个--debug选项,很多人应该都了解过,选项值依次可以有a、j、i、v、b和m,具体用法大家可以参考make帮助手册。再结合make系统环境变量SHELL,给大家推荐如下几条组合命令。
$ make --debug=a,m SHELL="bash -x" > make.log 2>&1 # 可以获取make过程最完整debug信息 $ make --debug=v,m SHELL="bash -x" > make.log 2>&1 # 一个相对精简版,推荐使用这个命令 $ make --debug=v > make.log 2>&1 # 次精简一点的版本 $ make --debug=b > make.log 2>&1 # 最精简的版本
简单解释一下各参数的输出内容,a包含j、a包含i、a包含v、v包含b,m与其他无交集。SHELL="bash -x"是打开shell的xtrace选项,丰富shell命令执行时的信息输出。在上面推荐的使用组合中,我们选择参数为v的这个命令组合执行下make过程,使用grep是为了给输出内容增加行号信息。选择好调试选项后,再对makefile_debug小例子执行一次make过程。
$ cd ./../makefile_debug/ $ make --debug=v,m SHELL="bash -x" 2>&1 | grep -n . > make.log
输出显示结果如下。
$ cat make.log 1:GNU Make 3.82 2:Built for x86_64-redhat-linux-gnu 3:Copyright (C) 2010 Free Software Foundation, Inc. 4:License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> 5:This is free software: you are free to change and redistribute it. 6:There is NO WARRANTY, to the extent permitted by law. 7:Reading makefiles... 8:Reading makefile `Makefile'... 9:Reading makefile `cfg_makefile' (search path) (no ~ expansion)... 10:Updating makefiles.... 11: Considering target file `cfg_makefile'. 12: Finished prerequisites of target file `cfg_makefile'. 13: No need to remake target `cfg_makefile'. 14: Considering target file `Makefile'. 15: Finished prerequisites of target file `Makefile'. 16: No need to remake target `Makefile'. 17:Updating goal targets.... 18:Considering target file `default'. 19: File `default' does not exist. 20: Considering target file `prqst'. 21: File `prqst' does not exist. 22: Finished prerequisites of target file `prqst'. 23: Must remake target `prqst'. 24:Invoking recipe from Makefile:15 to update target `prqst'. 25:+ echo 'zz is 4444' 26:zz is 4444 27: Successfully remade target file `prqst'. 28: Finished prerequisites of target file `default'. 29:Must remake target `default'. 30:Invoking recipe from Makefile:8 to update target `default'. 31:+ echo 'Makefile default start' 32:Makefile default start 33:+ echo 'bb is 9999' 34:bb is 9999 35:+ echo 'cccc is in main makefile before' 36:cccc is in main makefile before 37:make -f sub_makefile 38:+ make -f sub_makefile 39:GNU Make 3.82 40:Built for x86_64-redhat-linux-gnu 41:Copyright (C) 2010 Free Software Foundation, Inc. 42:License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> 43:This is free software: you are free to change and redistribute it. 44:There is NO WARRANTY, to the extent permitted by law. 45:Reading makefiles... 46:Reading makefile `sub_makefile'... 47:Updating makefiles.... 48: Considering target file `sub_makefile'. 49: Finished prerequisites of target file `sub_makefile'. 50: No need to remake target `sub_makefile'. 51:Updating goal targets.... 52:Considering target file `all'. 53: File `all' does not exist. 54: Finished prerequisites of target file `all'. 55:Must remake target `all'. 56:Invoking recipe from sub_makefile:5 to update target `all'. 57:make[1]: Entering directory `/root/os-newbie/makefile_debug' 58:+ echo 'sub_makefile start' 59:sub_makefile start 60:+ echo 'cccc is 222222' 61:cccc is 222222 62:+ echo 'bb is 9999 in sub_makefile' 63:bb is 9999 in sub_makefile 64:+ echo 'sub_makefile end' 65:sub_makefile end 66:Successfully remade target file `all'. 67:make[1]: Leaving directory `/root/os-newbie/makefile_debug' 68:+ echo 'cccc is in main makefile after' 69:cccc is in main makefile after 70:+ echo 'Makefile default end' 71:Makefile default end 72:Successfully remade target file `default'.
2.3.2、makefile的递归式结构解析
看到这么一大段输出,初学者也不必惊慌失措。认真看完分析就会豁然开朗,下面对关键点进行说明。
首先找到输出内容中的“GNU Make 3.82”的关键字信息(不同os环境版本号略有不同,这里是3.82)。
1:GNU Make 3.82 ...... 39:GNU Make 3.82 ......
每见到这样一行(GNU Make 3.82)的信息,说明在此行之前紧邻部分一定有一行make命令的执行语句。当然第一行的"GNU Make 3.82"之前的make语句一定是你在shell命令行中执行的那个make命令。我们将其补充完整。
$ make --debug=v,m SHELL="bash -x" 2>&1 1:GNU Make 3.82 ...... 37:make -f sub_makefile 38:+ make -f sub_makefile 39:GNU Make 3.82 ......
不管多么复杂的使用makefile的项目,首先从输出的debug信息中,将以上相关信息摘要出来。这就是整个项目中makefile的一个完整的递归调用链。出现多少次“GNU Make 3.82”,一定就执行了多少次make命令调用。
“GNU Make 3.82”关键字标志着进入一个make的过程,出现"Leaving directory"关键字的一行标识着离开本次make的过程。因此,前后2次make之前的关系主要就有父子和兄弟两种情况了。
其中父子关系的两个makefile的输出调试信息如下。
父makefile开始:GNU Make 3.82 子makefile开始:GNU Make 3.82 子makefile结束:Successfully remade target file ...... 父makefile结束:Successfully remade target file ......
同级兄弟关系的两个makefile的输出调试信息如下。
兄makefile开始:GNU Make 3.82 兄makefile结束:Successfully remade target file ....... 弟makefile开始:GNU Make 3.82 弟makefile结束:Successfully remade target file ......
make[1]这个中括号中的数字还可以帮助我们判断父子、兄弟关系,同级别的数字相同,父子关系数值加1。
2.3.3、单makefile的make过程解析
以上文中makefile_debug的Makefile为例,在同一个makefile的make过程的调试信息中,最关键摘要信息为。
39:GNU Make 3.82 ...... 45:Reading makefiles... ...... 47:Updating makefiles.... ...... 51:Updating goal targets.... 52:Considering target file `all'. ...... 54: Finished prerequisites of target file `all'. 55:Must remake target `all'. 56:Invoking recipe from sub_makefile:5 to update target `all'. ...... 66:Successfully remade target file `all'. ......
- 第一段内容(39行到44行),是一段标准的GNU Make的信息,无特殊含义。
- 第二段内容(45行到46行),显示对makefile读取的相关信息。
46:Reading makefile `sub_makefile'...
- 第三段内容(47行到50行),显示对已经读取makefile内容的更新。
- 第四段内容(51行),标志着从此行开始对targets的依赖关系进行更新。
- 第五段内容(52行到54行),显示此时正在对targets的依赖关系的更新。
- 第六段内容(55行到66行),显示对当前默认target的依赖关系更新后执行的recipe部分内容。
第五段和第六段是最重要的两部分,整个内容的骨架是由层层缩进的"Considering target file"关键词支撑起来的,下一个小结专门介绍这部分内容。
2.3.4、make过程targets依赖关系更新解析
最后这部分内容比较多,在开始说明之前,大家有必要先搞懂如下几个出现在调试信息中的基本词汇含义。
- target(目标):makefile文件中冒号前面的部分;
- prerequisite(依赖条件):makefile文件中冒号后面依赖的列表,dependency list;
- recipe(命令):具体执行的命令,相当于通常理解的command;
- rule(规则):target、prerequisite和recipe的一个组合;
- implicit rule(隐式规则):make -p -f /dev/null 可以查看到所有的隐式规则;
- invoking recipe(调用命令):具体执行一个recipe;
- pattern rule(模式规则):%.o:%.c就是一种模式规则;
明白了上面这些名词的含义,再来看调试日志的输出,大体即可看懂含义。一般来说可以总结出如下规律。
- "Considering target file"层层递进展开;
- "Considering target file"之后就是"File *** does not exist"或者"Finished prerequisites of target file",并且他们一定会与前面某处的"Considering target file"有一对一对应关系;
- "Finished prerequisites of target file"之后,需要确定"No need to remake target"或者"Must remake target";
- "Must remake target"之后会引出"Invoking recipe from Makefile:** to update target";
- "Invoking recipe from Makefile:** to update target"之后是具体的recipe内容,包括gcc ld等;
- “此时make命令会启动一个shell进行去执行每一个具体的recipe命令”;
- 具体的recipe内容之后是“Successfully remade target file”,这是与前面的"Must remake target"有一对一对应关系的;
- “Successfully remade target file”之后回退到上一级的“Finished prerequisites of target file”或者继续同级的下一个“Considering target file”;
大家可以尝试下载一个开源软件,如procps-ng、vsftpd等,结合上面的介绍仔细体会,相信一定会很有收获。
2.4、makefile调试的技巧2
上面的技巧1更多的还是在整体工程的makefile结构、makefile读取和makefile内部的rule之间的关系方面有很好的帮助作用。但是对于makefile中rule部分之前的变量部分的引用过程则表现的不是很充分。在这里,我们有另外一个技巧,可以把变量部分的引用过程给出一个比较好的调试信息。具体命令如下。
$ cd ./../makefile_debug/ $ make -p 2>&1 | grep -A 1 '^# makefile' | grep -v '^--' \ |awk '/# makefile/&&/line/{getline n;print $0,";",n}'|LC_COLLATE=C sort -k 4 -k 6n > var.log $ cat var.log # makefile (from `Makefile', line 1) ; aa := 11 # makefile (from `Makefile', line 3) ; cc := 11 # makefile (from `Makefile', line 4) ; bb := 9999 # makefile (from `cfg_makefile', line 1) ; MAKEFILE_LIST := Makefile cfg_makefile # makefile (from `cfg_makefile', line 1) ; xx := 4444 # makefile (from `cfg_makefile', line 2) ; yy := 4444 # makefile (from `cfg_makefile', line 3) ; zz := 4444 # makefile (from `sub_makefile', line 1) ; MAKEFILE_LIST := sub_makefile # makefile (from `sub_makefile', line 1) ; aaaa := 222222 # makefile (from `sub_makefile', line 2) ; bbbb := 222222 # makefile (from `sub_makefile', line 3) ; cccc := 222222
技巧2部分的命令比较复杂,但是输出比较简单。命令不做过多篇幅说明了,大家自己慢慢研究。
输出部分主要说明的是哪个makefile(比如Makefile)的哪个变量(比如bb)在这个makefile中的最终值是多少(这里是9999),同时会说明最终值在文件中哪一行设置(这里是line 4)。整个输出结果非常清晰一目了然。
技巧2部分有一个特殊的变量就是MAKEFILE_LIST,这个变量好像只出现在第一行,其值可以结合上面“Reading makefiles...”部分来理解,其值的含义是说当前makefile被加载时,一共被加载的makefile列表。
2.5、makefile调试的技巧3
技巧2可以把makefile文件中每个变量的最终值清晰的展现出来,但是对于这些变量引用过程中的中间值却没有展示。此时,我们需要依赖技巧3来帮助我们。
$(warning $(var123))
很多人可能都知道这个warning语句。我们可以在makefile文件中的变量引用阶段的任何两行之间,添加这个语句打印关键变量的引用过程。
此外,如果需要对一些关键makefile中的代码实现有准确把握,还需要一份耗哥(左耳朵耗子)的《跟我一起写makefile》的文档。
三、内核模块makefile分析
前面内容就是几年前做其他工作时积累的三大makefile调试技巧,用了很多文字来阐述他们。下面我们就可以用这三大技巧来剖析一下我们内核模块的Makefile文件了。
3.1、提取内核模块make关键信息
首先使用技巧1获取整体make构建过程信息。
$ cd ./../module_makefile $ echo '0:+ make' > make.log $ make --debug=v,m SHELL="bash -x" 2>&1 | grep -n . >> make.log 逐步提取make.log文件中关键信息。$ cat make.log | grep -B 1 'GNU Make ' | tee -a summary.log 0:+ make 1:GNU Make 3.82 -- 22:+ make -C /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/ M=/root/os-newbie/ module_makefile modules 23:GNU Make 3.82 -- 226:+ make -f scripts/Makefile.build obj=/root/os-newbie/module_makefile 227:GNU Make 3.82 -- 359:+ make -f ./scripts/Makefile.modpost 360:GNU Make 3.82
过程一目了然,连同shell命令行用户自主执行的一次make,一共调用了共4次make构建。
进一步再摘出makefile文件读取部分的信息。
$ cp summary.log summary_outline.log $ cat make.log | grep 'Reading makefile' | tee -a summary.log 7:Reading makefiles... 8:Reading makefile `Makefile'... 29:Reading makefiles... 30:Reading makefile `Makefile'... 35:Reading makefile `scripts/Kbuild.include' (search path) (no ~ expansion)... 36:Reading makefile `include/config/auto.conf' (search path) (don't care) (no ~ expansion)... 37:Reading makefile `arch/x86/Makefile' (search path) (no ~ expansion)... 233:Reading makefiles... 234:Reading makefile `scripts/Makefile.build'... 235:Reading makefile `include/config/auto.conf' (search path) (don't care) (no ~ expansion)... 236:Reading makefile `scripts/Kbuild.include' (search path) (no ~ expansion)... 237:Reading makefile `/root/os-newbie/module_makefile/Makefile' (search path) (no ~ expansion)... 238:Reading makefile `scripts/Makefile.lib' (search path) (no ~ expansion)... 366:Reading makefiles... 367:Reading makefile `scripts/Makefile.modpost'... 368:Reading makefile `include/config/auto.conf' (search path) (no ~ expansion)... 369:Reading makefile `scripts/Kbuild.include' (search path) (no ~ expansion)... 370:Reading makefile `/root/os-newbie/module_makefile/Makefile' (search path) (no ~ expansion)... 371:Reading makefile `scripts/Makefile.lib' (search path) (no ~ expansion)...
将updating makefile部分的信息摘要出来。
$ cat make.log | grep 'Updating makefiles' | tee -a summary.log 11:Updating makefiles.... 177:Updating makefiles.... 239:Updating makefiles.... 375:Updating makefiles....
将update目标依赖,以及首个target部分的信息摘要出来。
$ cat make.log | grep -A 1 'Updating goal targets' | tee -a summary.log 15:Updating goal targets.... 16:Considering target file `default'. -- 197:Updating goal targets.... 198:Considering target file `modules'. -- 255:Updating goal targets.... 256:Considering target file `__build'. -- 391:Updating goal targets.... 392:Considering target file `_modpost'.
查询出首个target 依赖条件部分结束行的信息,remake判断的行的信息,调用recipe开始行的信息。
$ cat summary.log | grep 'Considering target file' | awk -F: '{print $2}' |\ sed -n 's/Considering target file/Must remake target/p' | sed -n "s/'.//p" | \ xargs -i grep -C 1 -P ":"$'{}'"'.$" make.log | sort | uniq | tee -a summary.log 18: Finished prerequisites of target file `default'. 19:Must remake target `default'. 20:Invoking recipe from Makefile:9 to update target `default'. 352: Finished prerequisites of target file `modules'. 353:Must remake target `modules'. 354:Invoking recipe from Makefile:1308 to update target `modules'. 346: Finished prerequisites of target file `__build'. 347:Must remake target `__build'. 348:Invoking recipe from scripts/Makefile.build:148 to update target `__build'. 448: Finished prerequisites of target file `_modpost'. 449:Must remake target `_modpost'. 450:Successfully remade target file `_modpost'.
查询出首个target remake结束行的信息。
$ cat summary.log | grep 'Considering target file' |awk -F: '{print $2}'| \ sed -n 's/Considering target file/Successfully remade target file/p' | sed -n "s/'.//p" |\ xargs -i grep -P ":"$'{}'"'.$" make.log | sort | uniq | tee -a summary.log 350:Successfully remade target file `__build'. 450:Successfully remade target file `_modpost'. 451:Successfully remade target file `modules'. 453:Successfully remade target file `default'.
将工作目录切换的信息摘要出来。
$ cat make.log | grep -P 'Entering directory|Leaving directory' | tee -a summary.log 186:make[1]: Entering directory `/usr/src/kernels/3.10.0-693.2.2.el7.x86_64' 452:make[1]: Leaving directory `/usr/src/kernels/3.10.0-693.2.2.el7.x86_64'
最后把这些信息按行数顺序排序,其中第8行和第30行的Makefile的路径不难补充完整,显示如下。
$ cat summary.log |grep -v '\-\-' | sort -t: -k 1n | uniq | tee -a summary_sort.log 0:+ make 1:GNU Make 3.82 7:Reading makefiles... 8:Reading makefile `/root/os-newbie/module_makefile/Makefile'... 11:Updating makefiles.... 15:Updating goal targets.... 16:Considering target file `default'. 18: Finished prerequisites of target file `default'. 19:Must remake target `default'. 20:Invoking recipe from Makefile:9 to update target `default'. 22:+ make -C /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/ M=/root/os-newbie/module_makefile modules 23:GNU Make 3.82 29:Reading makefiles... 30:Reading makefile `/usr/src/kernels/3.10.0-693.2.2.el7.x86_64/Makefile'... 35:Reading makefile `scripts/Kbuild.include' (search path) (no ~ expansion)... 36:Reading makefile `include/config/auto.conf' (search path) (don't care) (no ~ expansion)... 37:Reading makefile `arch/x86/Makefile' (search path) (no ~ expansion)... 177:Updating makefiles.... 186:make[1]: Entering directory `/usr/src/kernels/3.10.0-693.2.2.el7.x86_64' 197:Updating goal targets.... 198:Considering target file `modules'. 226:+ make -f scripts/Makefile.build obj=/root/os-newbie/module_makefile 227:GNU Make 3.82 233:Reading makefiles... 234:Reading makefile `scripts/Makefile.build'... 235:Reading makefile `include/config/auto.conf' (search path) (don't care) (no ~ expansion)... 236:Reading makefile `scripts/Kbuild.include' (search path) (no ~ expansion)... 237:Reading makefile `/root/os-newbie/module_makefile/Makefile' (search path) (no ~ expansion)... 238:Reading makefile `scripts/Makefile.lib' (search path) (no ~ expansion)... 239:Updating makefiles.... 255:Updating goal targets.... 256:Considering target file `__build'. 346: Finished prerequisites of target file `__build'. 347:Must remake target `__build'. 348:Invoking recipe from scripts/Makefile.build:148 to update target `__build'. 350:Successfully remade target file `__build'. 352: Finished prerequisites of target file `modules'. 353:Must remake target `modules'. 354:Invoking recipe from Makefile:1308 to update target `modules'. 359:+ make -f ./scripts/Makefile.modpost 360:GNU Make 3.82 366:Reading makefiles... 367:Reading makefile `scripts/Makefile.modpost'... 368:Reading makefile `include/config/auto.conf' (search path) (no ~ expansion)... 369:Reading makefile `scripts/Kbuild.include' (search path) (no ~ expansion)... 370:Reading makefile `/root/os-newbie/module_makefile/Makefile' (search path) (no ~ expansion)... 371:Reading makefile `scripts/Makefile.lib' (search path) (no ~ expansion)... 375:Updating makefiles.... 391:Updating goal targets.... 392:Considering target file `_modpost'. 448: Finished prerequisites of target file `_modpost'. 449:Must remake target `_modpost'. 450:Successfully remade target file `_modpost'. 451:Successfully remade target file `modules'. 452:make[1]: Leaving directory `/usr/src/kernels/3.10.0-693.2.2.el7.x86_64' 453:Successfully remade target file `default'.
以上内容就是这个module_makefile内核模块的编译过程的重要摘要信息,大体的过程一目了然。
3.2、内核模块make过程初步分析
如果觉得summary_sort.log的文件内容还是过于复杂,可以先基于前面保存的summary_outline.log文件开始理解这个make过程。下面对这个make过程进行一个分析说明。
3.2.1、内核模块make过程一般步骤分析
- 内核模块工作目录中运行make后,先执行/root/os-newbie/module_makefile/Makefile的这个Makefile。
- 然后通过其中的make命令的-C /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/参数,切换工作目录到/usr/src/kernels/3.10.0-693.2.2.el7.x86_64/,这是3.10.0-693.2.2.el7版本内核源码的根目录。M=/root/os-newbie/module_makefile参数会将当前内核模块所在目录的值传递给内核源码目录工作的make命令。
- 在内核源码目录继续执行make操作,makefile为内核源码根目录的Makefile文件,即/usr/src/kernels/3.10.0-693.2.2.el7.x86_64/Makefile文件。此时默认target为Makefile中的modules这个target。
- 在target modules下,分别会先后执行make -f scripts/Makefile.build和make -f ./scripts/Makefile.modpost两个 recipe。
- 其中Makefile.build的默认target为__build,Makefile.modpost的默认target为_modpost。
3.2.2、内核源码根目录makefile分析
内核源码根目录(/usr/src/kernels/3.10.0-693.2.2.el7.x86_64/Makefile)构建时,默认target为Makefile中的modules这个target。这个文件所处的位置可以通过354行内容获取。
352: Finished prerequisites of target file `modules'. 353:Must remake target `modules'. 354:Invoking recipe from Makefile:1308 to update target `modules'. 359:+ make -f ./scripts/Makefile.modpost
其中这里的1308为modules target所处位置下一行的第一个recipe的行数,那么modules就处在1307行位置。查看Makefile源码。
#cat -n /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/Makefile | head -n 1310 | tail -n 10 1301 1302 module-dirs := $(addprefix _module_,$(KBUILD_EXTMOD)) 1303 PHONY += $(module-dirs) modules 1304 $(module-dirs): crmodverdir $(objtree)/Module.symvers 1305 $(Q)$(MAKE) $(build)=$(patsubst _module_%,%,$@) 1306 1307 modules: $(module-dirs) 1308 @$(kecho) ' Building modules, stage 2.'; 1309 $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
这里接着看1309行的内容,结合上面的make.log部分的359行内容make -f ./scripts/Makefile.modpost,我们很容易获知,对./scripts/Makefile.modpost这个makefile文件的递归调用是在内核源码根Makefile的1309行代码实现的。这1309行其实也是作为modules target下面recipe的一部分。
我们再来看内核源码根Makefile中的第一个递归调用make -f scripts/Makefile.build obj=/root/os-newbie/module_makefile。看如下三行关键log。
198:Considering target file `modules'. ...... 226:+ make -f scripts/Makefile.build obj=/root/os-newbie/module_makefile ...... 352: Finished prerequisites of target file `modules'.
很明显226行的make调用是在modules target的层层依赖的展开中被触发的,因此我们需要再深入研究198行到226行之间的部分的log。
198:Considering target file `modules'. 199: File `modules' does not exist. 200: Considering target file `_module_/root/os-newbie/module_makefile'. 201: File `_module_/root/os-newbie/module_makefile' does not exist. 202: Considering target file `crmodverdir'. ...... 212: Considering target file `Module.symvers'. ...... 221: Finished prerequisites of target file `_module_/root/os-newbie/module_makefile'. 222: Must remake target `_module_/root/os-newbie/module_makefile'. 223:Invoking recipe from Makefile:1305 to update target `_module_/root/os-newbie/module_makefile'. 224:+ cat include/config/kernel.release 225:+ cat include/config/kernel.release 226:+ make -f scripts/Makefile.build obj=/root/os-newbie/module_makefile
从223行log的内容可以看出来,Makefile源码的1305行,就是make -f scripts/Makefile.build obj=/root/os-newbie/module_makefile这个递归调用的代码实现。1304行的target的值就是_module_/root/os-newbie/module_makefile。
3.2.3、内核源码子流程makefile分析
上一小节已经深入分析了内核源码根目录的Makefile,本小节就简单说明下两次递归子调用的构建过程。
首先来看scripts/Makefile.build的过程。根据上文介绍,我们需要的log内容在256行到350行之间。即256行开始Considering target file `__build',然后350行完成Successfully remade target file `__build'。我们将关键代码摘要出来。
256:Considering target file `__build'. 258: Considering target file `/root/os-newbie/module_makefile/newbs.o'. 260: Considering target file `/root/os-newbie/module_makefile/newbie.o'. 280: Must remake target `/root/os-newbie/module_makefile/newbie.o'. 281:Invoking recipe from scripts/Makefile.build:341 to update target `/root/os-newbie/module_makefile/newbie.o'. ...... # gcc编译出newbie.o 296: Successfully remade target file `/root/os-newbie/module_makefile/newbie.o'. 297: Considering target file `/root/os-newbie/module_makefile/libbie.o'. 307: Must remake target `/root/os-newbie/module_makefile/libbie.o'. 308:Invoking recipe from scripts/Makefile.build:341 to update target `/root/os-newbie/module_makefile/libbie.o'. ...... # gcc编译出libbie.o 323: Successfully remade target file `/root/os-newbie/module_makefile/libbie.o'. 325: Finished prerequisites of target file `/root/os-newbie/module_makefile/newbs.o'. 326: Must remake target `/root/os-newbie/module_makefile/newbs.o'. 327:Invoking recipe from scripts/Makefile.build:477 to update target `/root/os-newbie/module_makefile/newbs.o'. ...... # ld从newbie.o和libbie.o链接出newbs.o 336: Successfully remade target file `/root/os-newbie/module_makefile/newbs.o'. 346: Finished prerequisites of target file `__build'. 347:Must remake target `__build'. 348:Invoking recipe from scripts/Makefile.build:148 to update target `__build'. 350:Successfully remade target file `__build'.
上面这一部分主要是完成了从源码newbie.c、libbie.c到newbs.o的过程。
其次再来看./scripts/Makefile.modpost的过程。这里我们需要的log内容在392行到450行之间。即392行开始Considering target file `_modpost',然后450行完成Successfully remade target file `_modpost'。我们继续将关键代码摘要出来。
392:Considering target file `_modpost'. 394: Considering target file `__modpost'. 415: Considering target file `/root/os-newbie/module_makefile/newbs.ko'. 439: Finished prerequisites of target file `/root/os-newbie/module_makefile/newbs.ko'. 440: Must remake target `/root/os-newbie/module_makefile/newbs.ko'. 441:Invoking recipe from scripts/Makefile.modpost:122 to update target `/root/os-newbie/module_makefile/newbs.ko'. ...... # 从newbs.o链接出newbs.ko 447: Successfully remade target file `/root/os-newbie/module_makefile/newbs.ko'. 448: Finished prerequisites of target file `_modpost'. 449:Must remake target `_modpost'. 450:Successfully remade target file `_modpost'.
这一部分主要是完成了从newbs.o到newbs.ko的链接转化。
3.2.4、内核模块Makefile初步分析
有了对内核模块make构建过程一定了解之后,我们再来看一下内核模块Makefile文件内容。整个文件以ifneq ($(KERNELRELEASE),)这一行条件判断最为关键。当在内核模块工作目录执行make时,由于KERNELRELEASE这个变量未定义,因此会执行else分支语句,即如下代码。
KERNELVER:=$(shell uname -r) KERNELDIR:=/usr/src/kernels/$(KERNELVER)/ PWD:=$(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
此时,内核模块代码中的Makefile文件中的这几行代码应该大家已经基本可以看懂了。
关于清理文件的部分代码$(MAKE) -C $(KERNELDIR) M=$(PWD) clean,按照上面的步骤也可以很容易搞懂。只是这里换成了内核源码根目录Makefile中的clean target。
3.2.5、构建过程Reading makefile的分析
关于"Reading makefile"所涉及的makefile文件部分,我们可以从make.log中的Reading makefiles部分发现,虽然内核有几千个makefile文件,但我们内核模块编译过程只用到如下7个,连同我们自己内核模块自定义Makefile总共才8个。
/root/os-newbie/module_makefile/Makefile /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/Makefile arch/x86/Makefile include/config/auto.conf scripts/Kbuild.include scripts/Makefile.build. scripts/Makefile.lib scripts/Makefile.modpost
编译过程使用到/root/os-newbie/module_makefile/Makefile文件的地方一共有三处。
$ cat summary_sort.log | grep -F 'module_makefile/Makefile' 8:Reading makefile `/root/os-newbie/module_makefile/Makefile'... 237:Reading makefile `/root/os-newbie/module_makefile/Makefile' (search path) (no ~ expansion)... 370:Reading makefile `/root/os-newbie/module_makefile/Makefile' (search path) (no ~ expansion)...
在经典著作lDD3的第30页,作者有这样一句描述“这个 makefile 在一次典型的构建中要被读 2次.”。从上下文内容来看,作者指的第一次读取应该指用户在shell命令行中执行make时调用了了一次。这样看来这个说法显然是不准确的,连同用户自己执行make调用的一次,一共应该被调用了三次才更准确。
3.3、内核模块make过程深入分析
此时,还剩内核模块Makefile条件分支中余下的两行我们还没有搞懂。要理解他们还需要进行一番深入分析。
obj-m+=newbs.o newbs-objs:=newbie.o libbie.o
3.3.1、关于变量KERNELRELEASE
在内核源码根目录的Makefile文件中有这样几行代码。其中419行有对KERNELRELEASE进行赋值,而422行通过export命令将KERNELRELEASE变量设置为全局变量。因此后面递归式调用的所有内核源码Makefile中变量KERNELRELEASE的值否是非空,即ifneq ($(KERNELRELEASE),)条件判断结果为真,代码走if分支,进入上面这两行代码。
$ cat -n /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/Makefile | grep -A 3 ^KERNELRELEASE 419:KERNELRELEASE = $(shell cat include/config/kernel.release 2> /dev/null) 420-KERNELVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL), .$(SUBLEVEL)))$(EXTRAVERSION) 421- 422-export VERSION PATCHLEVEL SUBLEVEL KERNELRELEASE KERNELVERSION
3.3.2、关于变量替换的分析
理解这两行,我们还需要继续结合技巧2和技巧3的使用,执行如下命令。
$ cd ./../module_makefile $ make -p 2>&1 | grep -A 1 '^# makefile' | grep -v '^--' | \ awk '/# makefile/&&/line/{getline n;print $0,";",n}' | \ LC_COLLATE=C sort -k 4 -k 6n | uniq > variable.log
variable.log文件的内容不少,大概有4千多行,说明我们这个内核模块的构建过程涉及到了4千多个变量。我们在这里只把libbie和newbs这2个关键字相关的行摘出来。这些行的含义表示这些变量在某行最终确定为某个值,比如在scripts/Makefile.lib文件的第52行,multi-used的值最终确定为newbs.o。
$ cat variable.log | grep -P 'libbie|newbs' # makefile (from `/root/os-newbie/module_makefile/Makefile', line 3) ; newbs-objs := newbie.o libbie.o # makefile (from `scripts/Makefile.lib', line 52) ; multi-used := newbs.o # makefile (from `scripts/Makefile.lib', line 59) ; multi-objs := newbie.o libbie.o # makefile (from `scripts/Makefile.lib', line 77) ; modorder := /root/os-newbie/module_makefile/newbs.ko # makefile (from `scripts/Makefile.lib', line 79) ; obj-m := /root/os-newbie/module_makefile/newbs.o # makefile (from `scripts/Makefile.lib', line 83) ; real-objs-m := /root/os-newbie/module_makefile/newbie.o /root/os-newbie/module_makefile/libbie.o # makefile (from `scripts/Makefile.lib', line 86) ; multi-used-m := /root/os-newbie/module_makefile/newbs.o # makefile (from `scripts/Makefile.lib', line 88) ; multi-objs-m := /root/os-newbie/module_makefile/newbie.o /root/os-newbie/module_makefile/libbie.o # makefile (from `scripts/Makefile.modpost', line 64) ; __modules := /root/os-newbie/module_makefile/newbs.ko # makefile (from `scripts/Makefile.modpost', line 65) ; modules := /root/os-newbie/module_makefile/newbs.ko
此时基本可以确认还没搞懂的2行内容和scripts/Makefile.lib这个文件高度相关。在Makefile.lib文件中使用了用户自定义的2个makefile变量obj-m和newbs-objs。而且重点在文件中的52到88行这部分,内容如下。
$ cat -n /usr/src/kernels/3.10.0-693.2.2.el7.x86_64/scripts/Makefile.lib | head -n 90 |\ tail -n +45 ...... 45 # Subdirectories we need to descend into 46 47 subdir-ym := $(sort $(subdir-y) $(subdir-m)) 48 49 # if $(foo-objs) exists, foo.o is a composite object 50 multi-used-y := $(sort $(foreach m,$(obj-y), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))), $(m)))) 51 multi-used-m := $(sort $(foreach m,$(obj-m), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))), $(m)))) 52 multi-used := $(multi-used-y) $(multi-used-m) 53 single-used-m := $(sort $(filter-out $(multi-used-m),$(obj-m))) 54 55 # Build list of the parts of our composite objects, our composite 56 # objects depend on those (obviously) 57 multi-objs-y := $(foreach m, $(multi-used-y), $($(m:.o=-objs)) $($(m:.o=-y))) 58 multi-objs-m := $(foreach m, $(multi-used-m), $($(m:.o=-objs)) $($(m:.o=-y))) 59 multi-objs := $(multi-objs-y) $(multi-objs-m) 60 61 # $(subdir-obj-y) is the list of objects in $(obj-y) which uses dir/ to 62 # tell kbuild to descend 63 subdir-obj-y := $(filter %/built-in.o, $(obj-y)) 64 65 # $(obj-dirs) is a list of directories that contain object files 66 obj-dirs := $(dir $(multi-objs) $(obj-y)) 67 68 # Replace multi-part objects by their individual parts, look at local dir only 69 real-objs-y := $(foreach m, $(filter-out $(subdir-obj-y), $(obj-y)), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))),$($(m:.o=-objs)) $($(m:.o=-y)),$(m))) $(extra-y) 70 real-objs-m := $(foreach m, $(obj-m), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))),$($(m:.o=-objs)) $($(m:.o=-y)),$(m))) 71 72 # Add subdir path 73 74 extra-y := $(addprefix $(obj)/,$(extra-y)) 75 always := $(addprefix $(obj)/,$(always)) 76 targets := $(addprefix $(obj)/,$(targets)) 77 modorder := $(addprefix $(obj)/,$(modorder)) 78 obj-y := $(addprefix $(obj)/,$(obj-y)) 79 obj-m := $(addprefix $(obj)/,$(obj-m)) 80 lib-y := $(addprefix $(obj)/,$(lib-y)) 81 subdir-obj-y := $(addprefix $(obj)/,$(subdir-obj-y)) 82 real-objs-y := $(addprefix $(obj)/,$(real-objs-y)) 83 real-objs-m := $(addprefix $(obj)/,$(real-objs-m)) 84 single-used-m := $(addprefix $(obj)/,$(single-used-m)) 85 multi-used-y := $(addprefix $(obj)/,$(multi-used-y)) 86 multi-used-m := $(addprefix $(obj)/,$(multi-used-m)) 87 multi-objs-y := $(addprefix $(obj)/,$(multi-objs-y)) 88 multi-objs-m := $(addprefix $(obj)/,$(multi-objs-m)) 89 subdir-ym := $(addprefix $(obj)/,$(subdir-ym)) 90 obj-dirs := $(addprefix $(obj)/,$(obj-dirs)) ......
对于一个makefile新手,这些语法多半是看不懂的。我一开始看到这部分代码,也完全看不懂。此时技巧3就派上用场了,最好再参考耗哥的《跟我一起写Makefile》一起看。
技巧3涉及修改内核源码树中makefile文件scripts/Makefile.lib,再开始修改之前最好能备份整个目录"/usr/src/kernels/3.10.0-693.2.2.el7.x86_64/"。
比如你对58行看不太懂,那么你可以这样做。
$(warning $(multi-used-m)) multi-objs-m := $(foreach m, $(multi-used-m), $($(m:.o=-objs)) $($(m:.o=-y))) $(warning $(multi-objs-m))
再次在模块目录执行make命令,即可看到如下内容。$(multi-used-m)的值就是newbs.o,$(multi-objs-m)的值就是newbie.o libbie.o。
scripts/Makefile.lib:58: newbs.o scripts/Makefile.lib:60: newbie.o libbie.o
实际上,正是这一行引用了用户内核模块中的newbs-objs变量。这里最关键的是这段$($(m:.o=-objs)),等价于result:=$(m:.o=-objs)、$(result)。$(m:.o=-objs)叫变量的替换引用,会把值是newbs.o的变量m的.o后缀替换为-objs后缀,得到新值newbs-objs。而后$(result)就是再对$(newbs-objs)求变量引用,正好用到我们的变量定义。
还有一行代码比较重要,scripts/Makefile.lib中的第53行,如下。大家可以自行分析这行代码。
single-used-m :=$(sort $(filter-out $(multi-used-m),$(obj-m)))
以上内核模块makefile分析部分就是使用这3个技巧,对整个内核模块编译过程进行的一个大体分析。大家可以继续在make.log中深挖,找出最关键的newbs.ko文件是怎么一步步编译出来的。