零基础学Linux内核之编译相关篇(3)_makefile入门基础

本文详细介绍了Linux内核开发中进程、线程、IPC、信号、消息传递等概念,以及Makefile的基础语法、变量、分支和函数应用。通过实例学习,掌握如何编写高效Makefile进行编译管理和依赖跟踪。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

零基础学Linux内核系列文章目录

前置知识篇
1. 进程
2. 线程
进程间通信篇
1. IPC概述
2. 信号
3. 消息传递
4. 同步
5. 共享内存区
编译相关篇
1. GCC编译
2. 静态链接与动态链接
3. makefile入门基础


一、前言

本节主要介绍一下makefile的一些基本语法,当然,了解完这些后并不能对实际工程里的make工程有所帮助,只能算是一个前置知识。


二、前置条件

UB18 + 一点点的基础知识


三、本文参考资料

《 [野火]i.MX Linux开发实战指南》
百度


四、正文部分

4.1 背景及定义

  1. 问题场景
    编译多个文件依赖过多,过于复杂

    gcc hello.c aaa.c bbb.c -o hello
    
  2. make工具:
    它可以帮助我们找出项目里面修改变更过的文件,并根据依赖关系,找出受修改影响的其他相关文件,
    然后对这些文件按照规则进行单独的编译,这样一来,就能避免重新编译项目的所有的文件。

  3. 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变得更加通用,需要引入变量和分支进行处理。

  1. 基本语法
    在Makefile中的变量,有点像C语言的宏定义,在引用变量的地方使用变量值进行替换。
    变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:

    	“=” :延时赋值,该变量只有在调用的时候,才会被赋值
    	
    	“:=” :直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。
    	
    	“?=” :若变量的值为空,则进行赋值,通常用于设置默认值。
    	
    	“+=” :追加赋值,可以往变量后面增加新的内容。
    

    当我们想使用变量时,其语法如下:

    	$(变量名)
    

    下面通过一个实验来讲解这四种定义方式,对于后两种赋值方式比较简单,主要思考延时赋值和直接赋值的差异,实验代码如下所示。
    在这里插入图片描述
    这里主要关心VAR_B和VAR_C的赋值方式,实验结果如下图所示。
    执行完make命令 后,只有VAR_C是FILEA。这是因为VAR_B采用的延时赋值,只有当调用时,才会进行 赋值。
    当调用VAR_B时,VAR_A的值已经被修改为FILEA FILEB,因此VAR_B的变量值也就等于FILEA FILEB。
    (其中 -f 表示指定makefile路径)
    在这里插入图片描述

  2. 改造默认规则
    接下来使用变量对前面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文件的默认生成规则,以及增加头文件的依赖。

  3. 改造链接规则
    与*.o文件的默认规则类似,我们也可以使用变量来修改生成最终目标 文件的链接规则,具体参考如下代码。
    在这里插入图片描述
    使用自动化变量“ @ ”表示目标文件“ @”表示目标文件“ @”表示目标文件(TARGET)”,使用自动化变量“ ” 表示所有的依赖文件即“ ^”表示所有的依赖文件即“ 表示所有的依赖文件即(OBJS)”。

  4. 其它自动化变量
    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,以适应包含多级目录的工程,修改后的内容如下所示。
在这里插入图片描述
 


五、总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值