X86/Debian GNU/Linux/gcc。《Linux C编程一站式学习》PartII练习之一,对我陌生信息较多,摘抄较多。个人觉得编写Makefile时涉及蓝色标题较多,红色标题能够帮助我们省去部分体力或者眼力劳动。
1 Makefile的出现
以目前功力示例一个由三个文件组成的简单的程序:
/* Filename: main.h
* Brife: Define some data
* Author: One fish
* Date: 2014.8.4 Monday
*/
#ifndef MAIN_H
#define MAIN_H
#define MAX_GIRLS 2
#define MAX_BOYS 2
#endif
/* Filename: main.c
* Brife: Supporting to wirte makefile file
* Author: One fish
* Date: 2014.8.4 Monday
*/
#include <stdio.h>
#include "girls.h"
#include "boys.h"
int main(void)
{
int girl_num = 0;
int boy_num = 0;
girl_num = girls_number( 1 );
boy_num = boys_number( 2 );
printf("girl's number is: %d, boy's number is:%d\n", girl_num, boy_num);
return 0;
}
/* Filename: girls.h
* Brife: Declare functions
* Author: One fish
* Date: 2014.8.4 Monday
*/
#ifndef GIRLS_H
#define GIRLS_H
#include "main.h"
extern int girls_number(int n);
#endif
/* Filename: girls.c
* Brife: Define functions
* Author: One fish
* Date: 2014.8.4 Monday
*/
#include <stdio.h>
#include "girls.h"
int girls_number(int n)
{
return MAX_GIRLS - n;
}
/* Filename: Boys.h
* Brife: Declare boys.c's function
* Author: One fish
* Date: 2014.8.4 Monday
*/
#ifndef BOYS_H
#define BOYS_H
#include "main.h"
extern int boys_number(int n);
#endif
/* Filename: Boys.c
* Brife: Define function
* Author: One fish
* Date: 2014.8.4 Monday
*/
#include "boys.h"
int boys_number(int n)
{
return MAX_BOYS - n;
}
所有的文件都在同一目录下。对以上这段程序可以用gcc来完成程序的编译与链接:
gcc main.c girls.c boys.c main |
只要其中有一个文件被修改时,就要把所有的源文件重新编译一遍(一方面,在源文件较多时,VS或者MDK-Keil中全部重新编译和只重新编译修改部分的时间差异还是有的)。可以用gcc分别编译各个文件,然后再用gcc来链接:
gcc -c main.c gcc -c girls.c gcc -c boys.c gcc main.o girls.o boys.o |
这样,如果只有其中一个文件被修改(如boys.c),重新编译时只需要:
gcc -c boys.c gcc main.o girls.o boys.o |
2 Makefile编写规则基础
一般的程序都是由多个源文件编译链接而成的。这些源文件的处理步骤通常用Makefile来管理。
(1) Makefile组成
Makefile由一组规则组成,每条规则的格式是:
target … : prerequisite … comand1 comand2 …… |
如:
girls.o:girls.c girls.h main.h |
girls.o是这条规则的目标(target),girls.c 、girls.h、main.h是这条规则的条件(prerequisite)。目标和条件的关系是:欲更新目标,必须首先更新条件;所有条件中只有一个条件被更新了,目标也必须随之被更新。所谓“更新”就是执行一遍规则中的命令列表(command),命令列表中的每条命令必须以一个Tab开头,且不能是空格,对于Makefile中的每个以Tab开头的命令,make会创建一个Shell进程去执行它。
(2) 编写一个Makefile
按照Makefile组成,对1中的程序编写一个Makefile:
main: main.o girls.o boys.o gcc main.o girls.o boys.o -o main main.o: main.c girls.h boys.h gcc -c main.c girls.o: girls.c girls.h main.h gcc -c girls.c boys.o boys.c boys.h main.h gcc -c boys.c |
然后在与源程序同一个目录下运行make编译:
lly@debian:~/mydir/lly_books/linux_c_programming_osl/makefile$make gcc -c main.c gcc -c girls.c gcc -c boys.c gcc main.o girls.o boys.o -o main |
(3) Makefile的约定俗成(clean)规则
Makefile的clean规则用来清除编译过程产生的二进制文件,保留源文件。
clean: -rm main *.o |
如果make执行的命令前面加了@字符,则不显示命令本身就显示命令对应的结果。通常make执行的命令如果出错就立刻终止,不再执行后续命令,但如果命令前加了-号,即使这条命令出错,make也会继续执行后续命令。
如果在make的命令中指定一个目标,则更新这个目标。如果不指定目标则更新Makefile中第一条规则的目标。如果要想更新clean这个目标得运行make clean。如果存在clean这个条件,而clean这个目标又不依赖任何条件,make就认为它不需要更新了。可以将clean声明为一个伪目标:.PHONY: clean。这样,不管clean存在更新与否都将更新clean这个目标。
clean是一个约定俗成的名字,在Makefile中类似这样的约定俗成的名字有:
- all,执行主要的编译工作,通常用作缺省目标。
- install,执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷到不同的安装目录。
- clean,删除生成的二进制文件。
- distclean,不仅删除编译生成的二进制文件,也删除其它生成的文件,例如配置文件和格式转换后的文档,执行make distclean之后应该清除所有这些文件,只留下源文件。
(4) Makefile中的隐含规则和模式规则
如果一个目标在Makefile中的所有规则中没有命令列表,make会尝试在内建的隐含规则数据库中查找使用的规则。make的隐含规则数据库可以用make -p打印,打印出来的格式也是Makefile,包括很多变量和规则。
那么先前的Makefile可以写成:
main: main.o girls.o boys.o gcc main.o girls.o boys.o -o main main.o: girls.h boys.h girls.o: girls.h main.h boys.o boys.h main.h .PHONY: clean clean: -rm main *.o |
执行以下make clean后,在同一个目录下运行make编译:
lly@debian:~/mydir/lly_books/linux_c_programming_osl/makefile$make CC -c -o main.o main.c CC -c -o girls.o girls.c CC -c -o boys.o boys.c gcc main.o girls.o boys.o -o main |
跟以上例子相关的隐含规则有:
#default OUTPUT_OPTION = -o $@ #default CC = cc #default COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c %.o: %c #commands to execute (built-in) $(COMPLE.c) $(OUTPUT_OPTION) $< |
- #在Makefile中表示单行注释。
- $@和$<是两个特殊的变量,$@的取值为规则中的目标,$<的取值为规则中的第一个条件。
- Makefile变量像C的宏定义一样,代表一串字符。在取值的地方展开。CC是一个Makefile变量,用$(CC)取它的值。
- cc是一个符号链接,通常指向gcc。
- CFLAGS这个变量没有定义,$(CFLAGS)展开是空,CPPFLAGS和TARGET_ARCH也是如此。
- %.o:%.c是一种特殊的规则,称为模式规则。
(5) 变量
变量可推迟定义:
foo = $(bar) bar = bar all: @echo $(foo) |
执行make则输出bar。Makefile变量在取值处展开(如想变量及时展开则用:=。如果变量没有定义则?=表示=,如果变量已经定义赋值则?=什么也不做。+=给变量追加定义(但不立即展开))的特性可以让我们把变量的值推迟到后面定义。如此就可以这样写一个Makefile:
main.o: main.c $(CC) $(CFLAGS) $(CPPFLAGS) -c $< CC = gcc CFLAGS = -o -g CPPFLAGS= -Iinclude |
编译命令可以展开生成gcc -o -g -Iinclude -c main.o。通常把CFLAGS定义成一些编译选项,而把CPPFLAGS定义成一些预处理选项。Makefile的变量含义如下:
- $?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。
- $^,表示规则中的所有条件,组成一个列表,以空格分隔。
- $@和$<见(4)。
- AR,静态库打包命令的名字,缺省值是ar。
- ARFLAGS,静态库打包命令的选项,缺省值是rv。
- AS,汇编器的名字。缺省值是as。
- ASFLAGS,汇编器的选项,没有定义。
- CC,C编译器的名字,缺省值是cc。
- CFLAGS,C编译器的选项,没有定义。
- CXX,C++编译器的选项,缺省值是g++。
- CXXFLAGS,C++编译器的选项,没有定义。
- CPP,C预处理器的名字,缺省值是$(CC) –E。
- CPPFLAGS,C预处理器的选项,没有定义。
- LD,连接器的名字,缺省值是ld。
- LDFLAGS,连接器的选项,没有定义。
- TARGET_ARCH,和目标平台相关的命令行选项,没有定义。
- OUTPUT_OPTION,输出命令选项,缺省值是-o $@。
- LINK.o,把.o文件链接在一起的命令行,缺省值是$(CC) $(LDFLAGS) $(TARGET_ARCH)。
- LINK.c,把.c文件链接在一起的命令行,缺省值是$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)。
- LINK.cc,把.cc文件(C++源文件)链接在一起的命令行,$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)。
- COMPILE.c,编译.c文件的命令行,缺省值是$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c。
- COMPILE.cc,编译.c文件的命令行,缺省值是$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c。
- RM,删除命令的名字,缺省值是rm -f。
个人觉得,此时可回看用变量写的Makefile或Makefile的隐含规则和模式规则,加深对Makefile的理解。
(6) 用Makefile自动处理头文件的依赖关系
在写某个目标依赖的条件时,往往需要到源码中查看信息。如main.o、girls.o、boys.o依赖的头文件,这中看源码的方式很容易出错,还有就是当为程序写好了Makefile,而源码改变了头文件依赖关系,需要重新写Makefile(还有可能忘记更新Makefile)。为了解决这个问题,可以在Makefile中用gcc -M选项自动生成目标文件和源文件的依赖关系。如果不需要输出与程序相关的系统头文件则用gcc -MM选项。
GNU官方手册建议这样写Makefile来自动处理头文件的依赖关系:
all: main
main: main.o girls.o boys.o gcc $^ -o $@
clean: -rm main *.o .PHONY: clean
sources = main.c girls.c boys.c #sources:.c=.d; include $(sources:.c=.d) %.d: %.c set-e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed's, \($*\)\.o[ :]*, \l.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$ |
首次用make编译运行的结果如下:
Makefile:12: main.d: No such file ordirectory Makefile:12: girls.d: No such fileor directory Makefile:12: boys.d: No such file ordirectory set -e; rm -f boys.d; \ cc -MM boys.c > boys.d.$$; \ sed's, \(boys\)\.o[ :]*, \l.o boys.d : ,g' < boys.d.$$ > boys.d; \ rm -f boys.d.$$ set -e; rm -f girls.d; \ cc -MM girls.c > girls.d.$$; \ sed's, \(girls\)\.o[ :]*, \l.o girls.d : ,g' < girls.d.$$ > girls.d; \ rm -f girls.d.$$ set -e; rm -f main.d; \ cc -MM main.c > main.d.$$; \ sed's, \(main\)\.o[ :]*, \l.o main.d : ,g' < main.d.$$ > main.d; \ rm -f main.d.$$ cc -c -o main.o main.c cc -c -o girls.o girls.c cc -c -o boys.o boys.c gcc main.o girls.o boys.o -o main |
sources变量包含我们要编译的所有.c文件,$(sources:.c=.d)是一个变量替换语法,把sources变量中的每一项的.c替换成.d。Makefile中的include(类似C的#inlude)表示包含三个文件main.d、girls.d和boys.d。第一次这个Makefile时,会有提示找不到*.d文件的警告,但make会把include的文件名也当作目标来尝试更新,且这些目标使用模式规则%.d:%.c,所以执行它的命令列表生成*.d文件。
用来生成*.d文件的命令执行过程为:
- set –e用来设置当前Shell进程为这样的状态,如果它执行任何一条命令的退出状态非零则立刻终止,不再执行后面命令。
- 把原来的*.d删掉。
- 重新生成*.c的依赖关系,保存文件*.d.xxxx(xxxx为当前shell进程id)。在Makefile中$有特殊含义,如果要表示它的字面意思则需要两个$。所以Makefile中传给Shell的是两个$,两个$来Shell中表示当前进程的id,一般用这个id给临时文件取名,避免文件名重复。
- 最后把临时文件*.d.xxxx删掉。
(7) 常用的make命令选项
-n
由于Makefile不是顺序执行的,用-n选项只打印要执行的命令而不会真的执行命令,这样可以看到命令的执行顺序,确认命令无误后再执行命令。
-C
一些规模较大的项目会把不同的模块或子系统的源代码放在不同的子目录中,然后在每个子目录下都写一个该目录的Makefile,然后在一个总的Makefile中用make -C命令执行每个每个子目录下的Makefile。
=和:=
在make命令行也可以用=或:=定义变量,如果这次编译我想加调试选项-g(Make CFLAGS = -g),但我不想每次编译都加-g选项,可以在命令行定义CFLAGS,而不必修改Makefile编译完了在改回来。
3 Makefile其它资源
《跟我一起写Makefile》---- 陈皓。[2014.8.3– 14:04 --- 2014.8.6-9:40]
BCNote Over.