深入解析Linux编程:使用make命令与Makefile文件编译大型程序
网课:(课程查看)

论坛:(学术问答)
开发板:(淘宝)
(天猫)
交流群一:QQ群:869222007(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
交流群二:QQ群:536785813(单片机嵌入式)
公众号:百文科技
版本日期 作者备注
V1
2020年
魏东山
技术文档
Linux中使用make命令来编译程序,特别是大型程序; make 命令执行的操作取决于 Makefile 文件。最简单的Makefile如下:
hello: hello.c
gcc -o hello hello.c
clean:
rm -f hello
将以上4行保存为Makefile文件(注意第2行和第4行必须用Tab键缩进,不能用空格键),放到01_hello目录下,然后直接执行make命令编译程序,执行“make clean”清除编译结果。
make命令根据文件更新时间戳来确定哪些文件需要重新编译。这可以让你避免编译已经编译过并且没有改变的程序,并且可以大大提高编译效率。
要完全理解Makefile的规则,请参阅《GNU Make用户手册》。以下仅作粗略介绍。
3.1 支持视频内容大纲 3.1.1 Makefile 规则和示例
参考文档:gunmake.htm
① 为什么需要Makefile
如何高效地编译程序?
你想达到什么效果?请参考Visual Studio:修改源文件或头文件,只需重新编译涉及的文件即可。
您可以重新生成APP
② Makefile其实很简单
一个简单的 Makefile 包含一系列以下格式的“规则”:
目标(target)…: 依赖(prerequiries)…
<tab>命令(command)
如果“依赖文件”比“目标文件”新,则执行“命令”重新生成“目标文件”。该命令的执行有两个条件:依赖文件比目标文件新,或者目标文件尚未生成。
③ 首先介绍一下Makefile的两个作用
A.$(foreach var,列表,文本)
简单地说,对于列表中的每个变量,将其更改为文本。对于列表中的每个元素,将其取出并赋值给var,然后将var更改为文本描述的形式。
例子:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d) // 最终 dep_files := .a.o.d .b.o.d
B.$(通配符模式)
按pattern列出的文件是否存在,列出所有存在的文件。
例子:
src_files := $( wildcard *.c) // 最终 src_files 中列出了当前目录下的所有.c` 文件
④ 逐步完善Makefile
第一个 Makefile 简单、粗暴且低效:
test : main.c sub.c sub.h
gcc -o test main.c sub.c
第二个Makefile效率很高,类似规则太多且冗长,并且不支持头文件检测:
test : main.o sub.o
gcc -o test main.o sub.o
main.o : main.c
gcc -c -o main.o main.c
sub.o : sub.c
gcc -c -o sub.o sub.c
clean:
rm *.o test -f
第三个Makefile高效精炼,不支持检测头文件:
test : main.o sub.o
gcc -o test main.o sub.o
%.o : %.c
gcc -c -o $@ $<
clean:
rm *.o test -f
第四个Makefile高效、精致,支持头文件检测(但需要手动添加头文件规则):
test : main.o sub.o
gcc -o test main.o sub.o
%.o : %.c
gcc -c -o $@ $<
sub.o : sub.h
clean:
rm *.o test -f
第五个Makefile高效、精致,并且支持自动检测头文件:
objs := main.o sub.o
test : $(objs)
gcc -o test $^
# 需要判断是否存在依赖文件
# .main.o.d .sub.o.d
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))
# 把依赖文件包含进来
ifneq ($(dep_files),)
include $(dep_files)
endif
%.o : %.c
gcc -Wp,-MD,.$@.d -c -o $@ $<
clean:
rm *.o test -f
distclean:
rm $(dep_files) *.o test -f
3.1.2 通用Makefile的使用
我参考Linux内核Makefile写了一个通用的Makefile,可以用来编译应用程序:
① 支持多目录、多层目录、多文件;
② 支持设置所有文件的编译选项;
③ 支持设置某个目录的编译选项;
④ 支持对某个文件单独设置编译选项;
⑤ 简单易用。
使用 git 下载本教程的文档后,可以在以下目录中找到说明和示例:
01_all_series_quickstart\
04_嵌入式 Linux 应用开发基础知识\source\05_general_Makefile
3.1.3 通用Makefile分析
①零星知识点
A、make命令的使用:
执行make命令时,会在当前目录下查找名为“Makefile”的文件,并根据其指令进行操作,生成第一个目标。
我们可以使用“-f”选项来指定文件,而不是使用名为“Makefile”的文件,例如:
make -f Makefile.build
我们可以使用“-C”选项来指定目录并切换到其他目录,例如:
make -C a/ -f Makefile.build
我们可以指定一个目标,不再默认生成第一个目标:
make -C a/ -f Makefile.build other_target
B. 立即变量和延迟变量:
定义变量的语法如下:
A = xxx // 延时变量
B ?= xxx // 延时变量,只有第一次定义时赋值才成功;如果曾定义过,此赋值无效
C := xxx // 立即变量
D += yyy // 如果 D 在前面是延时变量,那么现在它还是延时变量;
// 如果 D 在前面是立即变量,那么现在它还是立即变量
GNU make中有两种给变量赋值的方式:延迟变量和立即变量。
上图中,变量A是一个延迟变量,只有在使用时才对其值进行扩展和确定。例如:
A = $@
test:
@echo $A
在上面的Makefile中,变量A的值仅在执行期间确定。它等于test,是一个延迟变量。
如果使用“A := @”,这是一个立即变量,然后@”,这是一个立即变量,然后@”,这是一个立即变量,然后@为空,所以A的值为空。
C. 变量导出:
在编译程序时,我们会不断地使用“make -C dir”切换到其他目录,执行其他目录中的Makefile。如果您希望变量的值在所有目录中可见,请将其导出。
例如“CC = $(CROSS_COMPILE)gcc”,这个CC变量代表编译器,在整个过程中都是一样的。定义好后,使用“export CC”将其导出。
D. Makefile 中可以使用 Shell 命令:
例如:
TOPDIR := $(shell pwd)
这是一个立即变量,TOPDIR 等于 shell 命令 pwd 的结果。
E. 如何将第一个目标放入 Makefile 中:
如果执行make命令时不指定目标,则默认生成第一个目标。
所以“目标1”,位置很重要。有时将第一个目标完全放在文件前面并不方便。在这种情况下,您可以将目标直接放在文件前面,然后再完善其依赖项和命令。例如:
First_target: // 这句话放在前面
.... // 其他代码,比如 include 其他文件得到后面的 xxx 变量
First_target : $(xxx) $(yyy) // 在文件的后面再来完善
command
F. 假设目标:
我们的 Makefile 中有这个目标:
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
如果当前目录下正好有一个名为clean的文件,那么执行make clean时就不会执行这些删除命令。
这时,我们需要将“clean”目标设置为“假想目标”,以保证执行“make clean”时能够执行那些删除命令。
使用以下语句将“clean”设置为假设目标:
.PHONY : clean
G、常用功能:
我。 $(foreach var,列表,文本)
简单地说,对于列表中的每个变量,将其更改为文本。对于列表中的每个元素,将其取出并赋值给var,然后将var更改为文本描述的形式。
例子:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d) // 最终 dep_files := .a.o.d .b.o.d
二. $(通配符模式)
按pattern列出的文件是否存在,列出所有存在的文件。
例子:
src_files := $( wildcard *.c) // 最终 src_files 中列出了当前目录下的所有.c 文件
三. $(过滤模式...,文本)
过滤掉并保留文本中符合模式格式的内容。
例子:
obj-y := a.o b.o c/ d/
DIR := $(filter %/, $(obj-y)) //结果为:c/ d/
四. $(过滤掉模式...,文本)
过滤掉文本中符合模式格式的内容。
例子:
obj-y := a.o b.o c/ d/
DIR := $(filter-out %/, $(obj-y)) //结果为:a.o b.o
六. $(patsubst模式,替换,文本)
在文本'中查找与格式模式匹配的单词'并将其替换为替换'。通配符可以用在“pattern”和“replacement”中。
例如:
subdir-y := c/ d/
subdir-y := $(patsubst %/, %, $(subdir-y)) // 结果为:c d
② 通用Makefile的设计思路
A、在Makefile中确定需要编译的文件和目录,如:
obj-y += main.o
obj-y += a/
“Makefile”文件始终包含在“Makefile.build”中。
B、在Makefile.build中设置编译规则。有3条编译规则:
我。如何编译子目录?进入子目录进行编译:
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
二.如何编译当前目录下的文件?
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
三.当前目录下的.o和子目录下的built-in.o必须打包:
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
C、在顶层Makefile中,将顶层目录中的built-in.o链接到APP中:
$(TARGET) : built-in.o
$(CC) $(LDFLAGS) -o $(TARGET) built-in.o
③ 情景解读
无需阅读本节下面的内容。为编写《嵌入式Linux应用开发完全手册升级版》一书而准备。只需结合第 3.0 节观看视频即可。
3.2 Makefile规则
一个简单的 Makefile 包含一系列以下格式的“规则”:
目标(target)…: 依赖(prerequiries)…
<tab>命令(command)
目标通常是要生成的文件的名称,可以是可执行文件或OBJ文件,也可以是要执行的操作的名称,例如clean。
依赖项是用于生成目标的材料(例如源文件)。一个目标通常有多个依赖项。
命令是生成目标时执行的操作。一条规则可以包含多个命令,每个命令占一行。
注意:每个命令行前面必须有一个Tab字符,即命令行的第一个字符是Tab。这就是事情很容易出错的地方。
通常,如果依赖项发生更改,则需要规则来调用命令来更新或创建目标。但并非所有目标都具有依赖性。例如,目标“clean”用于清理文件,它没有依赖关系。
规则通常解释如何以及何时重建目标。在创建或更新目标之前,make 首先调用命令来处理依赖关系。
当然,规则也可以用来解释如何以及何时执行一个动作,即打印一条提示消息。
Makefile 可以包含规则以外的文本,但简单的 Makefile 只需包含规则。虽然实际规则比此处显示的示例更复杂,但格式完全相同。
对于上述Makefile,执行“make”命令时,只有当hello.c文件比hello文件新时,才会执行命令“armlinux-gcc –o hello hello.c”,生成可执行文件hello;如果还没有hello文件,该命令也会被执行。
当运行“make clean”时,由于目标clean没有依赖关系,因此它的命令“rm -f hello”将被强制执行。
3.3 Makefile中的赋值方法
定义变量的语法如下:
immediate = deferred
immediate ?= deferred
immediate := immediate
immediate += deferred or immediate
define immediate
deferred
endef
GNU make中有两种给变量赋值的方式:延迟变量和立即变量。区别在于它们的定义方式和扩展方式。前者是在使用变量时展开的,也就是说变量的值在实际使用时就确定了;后者的值在定义时就已经确定了。使用=、?= 定义的变量或使用define 指令定义的变量是延迟变量;使用 := 定义的变量是直接变量。需要注意的一点是,?= 仅在变量尚未定义时才有效,即 ?= 用于定义延迟变量第一次出现的情况。
对于附加运算符+=,如果前面使用(:=)将右侧变量定义为立即变量,则它也是立即变量,否则它是延迟变量。
3.4 Makefile常用函数
函数调用的格式如下:
$(function arguments)
这里的function是函数名,arguments是函数的参数。参数和函数名称由空格或制表符分隔。
如果有多个参数,请用逗号分隔。这些空格和逗号不是参数值的一部分。
内核Makefile中使用了大量的函数。现在我们就介绍一些常用的。
3.4.1 字符串替换和分析函数
(1)$(替换自、至、文本)
使用 to 替换文本中出现的所有 from。
例如:
$(subst ee,EE,feet on the street)
结果是街上的 fEEt。
(2)$(patsubst 模式,替换,文本)
查找文本中与格式模式匹配的单词,并将其替换为替换。模式和替换
通配符可以用在 .
例如:
$(patsubst %.c,%.o,x.c.c bar.c)
结果是:xco bar.o。
(3)$(剥离字符串)
删除前导和尾随空格,并将中间的多个空格压缩为单个空格。
例如:
$(去掉 abc )
结果是abc。
(4)$(findstring 查找,in)
在字符串in中查找find。如果找到则返回值为find,否则返回值为空。
例如:
$(查找字符串 a,abc)
$(查找字符串a,bc)
将分别产生值 a 和 `` (空字符串)。
(5)$(过滤模式...,文本)
返回文本中以空格分隔且与格式模式匹配的单词...,并删除与格式模式不匹配的单词...。
例如:
$(filter %.c %.s,foo.c bar.c baz.s ugh.h)
结果是 foo.c bar.c baz.s。
(6)$(过滤掉模式...,文本)
返回文本中与格式模式不匹配的以空格分隔的单词...,并删除与格式模式匹配的单词...。它是函数filter的反函数。
例如:
$(filter %.c %.s,foo.c bar.c baz.s ugh.h)
结果是呃.h。
(7)$(排序列表)
按字母顺序对列表中的单词进行排序并删除重复的单词。输出由单个空格分隔的单词列表。
例如:
$(sort foo bar lose)
返回值是bar foo loss。
3.4.2 文件名功能
(1)$(目录名称...)
提取名称中每个文件名的路径部分...文件名的路径部分包括从文件名的第一个字符到最后一个斜杠。
破折号之前的所有字符(包括斜杠)。
例如:
$(dir src/foo.c hacks)
结果是 src/./。
(2)$(不是目录名称...)
提取names...中每个文件名中除路径部分(真实文件名)之外的所有字符。
例如:
$(notdir src/foo.c hacks)
结果就是 foo.c 被黑客攻击。
(3)$(后缀名...)
提取names中每个文件名的后缀....
例如:
$(suffix src/foo.c src-1.0/bar.c hacks)
结果是.c.c。
(4)$(基本名称...)
提取名称中每个文件名中除后缀之外的所有字符....
例如:
$(basename src/foo.c src-1.0/bar hacks)
结果是 src/foo src-1.0/bar hacks。
(5)$(addsuffix 后缀,名称...)
参数name...是一系列文件名,用空格分隔; suffix是后缀名。将后缀(suffix)的值附加到每个独立文件名的末尾。完成后,文件名将连接起来,并用一个空格分隔。
例如:
$(addsuffix .c,foo bar)
结果是 foo.c bar.c。
(6)$(addprefix 前缀,名称...)
参数名称是一系列文件名,以空格分隔; prefix 是前缀名称。 will 前缀(前缀)
的值附加到每个单独文件名的前面,完成后,文件名将连接起来,并用一个空格分隔。
例如:
$(addprefix src/,foo bar)
结果是 src/foo src/bar。
(7)$(通配符模式)
参数pattern是包含通配符的文件名格式(通配符的使用方式与shell中相同)。函数通配符
结果是与格式匹配的实际文件名称列表,以空格分隔。
例如,当前目录下有文件1.c、2.c、1.h、2.h,则:
c_src := $(wildcard *.c)
结果是 1.c 2.c。
3.4.3 其他功能
(1)$(foreach 变量,列表,文本)
前两个参数 var 和 list 将首先展开。注意,此时最后一个参数文本并未展开;然后,将列表扩展得到的每个单词赋值给var变量;那么text引用的是要扩展的变量,所以每次text的扩展名都是不同的。
该函数的结果是由空格分隔的文本。对列表进行多次扩展后,得到新的列表,即:text。多次展开的单词连接在一起,单词之间用空格分隔。这创建了一个函数。 foreach的返回值。
下面是一个简单的示例,它将变量 files 的值设置为 dirs 中所有目录中的所有文件的列表:
dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))
这里的text为$(通配符$(dir)/*),其扩展过程如下:
① 赋给变量dir的第一个值为a,扩展结果为$(通配符a/*);
② 赋给变量dir的第二个值为b,扩展结果为$(通配符b/*);
③ 赋给变量dir的第三个值为c,扩展结果为$(通配符c/*);
④ 如此继续扩大。
此示例与以下示例具有相同的结果:
files := $(wildcard a/* b/* c/* d/*)
(2)$(if条件,则部分[,else部分])
首先删除第一个参数“condition”的前导和尾随空格,然后展开它。如果扩展为非空字符串,则条件“condition”为“true”;如果扩展为空字符串,则条件“条件”为“假”。
如果条件'condition'为'true',则计算第二个参数'then-part'的值并将其用作整个函数if的值。
如果条件'condition'为'false'且第三个参数存在,则计算第三个参数'else-part'的值,并将该值作为整个函数if的值;如果第三个参数不存在,则该函数将不计算任何内容并返回空值。
注意:只能计算“then-part”和“else-part”之一,不能同时计算两者。这可能会产生副作用(例如函数 shell 调用)。
(3) (originvariable) 变量“variable”是查询变量的名称,而不是对该变量的引用。因此,不能使用 '(origin 变量)。变量“variable”是查询变量的名称,而不是对该变量的引用。因此,不能使用 '(originvariable) 变量。 “variable”是查询变量的名称,而不是对该变量的引用。因此,变量不能写成''加括号的格式。当然,如果需要使用非常量的文件名,可以在文件名中使用变量引用。
函数origin的结果是一个字符串,字符串变量定义如下:
'undefined':如果变量'variable'从未被定义过;
'default':变量'variable'是默认定义;
'environment':变量'variable'定义为环境变量,选项'-e'不打开;
'environment override':变量'variable'被定义为环境变量,选项'-e'打开;
'file':变量'variable'在Makefile中定义;
‘command line’:变量‘variable’在命令行中定义;
'override':变量'variable'是在Makefile中使用override指令定义的;
'automatic':变量'variable'是一个自动变量
(4)$(shell 命令参数)
函数shell是make与外部环境之间的通信工具。执行函数 shell 的结果与在控制台上执行“命令参数”的结果类似。但是,如果“命令参数”的结果包含换行符(和回车符),它们将在函数 shell 的返回结果中被处理为单个空格。如果返回结果以换行符(和回车符)结尾,它将被删除。 。
例如,当前目录下有文件1.c、2.c、1.h、2.h,则:
c_src := $(shell ls *.c)
结果是“1.c 2.c”
“Makefile简介”的这一部分可以作为阅读内核、引导加载程序和应用程序的Makefile文件时的参考手册。下面以options程序的Makefile为例进行演示。 Makefile的内容如下:
File: Makefile
01 src := $(shell ls *.c)
02 objs := $(patsubst %.c,%.o,$(src))
03
04 test: $(objs)
05 gcc -o $@ $^
06
07 %.o:%.c
08 gcc -c -o $@ $<
09
10 clean:
11 rm -f test *.o
在上面的Makefile中,@、@、@、^、