浅析Makefile(基于虚拟机Ubuntu环境)
相信对于很多接触这篇文章的人来说,都对Makefile有着或多或少的理解,最起码应该知道Makefile的核心功能是编译,初学者可能会对这一奇怪产物感到不解其意,这是因为大多数人已经习惯了windows环境下一键编译,事实上,图形化界面的背后是IDE为使用者节省了Makefile这一步。那么简单说说本人对Makefile的理解:相信对接触过Linux的人(只会敲几个命令行的不算>_
浅析Makefile
What is Makefile and how to edit a simple one
什么是Makefile?
相信对于很多接触这篇文章的人来说,都对Makefile有着或多或少的理解,最起码应该知道Makefile的核心功能是编译,初学者可能会对这一奇怪产物感到不解其意,这是因为大多数人已经习惯了windows环境下一键编译,事实上,图形化界面的背后是IDE为使用者节省了Makefile这一步。那么简单说说本人对Makefile的理解:相信对接触过Linux的人(只会敲几个命令行的不算>_<)来说,对于Makefile会有更加清晰的认知,尤其是接触过SHELL脚本编程的友友们。Makefile实际上也是脚本的一种,其中撰写的内容相当于SHELL脚本中的shell命令,但是需要通过命令工具make进行解释,然后操作系统根据指令完成相应的编译链接操作,同时Makefile和C、C++以及Python一样,有属于自己的书写格式、函数、关键字等语法,并且与SHELL脚本有着较大的区别,这一点将会在实际运用中较为明显地体现出来。
那么,到此我们可以认为Makefile本质上是一个建立在一定规则上的具有编译链接文件功能的脚本工具。
为什么是通过Makefile实现工程编译?
相信在学习Makefile之前,很多人已经接触过以下语句:
gcc main.c -o main.o
这个语句的作用在于将main.c这个源文件通过编译链接之后生成可执行文件main.o (PS:“.o"指的是”.obj",即object文件,是Linux系统中的可执行文件,对应的是windows环境下的.exe文件,即excute文件)。
很多时候,可能我们需要编译和链接的文件不止一个,比如以下情况:
那我们需要像下面这样进行gcc操作:
gcc main.c pid.c math.c -o main.o
有的时候,可能还不仅仅存在源文件,比如:
gcc pid.c pid.h -o pid.o
gcc pid.o main.c math.c -o main.o
在实际工程中,我们面临的环境比平常练习的情况可要复杂的多,单纯地使用gcc指令显得较为繁琐,尤其是当工程量级较大时,gcc指令更是尤为鸡肋,对于工程的维护和更新百害而无一利,比如以下情况:
当然上图这种情况依旧是较为乐观的,但是已经可以看出来gcc指令的不便之处了,而Makefile正是为了应对这些情况而存在的,可以认为Makefile是gcc的plus版本,但是其本身又处处离不开gcc的支持。
百度上对于Makefile的解释是:根据文件间的依赖关系来描述整个工程所有文件的编译顺序和编译规则。事实上,暂时不必深究这些专用术语,随着学习的深入,你将慢慢理解这些话想要表述的意思
Tips:C/C++编译执行过程
C语言 .c --》 .exe
预处理 : 把.c .h 文件展开形成一个文件,宏定义直接替换成头文件,生成库文件 .i 文件
gcc -e hello.c -o hello.i
汇编 : 把.i文件生成一个汇编文件 .s
gcc -s hello.i -o hello.s
编译 : 把.s文件生成一个.o .obj文件
gcc -c hello.s -o hello.o
链接 :把.o链接成一个.exe文件
gcc hello.o -o hello
如何创建Makefile(基于Linux虚拟机Ubuntu操作系统环境下)
若安装了vim,可以直接用以下命令创建Makefile
vim makefile
#若未安装vim,可以采用以下命令安装vim之后操作
sudo apt-get install vim
vim --version #若出现相关版本信息则安装成功
vim --help #建议在编辑makefile时通过此命令查看相关帮助,防止无法退出vim hahah>_< !!!
也可以使用nano命令
nano makefile
tips:在生成makefile时命名不一定是makefile,也有可能是Makefile等等,
不同的操作系统不尽相同,具体还得动手尝试,当然就算是阿猫阿狗也没有关
系,可以采用以下命令使用
make -f cat/dog/donkey……(whatever)
#具体原因请自行执行以下命令查看
make --help
浅说显式规则
所谓显式规则,可以理解为以下格式:
TARGET:OBJECT
[TAB] 指令
例如:
main.o:main.c
gcc -c main.c -o main.o
(其中的[TAB]指的是退格键,如果缺失了退格键或者存在其他字符将造成执行错误)
以上Makefile与gcc main.c -o main.o的作用是相同的,采用这个例子的目的既是因为它浅显易懂,同时也是为了便于讲解Makefile的规则问题。
·TARGET:目标文件,指的是通过指令最后要生成的文件
·OBJECT:依赖文件,指的是指令执行的依据和对象
·[TAB]:格式要求
·指令:负责依据依赖文件生成目标文件的操作
接下来我们将上述四个部分串联起来进行解释:
以上面的Makefile为例子,对于Makefile来说,每一次make命令的执行,首先检查依赖文件是否存在,若存在,则继续检查依赖文件的时间戳,如果依赖文件的时间戳发生了变化,那么就说明文件进行了更新,需要重新编译和链接,生成新的目标文件,若不存在,则说明要生成依赖文件来完成目标文件的更新,这就是所谓指令执行的依据,当这个依据成立,那么就会执行当前依据下的指令,即gcc -c main.c -o main.o,由于一般情况下,依据是与指令相关联,即main发生了变化,那就对main进行操作,阿猫阿狗发生了变化,那就对阿猫阿狗进行操作,这也就是所谓的指令执行的对象,而[TAB]之后跟随的指令就是检测到依赖后需要执行的内容。那么到此,我们可以重新捋一遍整体思路,如下图:
什么是递归式执行?
main.o:pid.o math.o gpio.o
gcc pid.o math.o gpio.o -o main.o
pid.o:pid.c pid.h
gcc pid.c pid.h -o pid.o
math.o:math.c math.h
gcc math.c math.h -o math.o
gpio.o:gpio.c gpio.h
gcc gpio.c gpio.h -o gpio.o
首先我们将所有的依赖文件touch一下,此时所有依赖文件的最近更新时间都将被改变,再把所有的.o后缀文件删除,接着我们从Makefile的第一行开始分析:
首先我们检测到main.o,继而检测依赖文件pid.o、math.o、gpio.o,由于我们提前删除了所有的.o文件,系统将检测不到依赖文件的存在,于是系统会想办法生成这些依赖文件,那么pid.o、math.o、gpio.o紧接着成为了新的目标文件,由于所有的依赖文件都被我们touch过了,所以下面的六行指令将逐一执行,mian.o所依赖的.o文件得以生成,满足指令执行的条件,就会通过相应的指令生成main.o啦,当然,这只是一个简单的二层关系,这样的层数可以不停向下叠加,实际层数由工程本身决定。
上述的这种关系和递归的思想特别相近,因为要求解f(x),所以要去寻找f(x-1),因为要求解f(x-1),所以要去寻找f(x-2),像这样逐层递进,直到最后找到一个确切的答案,即f(0) = 1,然后再逐层回归,求解出f(x),所以Makefile的执行方式,又被称为递归式执行。
目标唯一性
其实在大多数的帖子和博客并没有提及过目标唯一性这个词,事实上这也只是笔者凭着一厢情愿胡诌出来的一个特性,大多数人在讨论Makefile时都习惯将这个特性将递归式执行结合起来,但是都不会将其明显地挑明,以至给笔者在初学时造成了一些困扰,现在我们将之前的代码进行些许改变:
pid.o:pid.c pid.h
gcc pid.c pid.h -o pid.o
main.o:pid.o math.o gpio.o
gcc pid.o math.o gpio.o -o main.o
math.o:math.c math.h
gcc math.c math.h -o math.o
gpio.o:gpio.c gpio.h
gcc gpio.c gpio.h -o gpio.o
仅仅是将前面两组语句的顺序进行调换,Makefile的执行结果就截然不同,前者将会生成我们想要的main.o,但是后者只会生成pid.o,且不会再生成math.o和gpio.o,事实上,只要main.o这一组语句没有放在开头,我们都将得不到main.o,而只是会得到pid.o,math.o,gpio.o等这些中间文件,那么,这个特性就很明显了,就是每个Makefile实际上只存在一个目标文件,其他的目标文件只是作为这个首要目标与其他依赖文件之间的中间文件,也就是递归执行时的f(x-1)……f(x-n)。而Makefile默认将文件开头的第一个目标视作最终目标,与最终目标无关的目标将统统得不到执行,所以,在撰写Makefile时,理清楚文件之间的依赖关系,确定好编译规则和编译顺序是至关重要的。
简单说说变量
变量:
= (替换) += (追加) :=(恒等于)
TAR = test
TAR += test1
TAR := test
TARGET = test
OBJ = circle.o main.o
OBJ += cube.o
CC := gcc
test : circle.o main.o cube.o
gcc circle.o main.o cube.o
#可以被替换成下面的样子
$(TAR): $(OBJ)
$(CC) -c $(OBJ) -o $(TAR)
通过上面这些例子,我们其实可以看出来,变量最大的作用就在于代码管理,我们可以无须一个个地对.c和.h文件进行书写,而是可以用一个简短的变量代替,这对于代码修改是极为有利和便捷的。$(NAME)是变量引用的标准格式,在对Makefile进行解释时,会将对应的变量展开成其所代表的实际文件名。
简单说说通配符
通配符的作用在某种程度上与变量无异,但是其灵活性和便捷性又可以堪称秒杀变量,浅举几个小例子:
# %.c %.o 任意的.c .o *.c *.o 所有的.c .o
TAR = test
OBJ = circle.o main.o cube.o
CC := gcc
test = circle.o main.o cube.o
gcc circle.o main.o cube.o
%.o:%.c
$(CC) -c %.c -o %.o #无论TAR里面存在多少的.c文件,都可以转换成.o文件,仅需改变TAR里所包含的依赖文件即可
#可以被替换成下面的样子
$(TAR): $(OBJ)
$(CC) -c $(OBJ) -o $(TAR)
# 通配符:$^所有的依赖文件 $@所有的目标文件 $<所有的依赖文件的第一个文件
TAR = test
OBJ = circle.o main.o cube.o
CC := gcc
test = circle.o main.o cube.o
gcc circle.o main.o cube.o
%.o:%.c
$(CC) -c %.c -o %.o #无论TAR里面存在多少的.c文件,都可以转换成.o文件,仅需改变TAR里所包含的依赖文件即可
#可以被替换成下面的样子
$(TAR): $(OBJ)
$(CC) -c $^ -o $@
浅说伪命令
伪命令其实在本质上和其他的命令并没有太大差别,只是其不生成真实的目标,也并不存在任何依赖文件,只需要在每次执行make命令时进行调用,就会执行相应的指令,如下:
TAR = test
OBJ = circle.o main.o cube.o
CC := gcc
test = circle.o main.o cube.o
gcc circle.o main.o cube.o
%.o:%.c
$(CC) -c %.c -o %.o
$(TAR): $(OBJ)
$(CC) -c $(OBJ) -o $(TAR)
.PHONY:clean:
rm *.o
#可以借助此命令删除产生的.o文件,避免影响下次编译(残余的.o文件会使得编译报错)
只需执行以下命令,即可使伪命令生效
make clean
目前题主所用的伪命令即为上面的clean,用于清除每一次编译产生的中间文件,尤其是.o文件,上面已经提过,目标文件的更新取决于依赖文件的存在与否与时间戳,如果.o文件不能得到及时删除,那么每一次make都将不会更新以.o为依赖文件的目标文件(这里可以反复多思考几遍,加深印象),同时伪命令要注意其命名不可于当前路径下的文件重名,具体原因请读者自行探究,最好能动手实践,这对于Makefile的理解将会大有帮助。
其他
关于Makefile,关键字、路径包含、多文件路径的Makefile编写等其他进阶内容,日后若有时间也将继续加以说明,若想深入了解Makefile,特别推荐大家去看陈皓老师的系列博客”跟我一起写Makefile“,为数不多的对于Makefile讲解很深的文章!
最后,特别感谢:感谢Siya同志对该博客的修订工作提供的宝贵意见 >_<
更多推荐
所有评论(0)