内核模块编译过程探秘

简介: 内核开发者刚刚入门时,都会学习写一个Hello World内核模块。这个内核模块中一定会包含一个Makefile文件。对于这个Makefile文件的内容和格式,几乎每个内核开发者都应该已经熟稔于心。

一、既熟悉又陌生的内核模块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就是一种模式规则;

明白了上面这些名词的含义,再来看调试日志的输出,大体即可看懂含义。一般来说可以总结出如下规律。


  1. "Considering target file"层层递进展开;
  2. "Considering target file"之后就是"File *** does not exist"或者"Finished prerequisites of target file",并且他们一定会与前面某处的"Considering target file"有一对一对应关系;
  3. "Finished prerequisites of target file"之后,需要确定"No need to remake target"或者"Must remake target";
  4. "Must remake target"之后会引出"Invoking recipe from Makefile:** to update target";
  5. "Invoking recipe from Makefile:** to update target"之后是具体的recipe内容,包括gcc ld等;
  6. “此时make命令会启动一个shell进行去执行每一个具体的recipe命令”;
  7. 具体的recipe内容之后是“Successfully remade target file”,这是与前面的"Must remake target"有一对一对应关系的;
  8. “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文件是怎么一步步编译出来的。

相关文章
|
6月前
|
Linux 编译器 开发工具
Android内核的编译过程
Android内核的编译过程
81 0
|
2月前
编译cuttlefish内核遇到的问题
编译cuttlefish内核遇到的问题
|
5月前
|
Linux 编译器 C语言
编译Linux内核:基础、重要性和交叉编译方法
Linux内核作为操作系统的心脏,负责管理计算机的硬件资源,同时也是运行所有其他程序的基础。理解如何编译Linux内核对于系统管理员、开发者乃至高级用户来说都是一项极其宝贵的技能。本文将介绍编译Linux内核的基本知识、编译的重要性、具体步骤以及交叉编译的概念。
429 0
|
6月前
|
编译器 Linux C语言
Linux嵌入式系统之交叉编译中编译器与解释器的区别
Linux嵌入式系统之交叉编译中编译器与解释器的区别
48 0
|
Dart Java 编译器
Android编译器及编译工具之编译器
习惯了IDE以及各种现成的编译工具为我们提供便捷的编译方式,我们很少会操心编译工具的编译过程和原理,但是工具越高级,隐藏的细节就越多,这样编译遇到问题时我们难以定位,遇到复杂的项目(尤其跨平台项目难以用ide)时不知如何下手。所以准备写两篇关于编译器和编译工具的文章。本文先来介绍编译工具。
206 0
一个Linux驱动工程师必知的内核编译机制
一个Linux驱动工程师必知的内核编译机制
|
算法 Linux C语言
一文搞懂内核模块依赖
一文搞懂内核模块依赖
|
算法 Linux C语言
一个Linux驱动工程师必知的内核模块知识
一个Linux驱动工程师必知的内核模块知识
将模块编译入内核
将模块编译入内核
112 0
|
Linux C语言
【Linux操作系统】程序的编译和动静态链接
【Linux操作系统】程序的编译和动静态链接
174 0
【Linux操作系统】程序的编译和动静态链接