在讨论Makefile文件时,首先需要了解以下的一些知识:
一、 C语言编译过程
C语言的编译过程可分为四个阶段:
1、预处理(Preprocessing)
对源程序中的伪指令(即以#开头的指令)和特殊符号进行处理的过程。
伪指令包括:1)宏定义指令;
2)条件编译指令;
3)头文件包含指令;
2、编译(Compilation)
编译就是将源程序转换为计算机可以执行的二进制代码。
说明:
在Linux下,目标文件的缺省后缀为.o
编译程序将通过词法分析和语法分析,将其翻译成为等价的汇编代码。
在使用gcc进行编译时,缺省情况下,不输出这个汇编代码的文件。如果需要,可以在编译时指定-S选项。这样,就会输出同名的汇编语言文件。
3、汇编(Assembly)
汇编的过程实际上是将汇编语言代码翻译成机器语言的过程。
产生一个扩展名为.o的目标文件。
4、链接(Linking)
目标代码不能直接执行,要想将目标代码变成可执行程序,还需要进行链接操作。才会生成真正可以执行的可执行程序。
链接操作最重要的步骤就是将函数库中相应的代码组合到目标文件中。
二、文件名后缀
gcc可以针对支持不同的源程序文件进行不同的处理,文件格式以文件的后缀来识别。
文件名后缀 | 文件类型 |
.c | C源文件 |
.C .cpp .cc .c++ .cxx | C++源文件 |
.h | 头文件 |
.i | 预处理后的C源文件 |
.s | 汇编程序文件 |
.o | 目标文件 |
.a | 静态链接库 |
.so | 动态链接库 |
三、 gcc编译器简介
gcc(GNU CompilerCollection)
在Linux平台上最常用的C语言编译系统是gcc,它是GNU项目中符合ANSI C标准的编译系统。
gcc的使用格式:
gcc [options][filenames]
说明:当不用任何选项时,gcc将会生成一个名为a.out的可执行文件。
例子:在linux上编译一个c程序(文件名为hello.c ;执行gcc hello.c)。
#include<stdio.h>
int main()
{
printf("helloworld.\n");
return0;
}
运行编译好的可执行c文件命令是./a.out
四、gcc编译器的工作过程
1、预处理(Preprocessing)
2、编译(Compilation &Assembly)
源代码转换为汇编语言(在编译时选择-S选项,可以看到生成的汇编代码.s文件)
汇编代码(.s)转换为目标代码(.o)
3、链接(Linking)
将目标代码与各库函数进行链接并重定位,生成可执行程序。
五、gcc命令行选项
1、预处理选项
选项 | 说明 |
-D name | 定义一个宏name,并可以指定值 |
-I dir | 指定头文件的路径dir。先在指定的路径中搜索要包含的头文件,若找不到,则在标准路径(/usr/include,/usr/lib及当前工作目录)上搜索。 |
-E | 只对文件进行预处理,不进行编译、汇编、链接,生成的结果送标准输出 即:只运行C预编译器 |
-o file | 将输出写到指定的文件file中 即:产生目标(.i 、.s 、 .o 、可执行文件等) |
例子:使用 -I选项包含保存在非标准位置中的头文件。
# gcc -I/usr/openwin/include file.c
例子:使用-D选项定义宏,其作用等价于在源文件中使用宏定义指令。
main()
{
printf("display-D variable %s\n",DOPTION);
printf("hello,everybody!!\n");
}
# gcc -D DOPTION='"testing -D"'hello.c
2、编译程序选项
选项 | 说明 |
-o file1 file2 | 将文件file2编译成可执行文件file1。 如果未使用该选项,则可执行文件放在a.out中 |
-S | 只进行编译,不进行汇编,生成汇编代码文件扩展名为.s 即:告诉编译器产生汇编语言文件后停止编译 |
-c | 只把源文件编译成目标代码.o,不进行汇编、链接。 用于实现对源文件的分别编译 |
-g | 在目标代码中加入供调试程序gdb使用的附加信息 |
-v | 显示gcc版本 |
-Wall | 显示警告信息 |
例子:在gcc中使用-W控制警告信息。
# gcc -Wall -o hello1 hello1.c
例子:使用gcc的-g选项来产生调试符号,
# gcc -g -o test1 test1.c
例子:多文件的编译。
//meng1.c
#include<stdio.h>
main()
{
intr;
printf("enteran integer,please!\n");
scanf("%d",&r);
square(r);
return0;
}
//meng2.c
#include
int square(int x)
{
printf("Thesquare=%d\n",x*x);
return(x*x);
}
编译方法一:
# gcc -c meng1.c
# gcc -c meng2.c
# gcc meng1.o meng2.o -o meng12
编译方法二:
# gcc -o meng13 meng1.c meng2.c
说明:
方法二不产生中间目标文件,直接生成一个可执行文件,因而,程序内容稍有改动,就要重新编译全部程序。
三、make是如何工作的
在默认的方式下,也就是我们只输入make命令。那么,
1、make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
3、如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。
4、如果edit所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
5、当然,你的C文件和H文件是存在的,于是make会生成 .o 文件,然后再用 .o 文件生命make的终极任务,也就是执行文件edit了。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。
于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如file.c,那么根据我们的依赖性,我们的目标file.o会被重编译(也就是在这个依性关系后面所定义的命令),于是file.o的文件也是最新的啦,于是file.o的文件修改时间要比edit要新,所以edit也会被重新链接了(详见edit目标文件后定义的命令)。
而如果我们改变了“command.h”,那么,kdb.o、command.o和files.o都会被重编译,并且,edit会被重链接。
四、makefile中使用变量
在上面的例子中,先让我们看看edit的规则:
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
我们可以看到[.o]文件的字符串被重复了两次,如果我们的工程需要加入一个新的[.o]文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。
比如,我们声明一个变量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正不管什么,只要能够表示obj文件就行了。我们在makefile一开始就这样定义:
objects = main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
于是,我们就可以很方便地在我们的makefile中以“$(objects)”的方式来使用这个变量了,于是我们的改良版makefile就变成下面这个样子:
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 文件加入,我们只需简单地修改一下 objects 变量就可以了。
关于变量更多的话题,我会在后续给你一一道来。
五、让make自动推导
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。
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)
这种方法,也就是make的“隐晦规则”。上面文件内容中,“.PHONY”表示,clean是个伪目标文件。
关于更为详细的“隐晦规则”和“伪目标文件”,我会在后续给你一一道来。
六、另类风格的makefile
即然我们的make可以自动推导命令,那么我看到那堆[.o]和[.h]的依赖就有点不爽,那么多的重复的[.h],能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。
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)
这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的.o文件,那就理不清楚了。
七、清空目标文件的规则
每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。。一般的风格都是:
clean:
rm edit $(objects)
更为稳健的做法是:
.PHONY : clean
clean :
-rm edit $(objects)
前面说过,.PHONY意思表示clean是一个“伪目标”,。而在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。
八、模式规则
模式规则类似于普通规则。只是在模式规则中,目标名中需要包含有模式字符“%”(一个),包含有模式字符“%”的目标被用来匹配一个文件名,“%”可以匹配任何非空字符串。规则的依赖文件中同样可以使用“%”,依赖文件中模式字符“%”的取值情况由目标中的“%”来决定。例如:对于模式规则“%.o : %.c”,它表示的含义是:所有的.o文件依赖于对应的.c文件。我们可以使用模式规则来定义隐含规则。
要注意的是:模式字符“%”的匹配和替换发生在规则中所有变量和函数引用展开之后,变量和函数的展开一般发生在make读取Makefile时(变量和函数的展开可参考第五 章 使用变量 和 第七章 make的函数),而模式规则中的“%”的匹配和替换则发生在make执行时。
在模式规则中,目标文件是一个带有模式字符“%”的文件,使用模式来匹配目标文件。文件名中的模式字符“%”可以匹配任何非空字符串,除模式字符以外的部分要求一致。例如:“%.c”匹配所有以“.c”结尾的文件(匹配的文件名长度最少为3个字母),“s%.c”匹配所有第一个字母为“s”,而且必须以“.c”结尾的文件,文件名长度最小为5个字符(模式字符“%”至少匹配一个字符)。在目标文件名中“%”匹配的部分称为“茎”(前面已经提到过,参考4.12 静态模式一节)。使用模式规则时,目标文件匹配之后得到“茎”,依赖根据“茎”产生对应的依赖文件,这个依赖文件必须是存在的或者可被创建的。
因此,一个模式规则的格式为:
%.o:%.c;<command>
这个模式规则指定了如何由文件“N.c”来创建文件“N.o”,文件“N.c”应该是已存在的或者可被创建的。
模式规则中依赖文件也可以不包含模式字符“%”。当依赖文件名中不包含模式字符“%”时,其含义是所有符合目标模式的目标文件都依赖于一个指定的文件(例如:%.o : debug.h,表示所有的.o文件都依赖于头文件“debug.h”)。这样的模式规则在很多场合是非常有用的。
同样一个模式规则可以存在多个目标。多目标的模式规则和普通多目标规则有些不同,普通多目标规则的处理是将每一个目标作为一个独立的规则来处理,所以多个目标就就对应多个独立的规则(这些规则各自有自己的命令行,各个规则的命令行可能相同)。但对于多目标模式规则来说,所有规则的目标共同拥有依赖文件和规则的命令行,当文件符合多个目标模式中的任何一个时,规则定义的命令就有可能将会执行;因为多个目标共同拥有规则的命令行,因此一次命令执行之后,规则不会再去检查是否需要重建符合其它模式的目标。看一个例子:
#sample Makefile
Objects = foo.obar.o
CFLAGS := -Wall
%x : CFLAGS += -g
%.o : CFLAGS +=-O2
%.o %.x : %.c
$(CC) $(CFLAGS)$< -o $@
当在命令行中执行“make foo.ofoo.x”时,会看到只有一个文件“foo.o”被创建了,同时make会提示“foo.x”文件是最新的(其实“foo.x”并没有被创建)。此过程表明了多目标的模式规则在make处理时是被作为一个整体来处理的。这是多目标模式规则和多目标的普通规则的区别之处。大家不妨将上边的例子改为普通多目标规则试试看将会得到什么样的结果。
最后需要说明的是:
1. 模式规则在Makefile中的顺序需要注意,当一个目标文件同时符合多个目标模式时,make将会把第一个目标匹配的模式规则作为重建它的规则。
2.Makefile中明确指定的模式规则会覆盖隐含模式规则。就是说如果在Makefile中出现了一个对目标文件合适可用的模式规则,那么make就不会再为这个目标文件寻找其它隐含规则,而直接使用在Makefile中出现的这个规则。在使用时,明确规则永远优先于隐含规则。
3. 另外,依赖文件存在或者被提及的规则,优先于那些需要使用隐含规则来创建其依赖文件的规则。