GNOL3'S BASEMENT

Don't cry over spilt milk.

0%

makefile笔记(一)_详记

Makefile的书写方式

学习参考

本文参考资料(因为是看着参考资料写的笔记,故无法做到无错别字)

跟着SeisMan学习Makefile,参考学习文档

或者点击下载PDF《跟我一起写Makefile》

初部了解makefile规则

1
2
3
4
target ... : prerequisites ...
command
...
...

我们以上面的一组模板来进行学习:

  1. target –> 可以是一个object file,也可以是执行文件,还可以是label。label与伪目标息息相关。
  2. prerequisites –> 是我们target的依赖文件,比如说main.o是main.out的依赖文件,而main.c main.h有时main.o的依赖文件。
  3. command –> 字面意义上的命令,是本target所要执行的任意shell命令

模仿示例

一个简单的例子

  • 小明想要编译一份新手PWN题目,需要关闭NX,关闭stack_protector,关闭PIE,但是每次改代码后重新编译都需要打长长的一串代码,这让小明很难受,你能帮小明写一个makefile让编译变的简单吗?(假设源文件为main.c)
1
2
3
4
5
ez_stack : main.o
cc -o ez_stack main.o -z execstack -g -fno-stack-protector -no-pie

main.o : main.c
cc -c main.c

运行一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[~/makefile_exercise]$ ls
main.c makefile
[~/makefile_exercise]$ make
cc -c main.c
cc -o ez_stack main.o -z execstack -g -fno-stack-protector -no-pie
[~/makefile_exercise]$ ls
ez_stack main.c main.o makefile
[~/makefile_exercise]$ checksec ez_stack
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/gnol/makefile_exercise/ez_stack'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
[~/makefile_exercise]$

当然这种简单的单文件,我们也可以直接:

1
2
3
ez_stack : main.c
cc main.c -o ez_stack -z execstack -g -fno-stack-protector -no-pie

为了可读性,我们可以加入\来进行换行,\作为换行作为换行的地方非常的多,基本上这也是一个常识。

所以我们可以把上述代码改写为:

1
2
3
4
5
ez_stack : main.c
cc main.c -o ez_stack \
-z execstack -g \
-fno-stack-protector \
-no-pie

文件稍微多一点的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

我们可以看到,大概的编译流程为先将x.c与x.h一同编译成x.o中间目标文件(vc编译出来的可能是.obj)。

实际上编译出.o文件到底要用到哪些.h文件在对应的.c文件中都进行了头文件包含,所以我们这么写是要让编写者明白项目依赖关系。而make工作只是执行了我们的定义命令,但是我们写依赖关系并非只是让自己看,make会比较targets和prerequisites文件的修改日期,如果prerequisites文件修改日期比targets要新,或者targets不存在的时候,make才会执行定义命令。简而言之,我们可以用以下伪代码来理解:

1
2
3
4
5
6
7
8
//时间戳越大表示时间离'现在'最进,也就是最新的
//time_prerequisites 依赖文件最近被修改的时间
//time_target 目标文件最近被修改的时间
//is_exist(target) 判断target是否存在的一个宏或者函数
if((time_prerequisites > time_target)&&is_exist(target))
{
system(command);
}

细心的我们(除了我),一定能够发现最后面有一行:

1
2
3
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

clean后没有接任何的依赖文件,有点像C语言或者汇编中的LABEL,我们的make对于这样的内容,因为上述伪指令描述的过程根本不成立,所以也不会自动的去执行后面所定义的命令。如果我们想要执行它,我们需要显式的调用,如make clean,如果你有过Linux使用精力,那一定也见到过安装文档里面有着make install这样的命令吧。类似这样的方法非常有用,我们可以在一个makefile中定义不用编译或者是与编译无关的命令,比如我们程序的打包,程序的备份,中间文件的删除等等。

make是如何工作的

在默认的方式下,也就是我么乃至输入make命令。那么make会进行下面的操作:

  1. 在当前目录中寻找名叫Makefile或者makefile的文件。
  2. 如果找到,便会存照文件中第一个target,并将此target作为最终的目标文件。以ez_stack这个为例就是,target == ez_stack的话,我们make会寻找到这个叫做ez_stack的文件并且追踪文件状态(实际上就是获取此文件的最近修改时间)
  3. 如果此target不存在,便会寻找prerequisites中的文件,如果能找到prerequisites中的文件,便会执行command部分的命令来生成文件。
  4. 如果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是否满足要求。(这很像一个递归,也可以说很像一个栈,同样你能感受到有分治的思想。)
  5. 总结,因为存在一级一级的target:prerequisites,最后都会细分到单个文件的生成,所有递归出口就是单个源文件的编译,我们层层编译,最后将所有的代码整合为了一个完成的可执行文件。(将一个大问题转化为若干个小问题在逐一攻破)

上述流程是对于第一次执行make而言的,target都是不存在的情况下的标签,对于target已经存在,command又会在何时执行,在上一级标题我们以伪代码形式进行了解释,便不在赘述(提醒一下,就是比较两个文件的最近修改时间啦)。

变量

脚本不能失去变量,就像西方不能失去耶路撒冷。

1
2
3
4
5
ez_stack : main.o
cc -c ez_stack main.o

clean:
rm main.o

上面一段代码,main.o被重复了三次,如果我们的依赖文件不只一个main.o呢,如果.o文件有很多很多,那么看起来一个是非常不方便的,同样可读性可能也会降低,复用性更不用说。

因此我们可以在一开始就做一个变量定义,比如

1
2
3
4
5
6
7
object = mian.o

ez_stack : $(object)
cc -o ez_stack $(object)

clean :
rm $(object)

那么对于文档中给出的edit那个makefile文件,结果就变成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)

后续还需要增加新的.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
rm edit $(objects)

这一段,文档说描述的内容准确到我们无法用自己的话再来转述一遍。写得十分的优美。但值得一提的是,我们可以看见我们的target:prerequisites对,已经变得很像C语言中的头文件包含了。(不知道大家能不能感受的我的想法,这个自动推导真的非常的形象)

以prerequisites为索引的makefile

上述代码其实我们可以发现defs.h被所有源文件所包含,command.hbuffer.h也是被好几个源文件所包含,所以我们可以将makefile写成如下样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

.PHONY : clean
clean :
rm edit $(objects)

非常简洁,但是这却有与C语言的直觉不相匹配了,在上一小结中,我们可以发现依赖的书写是默认以单个.c文件在进行归类,所以显得每一个.o都将所有的依赖的.h文件列了出来,造成了同一个.h的重复出现。

但是这里是以.h文件为基础,在前者能默认推到.c的情况下在组织依赖关系,带来的问题就是同一个.o的重复出现。

我觉得这不是真正的简洁。简洁应该工整而清楚,即使部分可能重复。作者给出了一些他认为的理由,我觉得单依赖关系看不清楚,这个风格就从我这pass了。

清空目标文件的规则

之前我们已经使用过make clean了,我们知道clean只是一个label,而非target。

清空文件有利于我们重新编译文件,同样也能让文件目录保持整洁。

1
2
clean :
rm ez_stack $(object)

上述书写能加稳健的写法是:

1
2
3
.PHONY : clean
clean :
-rm ez_stack $(object)

.PHONY表示clean是一个‘伪目标’,在rm前面加-的意思在于,无视错误,接着执行未执行的事情。因为clean往往是做一些辅助工作,且make会将第一个target作为目标,所以我们默认将clean放在文件的末尾。

Makefile的文件名

默认情况下,make会寻找”GNUmakefile”,”makefile”,”Makefile”等文件,通常我们使用Makefile,因为M大小会非常醒目。

我们还可以使用自己喜欢的名字来命名makefile文件,但是我们需要显式的指明我们的makefile文件,比如:”gnol3”,如果我们要用”gnol3”这个文件,我们需要使用make -f gnol3make --file gnol3来显示的调用。

引用其他的Makefile

在Makefile中使用include关键字可以把别的Makefile包含进来,这很像C语言的文件包含,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是:

1
include <filename>

filename可以是当前操作系统shell的文件模式(可以包含通配符和路径)

如果文件没有指明绝对路径或者相对路径,make会在目录下寻找,如果没有找到,那么make还会在以下路径中寻找:

  1. 如果make执行时,有-I--include-dir参数,那么make就会在这个参数的指定路径下去寻找。
  2. 如果存在目录<prefix>/include(一般为:/usr/local/bin/或者/usr/include)存在的话,make也会去这个目录下寻找。

如果我们指明的文件没有被寻找到,make会显示一个警告,但是不会出现致命错误。make会继续加载文件,当makefile读取完成,make会再重试那些没有找到,或是不能读取的文件,如果还是不行, 此时make会报一个致命错误。如果我们想要忽视掉这些无法读取的文件,我们可以再include前加上-,比如:

1
-include <filename>

其他版本make兼容的相关命令是sinclude,作用和这个是一摸一样的。

make的工作方式

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

我们可以不知道这些信息,但是知道这些信息能让我们更加清楚的知道make在干嘛

总结

我们敲的第一个程序一般来说是下面这个

1
2
3
4
5
6
#include<stdio.h>
int main(void)
{
printf("hello world!\n");
return 0;
}

学习makefile与学习C语言一样,我们在本次Makefile概论的学习中,把握了makefile的一个整体书写方式。在之后我们还需要做更多的练习,来巩固makefile的书写。

练习:

  1. 使用make编译简单的helloworld程序
  2. 书写并使用make,编译用C语言实现的简单的单向链表,需要具有最基本的增删改查功能。

相关阅读:makefile笔记(二)_随笔