[X]关闭

[米联客-XILINX-H3_CZ08_7100] LINUX基础篇连载-08 Makefile入门

文档创建者:LINUX课程
浏览次数:160
最后更新:2024-09-09
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-SOC » 2_LINUX应用开发
软件版本:vitis2021.1(vivado2021.1)
操作系统:WIN10 64bit
硬件平台:适用XILINX Z7/ZU系列FPGA
登录“米联客”FPGA社区-www.uisrc.com视频课程、答疑解惑!

1 什么是Makefile
在搞清楚什么是Makefile之前,我们不妨先来设想一下这个场景。在07篇4小节中,我们最后在链接的时候提到,当有多个文件时应把它们放在一起链接。现在我们有a.c、b.c、c.c三个文件,a依赖b,a依赖c,如果我们需要生成一个可执行文件就能输入以下几条命令:
  1. gcc -c a.c -o a.o  
  2. gcc -c b.c -o b.o  
  3. gcc -c c.c -o c.o  
  4. gcc a.o b.o c.o -o a.out
复制代码
不难发现,为了将三个文件链接到一起,我们需要输入四条命令。当工程还小的话我们还能手动操作一下,当有大量文件比如上万个文件的时候,我们就会考虑使用shell脚本来帮我们完成这些繁琐的任务了。而我们不必为每一个工程编纂一个这样的shell脚本,因为有一个现成的工具可以帮助我们完成这个工作,那就是Makefile。
Makefile制定了整个工程的编译、链接规则,它会告诉gcc哪些需要编译,哪些存在依赖关系,哪些需要链接。Makefile可以使用shell脚本,但是也有自己的一套语法,Makefile可以极大地提高效率,目前已经成为了一种常用的工程编译方法。
2 Makefile的格式
在学习语法前,有必要先来学习一下格式,因为格式不对可能使语法通通失效,Makefile我们之前提到可以使用shell,因此对shell和Makefile本身语法区分十分重要。
2.1 基本格式
Makefile由一系列的规则组成,这些规则的基本格式如下:
  1. <target> : <prerequisites>   
  2. [tab]   <commands>  
复制代码
选项:
  • <target>:目标,作用是确定构建的目标,是不可忽略的。
  • <prerequisites>:先决条件,用来明确目标的依赖关系,如果目标需要依赖其他的文件,需要将文件全部写在这里并以空格隔开。
  • [tab]:tab空格,在命令前需要使用tab空格,普通空格不起作用。
  • <commands>:命令,主要是告诉gcc对目标需要进行什么操作。
以上选项中,先决条件和命令是可选的,但是至少需要存在一个,即不可两者均空,而目标作为规则的主体是不能缺少的。
2.2 目标<target>
目标最常见的内容就是文件名了,比如hello.c。目标可以是一个文件名,也可以是多个文件名,如果是多个文件名则需要用空格将它们一一隔开。
除了文件名,目标还能是一个自己定义的字符串,叫做伪目标。使用过Makefile的应该都知道大名鼎鼎的make clean吧,其中的clean就是一个伪目标,clean在Makefile下最简单的规则如下:
  1. clean:  
  2.     rm *.o
复制代码
当然事情不会是一帆风顺的,不巧在这个目录中正好有一个叫clean的文件,而它已经被汇编成目标文件了,这时候我们make clean就不起作用了,因为clean已经生成目标文件了会被自动跳过。这种情况下就需要提前声明clean是个伪目标,而非文件:
  1. .PHONY: clean  
  2. clean:  
  3.     rm *.o temp  
复制代码
像这样的伪目标声明是非常常见的,并且不仅仅局限于clean这样的关键词。如果没有文件与伪命令混淆的话,不写声明也是可以的,但是最好养成写声明的好习惯,这有利于工程的鲁棒性。
2.3 先决条件<prerequisites>
先决条件经常是文件名,可以是一个文件也可以是多个文件,同样多个文件需要用空格一一隔开。
当先决条件不为空时,在执行当前命令之前会确保每个先决条件都已执行,即先决条件的文件已存在,且先决条件未被修改。如果不符合,那么就会转去执行先决条件,这个过程是递归的,只有从前往后所有条件都满足,才会执行后来的:
因此1小节中的例子其实可以写成如下的形式:
  1. all: test  
  2. test: a.o b.o c.o  
  3.     gcc a.o b.o c.o -o test.out  
  4. a.o: a.c  
  5.     gcc -c a.c  
  6. b.o: b.c  
  7.     gcc -c b.c  
  8. c.o: c.c  
  9.     gcc -c c.c  
  10.   
  11. clean:  
  12.     rm -rf *.o test.out  
复制代码
可以看到当我们make的时候,将默认执行all这个目标。all依赖于test这个目标,于是往下找,找到了test这个目标。但是test由依赖于三个文件,于是test的命令也暂时不执行,继续往下找。直到三个文件都一一汇编完成,才会往回一层层递归,最后生成test.out文件。
如果我们make完第一遍,修改了b.c文件的话,第二次make会发生什么呢?首先test依赖的三个文件中,a.c和c.c都没有改变,所以对应这两个目标的命令不会被执行,b.c由于有修改会被重新汇编,完成后返回test目标。将这些目标文件链接,生成最终的可执行文件。
另外值得注意的是写在先决条件中的内容必须在下面的目标中出现,否则就会因为无法满足先决条件而报错。
通过这种方式,避免了不必要的重复汇编工作,大大节省了所需要的时间。
2.4 命令<commands>
命令是Makefile中最重要的部分,因为这里规定了要怎么处理文件(大部分为生成目标文件),它一般由一行或者多行命令组成,在每段命令之前必须有一个tab空格。命令的每一行都是一个新的shell,所以上一行的内容并不会被下一行读取到。
如果希望被读到的话,可以将它们写在同一行再用空格隔开:
  1. test:  
  2.     export txt=hello; echo "txt=[$$txt]"
复制代码
也可以使用反斜杠:
  1. test:  
  2.     export txt=hello; \  
  3.     echo "txt=[$$txt]"
复制代码
还能使用关键词声明:
  1. .ONESHELL:  
  2. test:  
  3.     export txt=hello;  
  4.     echo "txt=[$$txt]"<span style="font-family: Helvetica; font-size: small; background-color: rgb(255, 255, 255);"> </span>
复制代码
3 Makefile语法
3.1 注释
与众多语言一样,Makefile也支持注释,可以使用#来注释:
  1. #注释  
  2. clean:   
  3.     rm *.o  #注释 <span style="font-family: Helvetica; font-size: small; background-color: rgb(255, 255, 255);"> </span>
复制代码
如果需要多行注释,可以在结尾加上\来将下一行也变成注释:
  1. #注释 \  
  2. 这还是注释
复制代码
在写注释的时候,一般更倾向于将注释作为一个独立的行,而不要和Makefile的有效行放在同一行中书写。
3.2 打印
默认情况下,在make执行的过程中,每条命令在执行前都会被打印出来,目的是为了方便用户看到make执行到哪一步了。
如果有以下的Makefile语句:
  1. test:  
  2.     # 这是测试
复制代码
我们执行make test则会出现以下情况:
  1. [root@ubuntu ~]# make test  
  2. [root@ubuntu ~]# # 这是测试
复制代码
可以看到注释也被一起打印出来了,很明显我们并不会希望注释打印出来,或者是echo语句,会打印一遍命令再执行一遍命令显得很多余。这时候就能在命令前加上@来关闭:
  1. test:  
  2.     @# 这是测试  
  3.     @echo TEST
复制代码
由于我们需要了解make的进度,所以并非每条指令都会加上@符号,常见的使用@的地方为注释和echo。
3.3 通配符
Makefile也支持通配符,其作用是模糊搜索文件名,常用的有星号(*)和问号(?),以下的例子为常见的clean的写法:
  1. clean:   
  2.     rm -f *.o <span style="font-family: Helvetica; font-size: small; background-color: rgb(255, 255, 255);"> </span>
复制代码
这里的*.o的意思就是所有.o后缀的文件,也就是所有目标文件。则clean的任务为强制删除所有目标文件。
3.4 模式匹配
Makefile支持一些简单的模式匹配,最常用的匹配符是%。%的含义与通配符中的*含义类似,譬如有如下语句:
  1. test1.o: test1.c  
  2.     gcc -c test1.c  
  3. test2.o: test2.c  
  4.     gcc -c test2.c
复制代码
使用匹配符%可以写成如下形式:
  1. %.o: %.c  
  2.     gcc -c $<
复制代码
使用%可以减少许多语句,用一条规则就能匹配大量的同类型文件。
3.5 内置变量
所谓内置变量就是Makefile自带的关键词,它们都有特定的含义。当然你也可以自定义,起作用的并不是这些内置变量,而是你所定义的东西。但是介于可读性的考虑,在使用到功能相似或相同的内置变量时,最好不要自定义。Makefile最常用的有CC、CFLAGS、LFLAGS等等。
其中CC表示选择的编译器。CFLAGS表示编译选项,-c 即编译生成目标文件,-Wall表示显示编译过程中遇到的所有warning。LFLAGS用来表示链接选项。更多的内置变量可以查看官方给的手册。
在3.4小节中的语句,则可以用以下方法表示:
  1. CC = gcc  
  2. CFLAGS = -c -Wall  
  3.   
  4. %.o: %.c  
  5.     $(CC) $(CFLAGS) $<
复制代码
3.6 自动变量
自动变量指的是各式各样的用来指代不同内容的符号,正是这些符号把Makefile搞得十分晦涩难懂,这些符号的目的其实是为了简化语句,在3.5小节中,我们就用到了一个自动变量&<。
常用的自动变量:
  • $@:指代all的依赖。
  • $<:指代<prerequisites>里的第一个先决条件。
  • $^:指代<prerequisites>里的所有先决条件。
2.3小节中的语句使用自动变量可以表示为如下形式:
  1. all: test  
  2. test: a.o b.o c.o  
  3.     gcc $^ -o $@  
  4. a.o: a.c  
  5.     gcc -c $<  
  6. b.o: b.c  
  7.     gcc -c $<  
  8. c.o: c.c  
  9.     gcc -c $<  
  10.   
  11. clean:  
  12.     rm -rf *.o test  
复制代码
如果将前两节的内容也用上,就能写成如下效果:
  1. CC = gcc  
  2. CFLAGS = -c -Wall  
  3. LFLAGS = -Wall  
  4.   
  5. all: test  
  6. test: a.o b.o c.o  
  7.     $(CC) $(LFLAGS) $^ -o $@  
  8. %.o: %.c  
  9.     $(CC) $(CFLAGS) $<  
  10.   
  11. clean:  
  12.     rm -rf *.o test  
复制代码
此外还有很多的自动变量,在此仅列举以上三个,更多的自动变量可以参考官方手册。
3.7 变量与赋值
Makefile可以自定义变量,在3.5小节中的内置变量,只需修改名字为非关键词就能成为我们自定义的变量。定义变量使用等于号=,调用变量使用$(),例如:
  1. test = Hello World  
  2. test:  
  3.     @echo $(test)<span style="font-family: Helvetica; font-size: small; background-color: rgb(255, 255, 255);"> </span>
复制代码
此外有别于我们平时所知道的=赋值,Makefile还提供了额外的几种赋值运算符:
  • = 在执行时扩展,允许递归扩展。
  • := 在定义时扩展。
  • ?= 只有在该变量为空时才设置值。
  • += 将值追加到变量的尾端。
来看具体实列:
  1. .ONESHELL:   
  2. test:  
  3.     x = foot  
  4.     y = $(x)ball  
  5.     x = basket
复制代码
y的结果是basketball,因为make会在整个Makefile展开后再赋值,前一次的赋值会被后一次的覆盖。
  1. .ONESHELL:   
  2. test:  
  3.     x := foot  
  4.     y := $(x)ball  
  5.     x := basket
复制代码
y的结果是football,如果不希望赋值被覆盖的话,就能使用:=来避免。使用:=后,变量的值取决于它所在的位置,而不是最终值。
  1. .ONESHELL:   
  2. test:  
  3.     x ?= foot  
  4.     y ?= $(x)ball  
  5.     x ?= basket  
复制代码
y的结果是football,?:只会在变量为空时起作用,也就是只在第一次有效赋值时才起作用。
  1. .ONESHELL:   
  2. test:  
  3.     x = Hello  
  4.     x += Word!
复制代码
x的结果为Hello Word!,在这个例子中+=的作用就是在原有的基础上加上空格和现有语句,也就是拼接。
3.8 判断
Makefile可以使用Bash语法来完成判断,如下所示:
  1. ifeq ($(BIT),32)  
  2.     CC = arm-linux-gnueabi-gcc  
  3. else  
  4.     CC = aarch64-linux-gnu-gcc  
  5. endif
复制代码
上面的语句的含义为,如果架构为32位,则选用arm-linux-gnueabi-gcc编译器,否则选用aarch64-linux-gnu-gcc编译器。
3.9 循环
  1. TEST = a b c  
  2. test:  
  3.     for i in $(TEST); do \  
  4.         echo $$i; \  
  5.     done
复制代码
这个循环的含义为依次打印出TEST列表中的内容,使用的是for循环,这样的写法和Python中的for比较类似。
3.10 函数
Makefile提供了许多内置函数,具体可以参照官方手册,这里仅介绍几个常用的函数。
patsubst函数主要用于模式匹配,可以用来替换文件名称,语法:
  1. SOURCES = a.c b.c c.c  
  2. OBJS = $(patsubst %.c, %.o, $(SOURCES))  
  3. test:  
  4.     @echo $(SOURCES)  
  5.     @echo $(OBJS)
复制代码
输出的结果为:
  1. a.c b.c c.c  
  2. a.o b.o c.o
复制代码
可以看到所有c文件都被替换成了o后缀的目标文件。
wildcard函数用于获取特定规则的文件名,例如:
  1. SOURCE_FILE = $(wildcard *.cpp)  
  2. test:  
  3.     @echo $(SOURCE_FILE)
复制代码
输出的结果为:
  1. a.c b.c c.c
复制代码
可以看到所有c文件都被输出来了。
shell函数可以用来执行一些shell命令,例如:
  1. files := $(shell echo *.c)
复制代码
它的作用和wildcard函数这样写效果是一样的:
  1. files := $(wildcard *.c)
复制代码
4 综合语法Makefile
综合以上各种语法,我们可以写出如下的Makefile文件:
  1. SOURCES = $(wildcard ./*.c)
  2. OBJS = $(patsubst %.c, %.o, $(SOURCES))
  3. FLAGS = -c -Wall
  4. target = test
  5. CC = gcc

  6. $(target): $(OBJS)
  7. $(CC) $(OBJS) -o $(target)

  8. %.o: %.c
  9. $(CC) $(FLAGS) $< -o $@

  10. .PHONY: clean
  11. clean:
  12. rm -rf $(OBJS) $(target)
复制代码

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则