目录
GCC是啥?
安装gcc
gcc工作流程
预处理
编译
汇编
链接
gcc常用参数
Makefile
makefile是啥?
make又是啥?
Makefile命名规则
makefile框架
规则的执行
文件的时间戳
make常用选项
变量
模式匹配和伪目标
函数
Makefile中通用部分做公共头文件
Makefile中使用shell命令
Makefile中条件判断
要想学会makefile,我觉得首先得了解GCC吧,要不然一团雾水,gcc是linux下的编译工具集,是GNU Compiler Collection的缩写,包含gcc、g++等编译器。
有些linux默认没有gcc,需要自己安装
$ sudo apt install gcc g++
可查看自己安装gcc的版本
$ gcc -V
- 预处理:主要做3件事:展开头文件、宏替换、去掉行注释。需要gcc调用预处理器来完成,最终得到的还是源文件
- 编译:需要gcc调用编译器对文件进行编译,最终得到一个汇编文件
- 汇编:需要gcc调用汇编器对文件进行汇编,最终得到一个二进制文件
- 连接:需要gcc调用链接器对程序需要调用的库进行链接,最终得到一个可执行的二进制文件
(参数 -o 指定生成的文件名)
test.c源文件
#include
#define NUM 5
int main() {
// 打印5个hello world
for(int i = 0; i < NUM; i++) {
printf("hello world!\n");
}
return 0;
}
$ gcc -E test.c -o test.i
会得到一个test.i的文件
$ gcc -S test.i -o test.s
会得到一个test.s的汇编文件
$ gcc -c test.s -o test.o
会得到一个test.o的目标文件 (看不懂)
$ gcc test.o -o test
得到一个可执行文件test
在使用gcc编译时可以通过参数控制内部自动执行几个步骤
参数-c进行文件的汇编,汇编之前的两步会自动执行
该命令直接一步到位。直接链接生成可执行文件,连接之前的三步会自动执行
(不加-o的话,gcc会自动生成文件名为a.out的可执行文件 )
-E : 预处理指定的源文件
-S : 编译指定的源文件
-c : 编译、汇编指定的源文件
-o [file2] [file1] / [file1] -o [file2] : 将file1编译成file2
-I : 指定include包含文件的搜索目录
-g : 在编译时,生成调试信息,该程序可以被调试器调试
-D : 在程序编译时指定一个宏
-w : 不生成任何警告信息
-Wall : 生成所有警告信息
-l : 在程序编译时指定使用的库
-L : 指定编译时搜索库的路径
-fPIC/fpic : 生成与位置无关的代码
-shared : 生成共享目标文件
-std : 指定C方言
先准备几个.c文件用来测试 :
calc.h
目录结构为:
一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。(百度百科)
make是一个命令工具,是一个解释makefile中指令的命令工具,make工具在构造项目时需要加载一个叫makefile的文件,makefile关系到了整个工程的编译顺序、编译规则。
makefile或者Makefile
Makefile框架是由规则构成的,make命令执行时先在Makefile文件中查找各种规则,对各种规则进行解析后运行规则,规则的基本格式:
target1,target2,...: depend1, depend2,...
[Tab]command
command
......
每条规则由3部分组成:目标(target)、依赖(depend)、命令(command)
- 目标:一般指要编译的目标,也可以是一个动作
- 依赖:执行当前目标所要依赖的条件,某个具体文件或库等一个目标可以有多个依赖
- 命令:该目标下要执行的具体命令,可以没有,也可以有多条,多条时,每条命令占一行,每个命令前必须有一个Tab键
在调用make命令编译程序时,make会先找到Makefile文件中的第1个格则,分析并执行相关动作,但是,在很多时候要执行的动作中使用的依赖是不存在的,如果需要使用的依赖不存在,这个动作也就不会执行,所以我们需要先将我们需要的依赖生成出来,在Makefile中添加新的规则,将不存在的依赖作为新规则中的目标,当这条新的规则对应的命令执行完毕,对应目标就被生成了,同时另一条规则中需要的依赖也就存在了。
如果只执行make命令,只会执行Makefile里的第一条规则,如果要执行不是第一条规则的命令,需要将要执行规则的目标写到make后面,即:make target。
如果要执行上述代码中clean规则,则:
yan@ubuntu:~/a/test$ make clean
rm *.o calc
make命令执行时会根据文件的时间戳来判定是否执行Makefile文件中相关规则中的命令
目标时间戳 > 所有依赖的时间戳 ---------> 不执行
目标时间戳 < 某些依赖的时间戳 ---------> 执行
假如我们修改一下add.c
此时,目标add.o的时间戳<他的依赖add.c的时间戳,再执行make命令时就可以执行
从上面也可以看到,当我们只改变add.c一个文件时,再执行make只会编译有改动的代码,所以,有人一开始学就会想,明明
calc:main.c add.c sub.c mul.c div.c
gcc main.c add.c sub.c mul.c div.c -I ./include -o calc
这样两行代码就可以解决的事,偏要把它拆分成那么多行来实现,但是,如果有很多.c文件呢,只要改动一个.c文件的内容,再执行make命令,他就会全部再编译一遍,执行效率是很低的。
make 默认在当前目录中寻找 GUNmakefile,makefile,Makefile 的文件作为 make 的输入文件
-f : 可以指定除上述文件名之外的文件作为输入文件
-v : 显示版本号
-n : 只输出命令但不执行
-s : 只执行命令,但不显示具体命令,此处也可在命令中用@符抑制命令输出
-w : 显示执行前执行后的路径
-C dir : 指定Makefile所在目录
将变量名的值取出的方法:$(变量名)
一开始的Makefile文件(最普通的写法):
calc:main.o add.o sub.o mul.o div.o
gcc main.o add.o sub.o mul.o div.o -o calc
main.o:main.c
gcc main.c -c -I ./include
add.o:add.c
gcc add.c -c -I ./include
sub.o:sub.c
gcc sub.c -c -I ./include
mul.o:mul.c
gcc mul.c -c -I ./include
div.o:div.c
gcc div.c -c -I ./include
clean:
rm *.o calc
自定义变量:(将上述2、3行改成)
常见预定义变量:
AR 生成静态库库文件的程序名称, 默认值为ar
AS 汇编编译器的名称, 默认值为as
CC C语言编译器的名称, 默认值为cc
CPP C语言与编译器的名称, 默认值为$(CC) -E
CXX C++语言编译器的名称, 默认值为g++
RM 删除文件程序的名称, 默认值为rm -f
ARFLAGS 生成静态库库文件程序选项 无默认值
ASFLAGS 汇编语言编译器的编译选项 无默认值
CFLAGS C 语言编译器的编译选项 无默认值
CPPFLAGS C 语言预编译的编译选项 无默认值
CXXFLAGS C++语言编译器的编译选项 无默认值
现在又可将一开始的Makefile文件改成:
1 TARGETS=calc
2 OBJS=main.o add.o sub.o mul.o div.o
3 CFLAGS+=-c
4 include=./include
5 $(TARGETS):$(OBJS)
6 $(CC) $(OBJS) -o $(TARGETS)
7
8
9 main.o:main.c
10 $(CC) main.c $(CFLAGS) -I include
11
12 add.o:add.c
13 $(CC) add.c $(CFLAGS) -I include
14
15 sub.o:sub.c
16 $(CC) sub.c $(CFLAGS) -I include
17
18 mul.o:mul.c
19 $(CC) mul.c $(CFLAGS) -I include
20
21 div.o:div.c
22 $(CC) div.c $(CFLAGS) -I include
23
24 clean:
25 $(RM) $(OBJS) $(TARGETS)
常见的自动变量
$*:表示目标文件的名称,不包含目标文件的扩展名
$+:表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件
$<:表示依赖项中第一个依赖文件的名称
$?:依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
$@:表示目标文件的名称,包含文件扩展名
$^:依赖项中,所有不重复的依赖文件,这些文件之间以空格分开
$%:如果目标是归档成员,则该变量表示目标的归档成员名称
一开始的代码可改成:
1 TARGETS=calc
2 OBJS=main.o add.o sub.o mul.o div.o
3 CFLAGS+=-c
4 include=./include
5 $(TARGETS):$(OBJS)
6 $(CC) $^ -o $@
7
8
9 main.o:main.c
10 $(CC) $< $(CFLAGS) -I include
11
12 add.o:add.c
13 $(CC) $< $(CFLAGS) -I include
14
15 sub.o:sub.c
16 $(CC) $< $(CFLAGS) -I include
17
18 mul.o:mul.c
19 $(CC) $< $(CFLAGS) -I include
20
21 div.o:div.c
22 $(CC) $< $(CFLAGS) -I include
23
24 clean:
25 $(RM) $(OBJS) $(TARGETS)
终于到这了,上面改的我手都麻了
有没有发现有几条很相似的语句:
那我们可不可以把它写成一个模板呢?那肯定可以的,这就用到了模式匹配了。
模式匹配:通过一个公式,代表若干满足条件的规则
%.o:%.c
gcc $< -c
%.o:%.c -----> .o依赖于对应的.c
%是一个通配符,匹配的是文件名
所以一开始的代码又可以改成:
1 TARGETS=calc
2 OBJS=main.o add.o sub.o mul.o div.o
3 CFLAGS+=-c
4 include=./include
5 $(TARGETS):$(OBJS)
6 $(CC) $^ -o $@
7
8
9 %.o:%.c
10 $(CC) $< $(CFLAGS) -I include
11
12 clean:
13 $(RM) $(OBJS) $(TARGETS)
如果我们目录下有一个名为clean的文件或目录,那执行make clean就执行不了了
那如何解决呢,这个时候就需要用到伪目标了
.PHONY:target
声明伪目标后,Makefile将不会判断目标是否存在或该目标是否需要更新
12 .PHONY:clean
13 clean:
14 $(RM) $(OBJS) $(TARGETS)
makefile中的函数都是有返回值的,所以要用$取出返回值
$(函数名 参数1,参数2, ....) 参数之间逗号隔开
两个常用函数:
$(wildcard PATTERN ...)
功能:获取指定目录下指定类型的文件名
参数:指定某个目录,搜索该目录下指定类型的文件
返回值:以空格分割的指定目录下的所有符合条件的文件列表
$(patsubst
, , ) 功能:按照指定的模式替换指定的文件名的后缀
参数:
pattern:模式字符串,指出要被替换的文件名中的后缀
replacement:要替换成什么后缀
text:存储要被替换的原始数据
返回值:被替换过后的字符串
了解这两个函数之后,原始代码又可以修改了。。。
1 TARGETS=calc
2 SRCS=$(wildcard ./*.c)
3 OBJS=$(patsubst %.c, %.o, $(SRCS))
4 CFLAGS+=-c
5 include=./include
6 $(TARGETS):$(OBJS)
7 $(CC) $^ -o $@
8
9
10 %.o:%.c
11 $(CC) $< $(CFLAGS) -I include
12
13 .PHONY:clean
14 clean:
15 $(RM) $(OBJS) $(TARGETS)
在里面分别写两份测试代码:
001cpp中有3个.cpp文件,如下:
002c中有3个.c文件,如下:
那么,通过上面的介绍,应该都会写这两个文件夹对应的makefile文件了吧,这里就直接放代码了哦
有没有发现,他俩的代码基本上是一样的,所以我们就可以重新在两个文件夹的外面重新写一个makefile,然后里面的这两个Makefile直接包含外面的那个,这样不就少写了好多代码嘛,把公共部分给写到另一个makefile里面去。
代码如下:
1
2 src=$(wildcard ./*.cpp ./*.c)
3 obj=$(patsubst %.c, %.o, $(src))
4
5 obj:=$(patsubst %.cpp, %.o, $(obj))
6
7 .PHONY:clean
8
9 $(target):$(obj)
10 $(CXX) $^ -o $@
11
12 %.o:%.cpp
13 $(CXX) $< -c
14
15 clean:
16 $(RM) $(obj) $(target)
17
那001cpp和002c文件夹里的Makefile就可以写成这样了。
至于不知道 :=代表啥,可以去看看这篇博客,或者自己去百度一下。补充一点:不管是gcc还是g++都可以编译C程序,编译程序的规则和参数都相同;g++可以直接编译C++c程序,gcc编译C++程序需要添加额外参数-lstdc++。
该测试的目录结构为:
语法:$(shell shell命令)
1
2 A:=$(shell ls)
3 B:=$(shell pwd)
4
5
6
7 test:
8 echo $(A)
9 echo $(B)
在makefile里面直接使用shell时需注意:
1.shell命令必须在规则里面:
应该写成:
A:=123
test:
if [ "$(A)" = "123" ]; then echo "yes"; else echo "no"; fi
echo "hello"
2. shell命令在makefile调用的时候每行shell都是一个单独的进程,上一行定义的变量在下一行是无效的
这种写法是输出不了B的值的,要写成:
test:
B=hello;echo $$B
或者:
test:
B=hello;\
echo $$B
3.在Makefile中要想引用shell的变量,应该以$$开头,shell变量不需要括号
上面那个例子中输出B的值,如果改成echo $B,则输出不了B的值。
条件判断
ifeq 判断是否相等
ifneq 判断是否不相等
ifdef 判断变量是否存在
ifndef 判断变量是否不存在
A=123
RS1:=
RS2:=
RS3:=
RS4:=
ifeq ($(A),123)
RS1:=yes
else
RS1:=no
endif
ifdef RS2
RS3:=YES
else
RS3:=NO
endif
test:
echo $(RS1)
echo $(RS3)
执行结果:
参考资料:Makefile | 爱编程的大丙 001-makefile相关概念介绍_哔哩哔哩_bilibili