[关闭]
@lishuhuakai 2016-11-04T21:25:33.000000Z 字数 6935 阅读 8597

深入学习Make命令和Makefile(上)

makefile


makeLinux下的一款程序自动维护工具,配合makefile的使用,就能够根据程序中模块的修改情况,自动判断应该对那些模块重新编译,从而保证软件是由最新的模块构成。本文分为上下两部分,我们将紧紧围绕make在软件开发中的应用展开详细的介绍。


一、都是源文件太多惹得祸

当我们在开发的程序中涉及众多源文件时,常常会引起一些问题。首先,如果程序只有两三个源文件,那么修改代码后直接重新编译全部源文件就行了,但是如果程序的源文件较多,这种简单的处理方式就有问题了。

设想一下,如果我们只修改了一个源文件,却要重新编译所有源文件,那么这显然是在浪费时间。其次,要是只重新编译那些受影响的文件的话,我们又该如何确定这些文件呢?比如我们使用了多个头文件,那么它们会被包含在各个源文件中,修改了某些头文件后,那些源文件受影响,哪些与此无关呢?如果采取拉网式大检查的话,可就费劲了。

由此可以看出,源文件多了可真是件让人头疼的事。幸运的是,实用程序make可以帮我们解决这两个问题——当程序的源文件改变后,它能保证所有受影响的文件都将重新编译,而不受影响的文件则不予编译,这真是太好了。

二、Make程序的命令行选项和参数

我们知道,make程序能够根据程序中各模块的修改情况,自动判断应对哪些模块重新编译,保证软件是由最新的模块构建的。至于检查哪些模块,以及如何构建软件由makefile文件来决定。

虽然make可以在makefile中进行配置,除此之外我们还可以利用make程序的命令行选项对它进行即时配置。Make命令参数的典型序列如下所示:

  1. make [-f makefile文件名][选项][宏定义][目标]

这里用[]括起来的表示是可选的。命令行选项由破折号“–”指明,后面跟选项,如:

  1. make e

如果需要多个选项,可以只使用一个破折号,如

  1. make kr

也可以每个选项使用一个破折号,如

  1. make k r

甚至混合使用也行,如

  1. make e kr

Make命令本身的命令行选项较多,这里只介绍在开发程序时最为常用的三个,它们是:
–k:
如果使用该选项,即使make程序遇到错误也会继续向下运行;如果没有该选项,在遇到第一个错误时make程序马上就会停止,那么后面的错误情况就不得而知了。我们可以利用这个选项来查出所有有编译问题的源文件。

–n:
该选项使make程序进入非执行模式,也就是说将原来应该执行的命令输出,而不是执行。

–f :
指定作为makefile的文件的名称。 如果不用该选项,那么make程序首先在当前目录查找名为makefile的文件,如果没有找到,它就会转而查找名为Makefile的文件。如果您在Linux下使用GNU Make的话,它会首先查找GNUmakefile,之后再搜索makefile和Makefile。按照惯例,许多Linux程序员使用Makefile,因为这样能使Makefile出现在目录中所有以小写字母命名的文件的前面。所以,最好不要使用GNUmakefile这一名称,因为它只适用于make程序的GNU版本。

当我们想构建指定目标的时候,比如要生成某个可执行文件,那么就可以在make命令行中给出该目标的名称;如果命令行中没有给出目标的话,make命令会设法构建makefile中的第一个目标。我们可以利用这一特点,将all作为makefile中的第一个目标,然后将让目标作为all所依赖的目标,这样,当命令行中没有给出目标时,也能确保它会被构建。

三、Makefile概述

上面提到,make命令对于构建具有多个源文件的程序有很大的帮助。事实上,只有make命令还是不够的,前面说过还必用须makefile告诉它要做什么以及怎么做才行,对于程序开发而言,就是告诉make命令应用程序的组织情况。

我们现在对makefile的位置和数量简单说一下。一般情况下,makefile会跟项目的源文件放在同一个目录中。另外,系统中可以有多个makefile,一般说来一个项目使用一个makefile就可以了;如果项目很大的话,我们就可以考虑将它分成较小的部分,然后用不同的makefile来管理项目的不同部分。

make命令和Makefile配合使用,能给我们的项目管理带来极大的便利,除了用于管理源代码的编译之外,还用于建立手册页,同时还能将应用程序安装到指定的目录。

因为Makefile用于描述系统中模块之间的相互依赖关系,以及产生目标文件所要执行的命令,所以,一个makefile由依赖关系和规则两部分内容组成。下面分别加以解释。

依赖关系由一个目标和一组该目标所依赖的源文件组成。这里所说的目标就是将要创建或更新的文件,最常见的是可执行文件。规则用来说明怎样使用所依赖得文件来建立目标文件。

make命令运行时,会读取makefile来确定要建立的目标文件或其他文件,然后对源文件的日期和时间进行比较,从而决定使用那些规则来创建目标文件。一般情况下,在建立起最终的目标文件之前,肯定免不了要建立一些中间性质的目标文件。这时,Make命令也是使用makefile来确定这些目标文件的创建顺序,以及用于它们的规则序列。

四、makefile中的依赖关系

make程序自动生成和维护通常是可执行模块或应用程序的目标,目标的状态取决于它所依赖的那些模块的状态。Make的思想是为每一块模块都设置一个时间标记,然后根据时间标记和依赖关系来决定哪一些文件需要更新。一旦依赖模块的状态改变了,make就会根据时间标记的新旧执行预先定义的一组命令来生成新的目标。

依赖关系规定了最终得到的应用程序跟生成它的各个源文件之间的关系。如下面的图1描述了可执行文件main对所有的源程序文件及其编译产生的目标文件之间的依赖关系,见下图:
此处输入图片的描述

就图1而言,我们可以说可执行程序main依赖于main.of1.off1.o。与此同时,main.o依赖于main.cdef1.hf1.o依赖于f1.cdef1.hdef2.h;而ff1.o则依赖于ff1.cdef2.hdef3. h。在makefile中,我们可以用目标名称,加冒号,后跟空格键或tab键,再加上由空格键或tab键分隔的一组用于生产目标模块的文件来描述模块之间的依赖关系。对于上例来说,可以作以下描述:

  1. main: main.o f1.o f2.o
  2. main.o: main.c def1.h
  3. f1.o: f1.c def1.h def2.h
  4. f2.o: f2.c def2.h def3.h

不难发现,上面的各个源文件跟各模块之间的关系具有一个明显的层次结构,如果def2.h发生了变化,那么就需要更新f1.of2.o,而f1.of2.o发生了变化的话,那么main也需要随之重新构建。

默认时,make程序只更新makefile中的第一个目标,如果希望更新多个目标文件的话,可以使用一个特殊的目标all,假如我们想在一个makefile中更新mainhello`这两个程序文件的话,可以加入下列语句达到这个目的:

  1. all: main hello

五、makefile中的规则

除了指明目标和模块之间的依赖关系之外,makefile还要规定相应的规则来描述如何生成目标,或者说使用哪些命令来根据依赖模块产生目标。就上例而言,当make程序发现需要重新构建f1.o的时候,该使用哪些命令来完成呢?很遗憾,到目前为止,虽然make知道哪些文件需要更新,但是却不知道如何进行更新,因为我们还没有告诉它相应的命令。

当然,我们可以使用命令gcc -c f1.c来完成,不过如果我们需要规定一个include目录,或者为将来的调试准备符号信息的话,该怎么办呢?所有这些,都需要在makefile中用相应规则显式地指出。

实际上,makefile是以相关行为基本单位的,相关行用来描述目标、模块及规则(即命令行)三者之间的关系。一个相关行格式通常为:冒号左边是目标(模块)名;冒号右边是目标所依赖的模块名;紧跟着的规则(即命令行)是由依赖模块产生目标所使用的命令。相关行的格式为:

  1. 目标:[依赖模块][;命令]

习惯上写成多行形式,如下所示:

  1. 目标:[依赖模块]
  2. 命令
  3. 命令

需要注意的是,如果相关行写成一行,“命令”之前用分号“;”隔开,如果分成多行书写的话,后续的行务必以tab字符为先导。对于makefile而言,空格字符和tab字符是不同的。所有规则所在的行必须以tab键开头,而不是空格键。初学者一定对此保持警惕,因为这是新手最容易疏忽的地方,因为几个空格键跟一个tab键在肉眼是看不出区别的,但make命令却能明察秋毫。

此外,如果在makefile文件中的行尾加上空格键的话,也会导致make命令运行失败。所以,大家一定要小心了,免得耽误许多时间。

六、Makefile文件举例

根据图1的依赖关系,这里给出了一个完整的makefile文件,这个例子很简单,由四个相关行组成,我们将其命名为mymakefile1。文件内容如下所示:

  1. main: main.o f1.o f2.o
  2. gcc -o main main.o f1.o f2.o
  3. main.o: main.c def1.h
  4. gcc -c main.c
  5. f1.o: f1.c def1.h def2.h
  6. gcc -c f1.c
  7. f2.o: f2.c def2.h def3.h
  8. gcc -c f2.c

注意,由于我们这里没有使用缺省名makefile 或者Makefile ,所以一定要在make命令行中加上-f选项。如果在没有任何源码的目录下执行命令make -f Mymakefile1的话,将收到下面的消息:

  1. make: *** No rule to make target main.c’, needed by main.o’. Stop.

Make命令将makefile中的第一个目标即main作为要构建的文件,所以它会寻找构建该文件所需要的其他模块,并判断出必须使用一个称为main.c的文件。因为迄今尚未建立该文件,而makefile又不知道如何建立它,所以只好报告错误。好了,现在建立这个源文件,为简单起见,我们让头文件为空,创建头文件的具体命令如下:

  1. $ touch def1.h
  2. $ touch def2.h
  3. $ touch def3.h

我们将main函数放在main.c文件中,让它调用function2function3,但将这两个函数的定义放在另外两个源文件中。由于这些源文件含有#include命令,所以它们肯定依赖于所包含的头文件。如下所示:

  1. /* main.c */
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include def1.h
  5. extern void function2();
  6. extern void function3();
  7. int main()
  8. {
  9. function2();
  10. function3();
  11. exit (EXIT_SUCCESS);
  12. }
  13. /* f1.c */
  14. #include def1.h
  15. #include def2.h
  16. void function2() {
  17. }
  18. /* f2.c */
  19. #include def2.h
  20. #include def3.h
  21. void function3()

建好源代码后,再次运行make程序,看看情况如何:

  1. $ make -f Mymakefile1
  2. gcc -c main.c
  3. gcc -c f1.c
  4. gcc -c f2.c
  5. gcc -o main main.o f1.o f2.o
  6. $

好了,这次顺利通过了。这说明Make命令已经正确处理了makefile描述的依赖关系,并确定出了需要建立哪些文件,以及它们的建立顺序。虽然我们在makefile 中首先列出的是如何建立main,但是make还是能够正确的判断出这些文件的处理顺序,并按相应的顺序调用规则部分规定的相应命令来创建这些文件。当这些命令执行时,make程序会按照执行情况来显示这些命令。

如今,我们对def2.h加以变动,来看看makefile能否对此作出相应的回应:

  1. $ touch def2.h
  2. $ make -f Mymakefile1
  3. gcc -c f1.c
  4. gcc -c f2.c
  5. gcc -o main main.o f1.o f2.o
  6. $

这说明,当Make命令读取makefile 后,只对受def2.h的变化的影响的模块进行了必要的更新,注意它的更新顺序,它先编译了C程序,最后连接生产了可执行文件。现在,让我们来看看删除目标文件后会发生什么情况,先执行删除,命令如下:

  1. $ rm f1.o

然后运行make命令,如下所示:

  1. $ make -f Mymakefile1
  2. gcc -c f1.c
  3. gcc -o main main.o f1.o f2.o
  4. $

很好,make的行为让我们非常满意。

七、makefile中的宏

makefile中可以使用诸如XLIBUIL等类似于Shell变量的标识符,这些标识符在makefile中称为“宏”,它可以代表一些文件名或选项。宏的作用类似于C语言中的define,利用它们来代表某些多处使用而又可能发生变化的内容,可以节省重复修改的工作,还可以避免遗漏。

Make的宏分为两类,一类是用户自己定义的宏,一类是系统内部定义的宏。用户定义的宏必须在makefile或命令行中明确定义,系统定义的宏不由用户定义。我们首先介绍第一种宏。

这里是一个包含宏的makefile文件,我们将其命名为mymakefile2,如下所示:

  1. all: main
  2. # 使用的编译器
  3. CC = gcc
  4. #包含文件所在目录
  5. INCLUDE = .
  6. # 在开发过程中使用的选项
  7. CFLAGS = -g -Wall ansi
  8. # 在发行时使用的选项
  9. # CFLAGS = -O -Wall –ansi
  10. main: main.o f1.o f2.o
  11. $(CC) -o main main.o f1.o f2.o
  12. main.o: main.c def1.h
  13. $(CC) -I$(INCLUDE) $(CFLAGS) -c main.c
  14. f1.o: f1.c def1.h def2.h
  15. $(CC) -I$(INCLUDE) $(CFLAGS) -c f1.c
  16. f2.o: f2.c def2.h def3.h
  17. $(CC) -I$(INCLUDE) $(CFLAGS) -c f2.c

我们看到,在这里有一些注释。在makefile中,注释以#为开头,至行尾结束。注释不仅可以帮助别人理解我们的makefile,如果时间久了,有些东西我们自己也会忘掉,它们对makefile的编写者来说也是很有必要的。

现在言归正传,先看一下宏的定义。我们既可以在make命令行中定义宏,也可以在makefile中定义宏。在makefile中定义宏的基本语法是:

  1. 宏标识符=值列表

其中,宏标识符即宏的名称通常全部大写,但它实际上可以由大、小写字母、阿拉伯数字和下划线构成。等号左右的空白符没有严格要求,因为它们最终将被make删除。至于值列表,既可以是零项,也可以是一项或者多项。如:

  1. LIST_VALUE = one two three

当一个宏定义之后,我们就可以通过$(宏标识符)或者${宏标识符}来访问这个标识符所代表的值了。

makefile中,宏经常用作编译器的选项。很多时候,处于开发阶段的应用程序在编译时是不用优化的,但是却需要调试信息;而正式版本的应用程序却正好相反,没有调试信息的代码不仅所占内存较小,进过优化的代码运行起来也更快。

对于Mymakefile1来说,它假定所用的编译器是gcc,不过在其他的UNIX系统上,更常用的编译器是cc或者c89,而非gcc。如果你想让自己的makefile适用于不同的UNIX操作系统,或者在一个系统上使用其他种类的编译器,这时就不得不对这个makefile中的多处进行修改。

但对于mymakefile2来说则不存在这个问题,我们只需修改一处,即宏定义的值就行了。除了在makefile中定义宏的值之外,我们还可以在make命令行中加以定义,如:

  1. $ make CC=c89

这样就不必担心空格所引起的问题了。现在让我们将前面的编译结果删掉,来测试一下mymakefile2的工作情况。命令如下所示:

  1. $ rm *.o main
  2. $ make -f Mymakefile2
  3. gcc -I. -g -Wall -ansi -c main.c
  4. gcc -I. -g -Wall -ansi -c f1.c
  5. gcc -I. -g -Wall -ansi -c f2.c
  6. gcc -o main main.o f1.o f2.o
  7. $

就像我们看到的那样,Make程序会用相应的定义来替换宏引用$(CC )$(CFLAGS )$(INCLUDE),这跟C语言中的宏的用法比较相似。

上面介绍了用户定义的宏,现在介绍make的内部宏。常用的内部宏有:

  1. $? :比目标的修改时间更晚的那些依赖模块表。
  2. $@ :当前目标的全路径名。可用于用户定义的目标名的相关行中。
  3. $< :比给定的目标文件时间标记更新的依赖文件名。
  4. $* :去掉后缀的当前目标名。例如,若当前目标是pro.o,则$*表示pro

八、小结

我们在本文中分别介绍了make程序的使用方法,makefile中的依赖关系及规则等基础知识,同时还介绍了一些常用的宏。在下篇文章中,我们会对makefile的高级功能做进一步的介绍。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注