Makefile的书写方式
学习参考
本文参考资料(因为是看着参考资料写的笔记,故无法做到无错别字)
跟着SeisMan学习Makefile,参考学习文档
或者点击下载PDF《跟我一起写Makefile》
初部了解makefile规则
1 | target ... : prerequisites ... |
我们以上面的一组模板来进行学习:
- target –> 可以是一个object file,也可以是执行文件,还可以是label。label与伪目标息息相关。
- prerequisites –> 是我们target的依赖文件,比如说main.o是main.out的依赖文件,而main.c main.h有时main.o的依赖文件。
- command –> 字面意义上的命令,是本target所要执行的任意shell命令
模仿示例
一个简单的例子
- 小明想要编译一份新手PWN题目,需要关闭NX,关闭stack_protector,关闭PIE,但是每次改代码后重新编译都需要打长长的一串代码,这让小明很难受,你能帮小明写一个makefile让编译变的简单吗?(假设源文件为
main.c
)
1 | ez_stack : main.o |
运行一下:
1 | [~/makefile_exercise]$ ls |
当然这种简单的单文件,我们也可以直接:
1 | ez_stack : main.c |
为了可读性,我们可以加入\
来进行换行,\
作为换行作为换行的地方非常的多,基本上这也是一个常识。
所以我们可以把上述代码改写为:
1 | ez_stack : main.c |
文件稍微多一点的例子
1 | edit : main.o kbd.o command.o display.o \ |
我们可以看到,大概的编译流程为先将x.c与x.h一同编译成x.o中间目标文件(vc编译出来的可能是.obj)。
实际上编译出.o文件到底要用到哪些.h文件在对应的.c文件中都进行了头文件包含,所以我们这么写是要让编写者明白项目依赖关系。而make工作只是执行了我们的定义命令,但是我们写依赖关系并非只是让自己看,make会比较targets和prerequisites文件的修改日期,如果prerequisites文件修改日期比targets要新,或者targets不存在的时候,make才会执行定义命令。简而言之,我们可以用以下伪代码来理解:
1 | //时间戳越大表示时间离'现在'最进,也就是最新的 |
细心的我们(除了我),一定能够发现最后面有一行:
1 | clean : |
clean
后没有接任何的依赖文件,有点像C语言或者汇编中的LABEL,我们的make对于这样的内容,因为上述伪指令描述的过程根本不成立,所以也不会自动的去执行后面所定义的命令。如果我们想要执行它,我们需要显式的调用,如make clean
,如果你有过Linux使用精力,那一定也见到过安装文档里面有着make install
这样的命令吧。类似这样的方法非常有用,我们可以在一个makefile中定义不用编译或者是与编译无关的命令,比如我们程序的打包,程序的备份,中间文件的删除等等。
make是如何工作的
在默认的方式下,也就是我么乃至输入make
命令。那么make会进行下面的操作:
- 在当前目录中寻找名叫
Makefile
或者makefile
的文件。 - 如果找到,便会存照文件中第一个target,并将此target作为最终的目标文件。以ez_stack这个为例就是,target == ez_stack的话,我们make会寻找到这个叫做ez_stack的文件并且追踪文件状态(实际上就是获取此文件的最近修改时间)
- 如果此target不存在,便会寻找prerequisites中的文件,如果能找到prerequisites中的文件,便会执行command部分的命令来生成文件。
- 如果prerequisites中的依赖文件也有存在不存在的情况,便递归向下寻找依赖文件的生成方式,通常表现为另一个
target:prerequisites ; command
,以edit这个为例,如果main.o不存在,就会寻找main.o如何生成,然后找到main.o : main.c defs.h
,并执行cc -c main.c
产生main.o文件,再去判断edit : main.o kbd.o command.o display.o
是否满足要求。(这很像一个递归,也可以说很像一个栈,同样你能感受到有分治的思想。) - 总结,因为存在一级一级的
target:prerequisites
,最后都会细分到单个文件的生成,所有递归出口就是单个源文件的编译,我们层层编译,最后将所有的代码整合为了一个完成的可执行文件。(将一个大问题转化为若干个小问题在逐一攻破)
上述流程是对于第一次执行make而言的,target都是不存在的情况下的标签,对于target已经存在,command又会在何时执行,在上一级标题我们以伪代码形式进行了解释,便不在赘述(提醒一下,就是比较两个文件的最近修改时间啦)。
变量
脚本不能失去变量,就像西方不能失去耶路撒冷。
1 | ez_stack : main.o |
上面一段代码,main.o被重复了三次,如果我们的依赖文件不只一个main.o呢,如果.o文件有很多很多,那么看起来一个是非常不方便的,同样可读性可能也会降低,复用性更不用说。
因此我们可以在一开始就做一个变量定义,比如
1 | object = mian.o |
那么对于文档中给出的edit那个makefile文件,结果就变成了
1 | objects = main.o kbd.o command.o display.o \ |
后续还需要增加新的.o文档,只用改一个object
就ok了,十分方便。
更加智能的make
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个 .o 文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
只要make看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果make找到一个 whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的新makefile又出炉了。
.PHONY
表示clean
是个伪目标文件。
1 | objects = main.o kbd.o command.o display.o \ |
这一段,文档说描述的内容准确到我们无法用自己的话再来转述一遍。写得十分的优美。但值得一提的是,我们可以看见我们的target:prerequisites
对,已经变得很像C语言中的头文件包含了。(不知道大家能不能感受的我的想法,这个自动推导真的非常的形象)
以prerequisites为索引的makefile
上述代码其实我们可以发现defs.h
被所有源文件所包含,command.h
与buffer.h
也是被好几个源文件所包含,所以我们可以将makefile写成如下样式:
1 | objects = main.o kbd.o command.o display.o \ |
非常简洁,但是这却有与C语言的直觉不相匹配了,在上一小结中,我们可以发现依赖的书写是默认以单个.c文件在进行归类,所以显得每一个.o都将所有的依赖的.h文件列了出来,造成了同一个.h的重复出现。
但是这里是以.h文件为基础,在前者能默认推到.c的情况下在组织依赖关系,带来的问题就是同一个.o的重复出现。
我觉得这不是真正的简洁。简洁应该工整而清楚,即使部分可能重复。作者给出了一些他认为的理由,我觉得单依赖关系看不清楚,这个风格就从我这pass了。
清空目标文件的规则
之前我们已经使用过make clean
了,我们知道clean只是一个label,而非target。
清空文件有利于我们重新编译文件,同样也能让文件目录保持整洁。
1 | clean : |
上述书写能加稳健的写法是:
1 | .PHONY : clean |
.PHONY
表示clean
是一个‘伪目标’,在rm前面加-
的意思在于,无视错误,接着执行未执行的事情。因为clean往往是做一些辅助工作,且make会将第一个target作为目标,所以我们默认将clean放在文件的末尾。
Makefile的文件名
默认情况下,make会寻找”GNUmakefile”,”makefile”,”Makefile”等文件,通常我们使用Makefile,因为M大小会非常醒目。
我们还可以使用自己喜欢的名字来命名makefile文件,但是我们需要显式的指明我们的makefile文件,比如:”gnol3”,如果我们要用”gnol3”这个文件,我们需要使用make -f gnol3
或make --file gnol3
来显示的调用。
引用其他的Makefile
在Makefile中使用include
关键字可以把别的Makefile包含进来,这很像C语言的文件包含,被包含的文件会原模原样的放在当前文件的包含位置。include
的语法是:
1 | include <filename> |
filename
可以是当前操作系统shell的文件模式(可以包含通配符和路径)
如果文件没有指明绝对路径或者相对路径,make会在目录下寻找,如果没有找到,那么make还会在以下路径中寻找:
- 如果make执行时,有
-I
或--include-dir
参数,那么make就会在这个参数的指定路径下去寻找。 - 如果存在目录
<prefix>/include
(一般为:/usr/local/bin/
或者/usr/include
)存在的话,make也会去这个目录下寻找。
如果我们指明的文件没有被寻找到,make会显示一个警告,但是不会出现致命错误。make会继续加载文件,当makefile读取完成,make会再重试那些没有找到,或是不能读取的文件,如果还是不行, 此时make会报一个致命错误。如果我们想要忽视掉这些无法读取的文件,我们可以再include
前加上-
,比如:
1 | -include <filename> |
其他版本make兼容的相关命令是sinclude
,作用和这个是一摸一样的。
make的工作方式
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
我们可以不知道这些信息,但是知道这些信息能让我们更加清楚的知道make在干嘛
总结
我们敲的第一个程序一般来说是下面这个
1 |
|
学习makefile与学习C语言一样,我们在本次Makefile概论的学习中,把握了makefile的一个整体书写方式。在之后我们还需要做更多的练习,来巩固makefile的书写。
练习:
- 使用make编译简单的helloworld程序
- 书写并使用make,编译用C语言实现的简单的单向链表,需要具有最基本的增删改查功能。
相关阅读:makefile笔记(二)_随笔