零基础学Linux内核系列文章目录
前置知识篇
1. 进程
2. 线程
进程间通信篇
1. IPC概述
2. 信号
3. 消息传递
4. 同步
5. 共享内存区
编译相关篇
1. GCC编译
2. 静态链接与动态链接
3. makefile入门基础
文章目录
一、前言
本节主要介绍一下makefile的一些基本语法,当然,了解完这些后并不能对实际工程里的make工程有所帮助,只能算是一个前置知识。
二、前置条件
UB18 + 一点点的基础知识
三、本文参考资料
《 [野火]i.MX Linux开发实战指南》
百度
四、正文部分
4.1 背景及定义
-
问题场景
编译多个文件依赖过多,过于复杂gcc hello.c aaa.c bbb.c -o hello
-
make工具:
它可以帮助我们找出项目里面修改变更过的文件,并根据依赖关系,找出受修改影响的其他相关文件,
然后对这些文件按照规则进行单独的编译,这样一来,就能避免重新编译项目的所有的文件。 -
Makefile文件:
上面提到的规则、依赖关系主要是定义在这个Makefile文件中的,我们在其中合理地定义好文件的依赖关系之后,make工具就能精准地进行编译工作。它们的关系如下图所示:
抓住它这种面向依赖的思想, 心里一定要谨记,Makefile中所有的复杂、晦涩的语法都是更好地为解决依赖问题而存在的。
当工程复杂度再上一个台阶的时候,会觉得手写Makefile也很麻烦, 那个时候可以用CMake、autotools等工具来帮忙生成Makefile。
实际上Windows系统下很多IDE工具内部也是使用类似Makefile的方式组织工程文件的, 只不过被封装成图形界面,对用户不可见而已。
4.2 makefile概览
1、 基础语法– 描述目标和依赖的特定格式,Makefile的核心。
2、 变量– 记录特定的信息,避免重复输入原始信息。尤其是手动输入原始信息很长时,特别好用。
3、 分支判断– 灵活控制多个不同的编译过程,方便兼容不同属性。
4、 头文件依赖– 监控头文件的变化,头文件也是程序的关键内容。
5、 隐含规则– 利用Makefile的一些默认规则,可以减少编写Makefile的工作量。
6、 自动化变量– 利用Makefile的默认的自动化变量,可以减少编写Makefile的工作量。
7、 模式规则– 灵活使用正则表达式,可以减少编写Makefile的工作量。
8、 函数– 使用Makefile的各种函数,可以更方便地实现Makefile的功能。
从上面的分析可以知道,Makefile的核心在于基础语法,用来描述目标和依赖的关系。
其他语法的目的,是为了减少我们编写Makefile工作量,让我们能够以更加优雅、更加简洁、更好维护的方式来实现Makefile的功能。
这跟我们程序开发是很相似的,不止要实现功能,还要兼顾程序的可读性、拓展性、可维护性等等。
4.3 make命令与makefile文件
4.3.1 makefile基本格式
注意在“ls -lh”、”touch test.txt”等命令前要使用Tab键,不能使用空格代替。
4.3.2 make命令
在终端上执行make命令时,make会在当前目录下搜索名为“Makefile”或“makefile”的文件,然后 根据该文件的规则解析执行。
如果要指定其它文件作为输入规则,可以通过“-f”参数指定输入文件(绝对路径/默认同路径),如“make -f /tmp/xxx/文件名”。
此处make命令读取我们的Makefile文件后,发现targeta是Makefile的第一个目标,它会被当成默认目标执行。
又由于targeta依赖于targetc和targetb目标,所以在执行targeta自身的命令之前,会先去完成targetc和targetb。
targetc的命令为pwd,显示了当前的路径。targetb的命令为touch test.txt ,创建了test.txt文件。
最后执行targeta自身的命令ls -lh ,列出当前目录的内容,可看到多了一个test.txt文件。
make targetd 、make targetb、make targetc命令:
由于targetd不是默认目标,且不被其它任何目标依赖,所以直接make的时 候targetd并没有被执行,
想要单独执行Makefile中的某个目标,可以使用”make 目标名“的语法,
例如上图中分别执行了”make targetd“ 、”make targetb“ 和”make targetc“指令,在执行”make targetd”目标时,可看到它的命令rm -f test.txt被执行,test.txt文件被删除。
从这个过程,可了解到make程序会根据Makefile中描述的目标与依赖关系,执行达成目标需要的shell命令。
简单来说,Makefile就是用来指导make程序如何干某些事情的清单。
4.4 makefile编译
4.4.1 使用GCC编译多个文件
如果我们直接使用GCC进行编译,需要使用如下命令:
相对于基础的hello.c编译命令,此处主要是增加了输入的文件数量,如“hello_main.c”、“hello_func.c”,
另外新增的“-I .”是告诉编译器头文件路径,让它在编译时可以在“.”(当前目录)寻找头文件,
其实不加”-I .”选项也是能正常编译通过的,此处只是为了后面演示Makefile的相关变量。
4.4.2 使用Makefile编译
该文件定义了默认目标hello_main用于编译程序,clean目标用于删除 编译生成的文件。
特别地,其中hello_main目标名与gcc编译生成的文件名“gcc -o hello_main”设置成一致了,也就是说,此处的目标hello_main在Makefile看来,已经是一个目标文件hello_main。
这样的好处是make每次执行的时候,会检查hello_main文件和依赖文件hello_main.c、hello_func.c的修改日期,
如果依赖文件的修改日期比hello_main文件的 日期新,那么make会执行目标其下的Shell命令更新hello_main文件,否则不会执行。
–> 使用touch命令更新一下hello_func.c(目标文件和依赖文件)的时间,可用于验证该结论是否成立
4.5 makefile语法
4.5.1 目标与依赖
[目标1]:[依赖]
[命令1]
[命令2]
[目标2]:[依赖]
[命令1]
[命令2]
-
目标:
指make要做的事情,可以是一个简单的代号,也可以是目标文件,需要顶格 书写,前面不能有空格或Tab。
一个Makefile可以有多个目标,写在最前面的第一 个目标,会被Make程序确立为 “默认目标”,例如前面的targeta、hello_main。 -
依赖:
要达成目标需要依赖的某些文件或其它目标。
例如前面的targeta依赖 于targetb和targetc,又如在编译的例子中,hello_main依赖于hello_main.c、hello_func.c源文 件,若这些文件更新了会重新进行编译。 -
命令1,命令2…命令n:
make达成目标所需要的命令。
只有当目标不存在或依赖文件的修改时间比目标文件还要新时,才会执行命令。
要特别注意命令的开头要用“Tab”键,不能 使用空格代替,有的编辑器会把Tab键自动转换成空格导致出错,若出现这种情况请检查自己的编辑器配置。
4.5.2 伪目标
前面我们在Makefile中编写的目标,在make看来其实都是目标文件,
例如make在执行的时候由于在目录找不到targeta文件,所以每次make targeta的时候,它都会去执行targeta的命令,期待执行后能得到名为targeta的同名文件。
如果目录下真的有targeta、targetb、targetc的文件,即假如目标文件和依赖文件都存在且是最新的,那么make targeta就不会被正常执行了,这会引起误会。
–> 就是怕make的目标文件恰好存在在目录里,导致make不能执行,故引入伪目标,防止make无效。
为了避免这种情况,Makefile使用“.PHONY”前缀来区分目标代号和目标文件,并且这种目标代号被称为“伪目标”,phony单词翻译过来本身就是假的意思。
也就是说,只要我们不期待生成目标文件,就应该把它定义成伪目标。
4.5.3 默认规则
整个编译过程包含如下图中的步骤,make在执行时也是 使用同样的流程,不过在Makefile的实际应用中,通常会把编译和最终的链接过程分开。
也就是说,我们的hello_main目标文件本质上并不是依赖hello_main.c和hello_func.c文件,而是依赖于hello_main.o和hello_func.o,
把这两个文件链接起来就能得到我们最终想要的hello_main目标文件。
另外,由于make有一条默认规则,当找不到xxx. o文件时,会查找目录下的同名xxx.c文件进行编译。根据这样的规则,我们可把Makefile改修改如下。
从make的输出可看到,它先执行了两条额外的“cc”编译命令,这是由make默认规则执行的,它们把C代码编译生成了同名的.o文件,
然后make根据Makefile的命令链接这两个文件得到最终目标文件hello_main。
4.5.4 使用变量
使用C自动编译成 *.o 的默认规则有个缺陷,由于没有显式地表示 *.o 依赖于.h头文件,假如我们修改了头文件的内容,那么 *.o并不会更新,这是不可接受的。
并且默认规则使用固定的“cc”进行编译,假如我们想使用ARM-GCC进行交叉编译,那么系统默认的“cc”会导致编译错误。
要解决这些问题并且让Makefile变得更加通用,需要引入变量和分支进行处理。
-
基本语法
在Makefile中的变量,有点像C语言的宏定义,在引用变量的地方使用变量值进行替换。
变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:“=” :延时赋值,该变量只有在调用的时候,才会被赋值 “:=” :直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。 “?=” :若变量的值为空,则进行赋值,通常用于设置默认值。 “+=” :追加赋值,可以往变量后面增加新的内容。
当我们想使用变量时,其语法如下:
$(变量名)
下面通过一个实验来讲解这四种定义方式,对于后两种赋值方式比较简单,主要思考延时赋值和直接赋值的差异,实验代码如下所示。
这里主要关心VAR_B和VAR_C的赋值方式,实验结果如下图所示。
执行完make命令 后,只有VAR_C是FILEA。这是因为VAR_B采用的延时赋值,只有当调用时,才会进行 赋值。
当调用VAR_B时,VAR_A的值已经被修改为FILEA FILEB,因此VAR_B的变量值也就等于FILEA FILEB。
(其中 -f 表示指定makefile路径)
-
改造默认规则
接下来使用变量对前面hello_main的Makefile进行大改造,如下所示。
”%”是一个通配符,功能类似”*”,如”%.o”表示所有以”.o”结尾的文件。
所以”%.o:%.c”在本例子中等价于”hello_main.o: hello_main.c”、”hello_func.o: hello_func.c”,即等价于o文件依赖于c文件的默认规则。
不过这行代码后面的”$(DEPS)”表示它除了依赖c文件,还依赖于变量”$(DEPS)”表示的头文件,所以当头文件修改的话,o文件也会被重新编译。这行代码出现了特殊的变量”$@”,”$<”,可理解为Makefile文件保留的关键字,是系统保留的自动化变量,
”$@”代表了目标文件,
”$<”代表了第一个依赖文件。
即”$@”表示”%.o”,”$<”表示”%.c”也就是说makefile可以利用变量及自动化变量,来重写.o文件的默认生成规则,以及增加头文件的依赖。
-
改造链接规则
与*.o文件的默认规则类似,我们也可以使用变量来修改生成最终目标 文件的链接规则,具体参考如下代码。
使用自动化变量“ @ ”表示目标文件“ @”表示目标文件“ @”表示目标文件“(TARGET)”,使用自动化变量“ ” 表示所有的依赖文件即“ ^”表示所有的依赖文件即“ ”表示所有的依赖文件即“(OBJS)”。 -
其它自动化变量
Makefile中还有其它自动化变量,此处仅列出方便以后使用到的时候进行查阅,见下表。
4.5.5 使用分支
为方便直接切换GCC编译器,我们还可以使用条件分支增加切换编译器 的功能。在Makefile中的条件分支语法如下:
4.5.6 使用函数
在更复杂的工程中,头文件、源文件可能会放在二级目录,编译生成的*.o或可执行文件也放到专门的编译输出目录方便整理,如下图所示。
示例中*.h头文件 放在includes目录下,*.c文件放在sources目录下,不同平台的编译输出分别存 放在build_x86和build_arm中。
实现这些复杂的操作通常需要使用Makefile的函数。
函数格式及示例
在Makefile中调用函数的方法跟变量的使用类似,以“$()”或“${}”符号包含函数名和参数,具体语法如下:
$(函数名 参数)
下面以常用的notdir、patsubst、wildcard函数为例 进行讲解,并且示例中都是我们后面Makefile中使用到的内容。
-
notdir函数
notdir函数用于去除文件路径中的目录部分。它的格式如下:$(notdir 文件名)
例如输入参数“./sources/hello_func.c”,函数执行后 的输出为“hell_func.c”,也就是说它会把输入中的“./sources/”路径部分去掉,保留文件名。
使用范例如下:
#上面的函数执行后会把路径中的“./sources/”部分去掉,输出为: hello_func.c -
wildcard函数
wildcard函数用于获取文件列表,并使用空格分隔开。它的格式如下:$(wildcard 匹配规则)
例如函数调用“$(wildcard *.c)”,函数执行后会把当前目录的所有c文件列出。
假设我们在上图中的Makefile目录下执行该函数,使用范例如下:
-
patsubst函数
patsubst函数功能为模式字符串替换。它的格式如下:$(patsubst 匹配规则, 替换规则, 输入的字符串)
当输入的字符串符合匹配规则,那么使用替换规则来替换字符串,
当匹配规则中有“%”号时,替换规则也可以例程“%”号来提取“%”匹配的内容加入到最后替换的字符串中。
有点抽象,请直接阅读以下示例:#执行如下函数
第一个函数调用中,由于“hello_main.c”符合“%.c”的匹配规则(%在Makefile中的类似于*通配符),
而且“%”从“hello_main.c”中提取出了“hello_main”字符,
把这部分内容放到替换规则“build_dir/%.o”的“%”号中,所以最终的输出为”build_di r/hello_main.o”。第二个函数调用中,由于由于“hello_main.xxx”不符合“%.c”的匹配规则,“.xxx”与“.c”对不上,所以不会进行替换,函数直接返回空的内容。
4.5.7 多级结构工程的Makefile
接下来我们使用上面三个函数修改我们的Makefile,以适应包含多级目录的工程,修改后的内容如下所示。