您的位置:首页 > 其它

我们一起学习Makefile

2012-11-23 14:18 381 查看
经过长时间学习和研究linux GNU
make工程管理器 ,现在把学习心得与大家分享一下,希望本文能教会您一些有用的东西。

make工具,是所有想在Linux/Unix系统上编程的用户都需要且必须掌握的工具。如果您写的程序没有用到make工具,则说明您写的程序仅仅是个人练习小程序,称不上有实用价值的程序,因此我们必须学习、掌握并灵活运用它。

在Linux/UNIX 系统中,习惯使用 Makefile或makfile 文件作为make命令目标文件。 Make工具最主要也是最基本的功能就是通过makefile文件来描述源程序之间的相互依赖关系并自动维护编译工作。而makefile
文件需要按照某种语法进行编写,文件中需要说明如何编译各个源文件并连接生成可执行文件,并要求定义源文件之间的依赖关系。

一、多文件编译的总体结构

如下图所示, 本示例 共包含 float类型加法、加法头函数、int类型加法、main主函数、float类型减法、减法头函数、int类型减法



主函数

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

#include "add.h"

#include "sub.h"

#include <stdio.h>

int main()

{

int x, y;

float a, b;

x=5;

y=2;

a=5.5;

b=2.2;

printf("%d + %d = %d/n", x, y, add_int(x, y));

printf("%3.1f + %3.1f = %3.1f/n", a, b, add_float(a, b));

printf("%d - %d = %d/n", x, y, sub_int(x, y));

printf("%3.1f - %3.1f = %3.1f/n", a, b, sub_float(a, b));

return 0;

}

加法头函数

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

/* add.h */

#ifndef _ADD_H_

#define _ADD_H_

extern int add_int(int x, int y);

extern float add_float(float x, float y);

#endif

int类型加法

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

/* add_int.c */

int add_int(int x, int y)

{

return x+y;

}

float类型加法

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

/* add_float.c */

float add_float(float x, float y)

{

return x+y;

}

~

减法头函数

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

/* sub.h */

#ifndef _SUB_H_

#define _SUB_H_

extern int sub_int(int x, int y);

extern float sub_float(float x, float y);

#endif

int类型减法

[cpp] view
plaincopyprint?

/* sub_int.c */

int sub_int(int x, int y)

{

return x-y;

}

float类型减法

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

/* sub_float.c */

float sub_float(float x, float y)

{

return x-y;

}

二、方法1
( 多文件编译 )

直接在Linux的Shell环境中,分别依次利用gcc -c *.c -o *.o命令进行编译,并链接生成可执行文件,具体做法如下:

依次编译上述文件:



编译后的结果文件:



然后,直接利用gcc -o main add_int.o add_float.o sub_int.o sub_float.o main.o命令,链接、编译成目标可执行文件main



最后,输入 ./main 运行结果



评析: 此方法遵照单文件编译方法,过程清晰、直观易懂;但效率很低,在编译文件数量很大或源文件修改时,此方法效率很低,且难以维护

三、方法
2 ( 多文件编译——使用makefile )

此方法为了避免方法1的不足,利用Linux GNU make工程管理器,进行编译、管理源文件。

首先,了解一下make和makefile。 GNU make是一个工程管理器,专门负责管理、维护较多文件的处理,实现自动化编译。如果一个工程项目中,有成百上千个代码源文件,若其中一个或多个文件进过修改,make就需要能够自动识别更新了的代码,不需要像方法1一样逐个输入编译冗长的命令行,就可以完成最后的编译工作。make执行时,自动寻找makefile(Makefile)文件,然后执行编译工作。因此,我们需要自己编写makefile文件(Makefile与makefile都可以直接被make命令识别,下同。但Linux区分大小写)来管理、维护工程文件,提高实际项目的工作效率。

其次,需要注意Linux makefile(Makefile)文件的编写规范和方法:

1、需要由make工具创建目标体target,即通常的目标文件或可执行文件

2、声明并给出创建的目标体所依赖的文件(dependency-file)

3、编写完成创建每个目标体时所需要执行的命令(command)

具体格式如下:

target: dependency-file1 dependency-file2 dependency-file3 ...

command

target:规划的目标。通常是程序中间体或最后所需要生成的文件名,如 *.o或obj可执行文件的名称。此外,target目标也可以是make执行动作的名称,如clean等

dependency-file:规则的依赖。生成规则目标所需要的文件名列表,通常是一个目标依赖于一个或多个文件。

command:规则的命令。make程序所执行的的动作,可以为shell命令或者在shell下执行的程序。一个规则可以有多条命令,每条命令占一行。 在此特别需要注意的是每条命令行开始必须以Tab字符缩进开始,Tab缩进字符会告诉make命令此行是一个命令行,make按照命令完成此行相应的动作。这是在书写makefile(Makefile)文件时最易忽视和犯错的地方,而且大多比较隐蔽。

命令实质上市对任何一个目标的依赖文件发生变化后重建目标的动作描述。一个目标可以没有依赖而只有动作,即只有命令,如clean。此目标只有命令,没有依赖,主要作用是用来删除make过程中产生的中间文件(*.o),做收尾清理工作。

最后,上面均是纸上谈兵,现在我们来看具体实例,以直观、具体、详尽的解释makefile文件的编写方法和规则。

方法1可以用如下makefile文件代替,makefile编写如下:

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

# target: dependency-file

main: main.o add_int.o add_float.o sub_int.o sub_float.o

# NOTE: Tab before command

gcc -o main main.o add_int.o add_float.o sub_int.o sub_float.o

main.o: main.c add.h sub.h

gcc -c main.c -o main.o

add_int.o: add_int.c add.h

gcc -c add_int.c -o add_int.o

add_float.o: add_float.c add.h

gcc -c add_float.c -o add_float.o

sub_int.o: sub_int.c sub.h

gcc -c sub_int.c -o sub_int.o

sub_float.o: sub_float.c sub.h

gcc -c sub_float.c -o sub_float.o

clean:

rm -f *.o main

说明:

#表示注释,其后的在编译预处理时,将被全部删除不执行

gcc -c 编译C语言源文件,编译生成目标文件 *.o

gcc -o 定义生成文件名称,可以为 *.o(目标文件)和 main(可执行文件)

rm -f *.o main 强制删去该目录下的所有*.o 目标文件和main可执行文件

在shell命令行执行make命令



查看make执行makefile文件后的编译结果如下:



与方法1的结果基本一致,并且直接生成了可执行文件main

最后,输入 ./main 运行结果



此方法,与方法1运行结果,完全一致!

评析: 方法2利用makefile文件,进行项目所有文件的编译管理,可保存、易修改,且编译执行效率高,大大减轻了每次编译的工作量

方法2,仅仅是最为初级的makefile项目管理格式,现在我们将逐步对其进行优化、改进

四、方法
3 (使用变量——改进1)

在编写makefile文件时,各部分引用变量的格式规范

1、 make变量引用不同于Linux Shell变量引用规则,而是需加括号,即 $(Var) 格式,无论 Var 是单字符变量名还是多字符变量名均可。

2、在命令行中出现的Shell变量,引用Shell的 $tmp 格式,一般为执行命令过程中的临时变量,不属于makefile变量,而是Shell变量。

3、对出现在命令行中的make变量,同样使用 $(Command) 格式来引用。

纸上得来终觉浅,绝知此事要躬行。输入vim makefile命令,在Shell 利用vim编辑器来编写makefile文件,具体写法如下:

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

OBJ=main.o add_int.o add_float.o sub_int.o sub_float.o

make: $(OBJ)

gcc -c main.c -o main.o

add_int.o: add_int.c add.h

gcc -c add_int.c -o add_int.o

add_float.o: add_float.c add.h

gcc -c add_float.c -o add_float.o

sub_int.o: sub_int.c sub.h

gcc -c sub_int.c -o sub_int.o

sub_float.o: sub_float.c sub.h

gcc -c sub_float.c -o sub_float.o

clean:

rm -f $(OBJ) main

然后,在shell命令行执行make命令



最后,输入 ./main 运行结果



此方法,与方法1和方法2运行结果,完全一致!

评析: 方法3利用makefile变量,引入变量使makefile更加简洁、清晰,便于分组、统一维护,编译管理更加高效

五、方法
4 (使用自动推导——改进2)

编写makefile文件,让make命令自动推导。只要make看到了 *.o 文件,它就会自动把与之对应的 *.c 文件加到依赖文件中,并且gcc -c *.c 也会被推导出来,所以makefile就简化啦。 此外,我们使用 $(Command) 格式,来引用命令变量。具体做法如下

首先,在Shell输入 vim makefile ,利用VIM编辑makefile文件内容

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

CC=gcc

OBJ=main.o add_int.o add_float.o sub_int.o sub_float.o

make: $(OBJ)

$(CC) -o main $(OBJ)

main.o: add.h sub.h

add_int.o: add.h

add_float.o: add.h

sub_int.o: sub.h

sub_float.o: sub.h

PHONY: clean

clean:

rm -f $(OBJ) main

然后,在shell命令行执行make命令



最后,输入 ./main 运行结果



此方法,与方法1、方法2和方法3的运行结果,完全一致!

评析: 方法4在makefile文件中,引入参数变量和命令变量,利用make命令自动推导依赖文件,来编译系统,高效但不太直观,高手可用

六、方法5 (使用自动变量($^
$< $@)——改进3)

在编写makefile文件中,有三个非常有用的变量,即分别是 $@ $^ $< 其代表的具体意义如下:

$@ : 目标文件

$^ : 所有依赖文件

$< : 第一个依赖文件

具体使用方法如下例所示

首先,在Shell输入 vim makefile ,利用VIM编辑makefile文件内容

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

CC=gcc

OBJ=main.o add_int.o add_float.o sub_int.o sub_float.o

make: $(OBJ)

$(CC) -o $@ $^

main.o: main.c add.h sub.h

$(CC) -c $<

add_int.o: add_int.c add.h

$(CC) -c $<

add_float.o: add_float.c add.h

$(CC) -c $<

sub_int.o: sub_int.c sub.h

$(CC) -c $<

sub_float.o: sub_float.c sub.h

$(CC) -c $<

PHONY: clean

clean:

rm -f $(OBJ) main

然后,在shell命令行执行make命令



最后,输入 ./main 运行结果



此方法,与方法1、方法2、方法3和方法4的运行结果,完全一致!

评析: 方法5在makefile文件中,引入参数变量、命令变量和自动变量,此方法编译系统,高效但不太直观,特别是维护修改不便,高手可秀。

七、方法6 (使用缺省规则(..c.o:)—— 改进4)

在依次使用了上述变量、自动推导、自动变量规则后,或许还有人认为太复杂,想寻求更简洁的方法,这里我们再介绍makefile缺省规则。

makefile的缺省规则如下:

..c.o:

gcc -c $<

这个规则表示,所有的 *.o 目标文件都是依赖于相应的 *.c 源文件的, 例如 main.o 依赖于 main.c 。 具体makefile编写方法如下

[cpp:showcolumns] view
plaincopyprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

CC=gcc

main: main.o add_int.o add_float.o sub_int.o sub_float.o

$(CC) -o $@ $^

..c.o:

$(CC) -c $<

clean:

rm -f *.o main

然后,在shell命令行执行make命令



最后,输入 ./main 运行结果



此方法,与方法1、方法2、方法3、方法4和方法5的运行结果,完全一致!

评析: 方法6在makefile文件中,引入缺省规则,是make自动推导,非常简洁、高效,但不太直观,特别是具体文件依赖关系不清,维护较不便。

==================================================================================

综合评析: 以上方法,由浅入深,点评剖析,重在灵活运用。方法2直观易懂,方法3引入变量简洁,这两种方法便于管理维护,推荐使用。方法4、方法5和方法6,主要是深入剖析makefile博大精深的编写使用方法,在具体项目管理实践中,可以选择借鉴使用,适合内功深厚者。

以上示例程序,均已测试并运行通过 ,具体测试编译环境如下:

Linux系统: Red Hat Linux Server 5.2

VIM编辑器:VIM - Vi IMproved 7.0

系统的环境:Linux安装在VMWare 7.0 虚拟机上

==================================================================================

编译Bug与Debug小结

1、makefile: 4: *** 遗漏分隔符 。 停止 。 错误提示,如下图



分析与处理: 以上错误提示,说明makefile文件第4行,分隔符格式不正确,导致错误。错误详见下图



从上图可见,第4行为command命令行,应该如上述方法2中强调所说,命令行应当Tab分隔缩进 ,解决后如下图所示:



2、make: main 是最新的。 错误提示,如下图



这是因为该文件目录中,已经存在了目标可执行文件 main ,请见下图



解决办法:输入 rm main 或者 rm -f main 命令,先删去
main 文件,然后再输入 make 命令
,进行编译链接即可



编译链接成功后,直接利用 ./main 运行生成的目标可执行文件即可啦 ^_^

Makefile里PHONY的相关介绍

PHONY目标并非实际的文件名:只是在显式请求时执行命令的名字。有两种理由需要使用PHONY目标:避免和同名文件冲突,改善性能。
如果编写一个规则,并不产生目标文件,则其命令在每次make该目标时都执行。例如:

  clean:

  rm*.o
temp

因为"rm"命令并不产生"clean"文件,则每次执行"makeclean"的时候,该命令都会执行。如果目录中出现了"clean"文件,则规则失效了:没有依赖文件,文件"clean"始终是最新的,命令永远不会执行;为避免这个问题,可使用".PHONY"指明该目标。如:

  .PHONY:
clean

  这样执行"makeclean"会无视"clean"文件存在与否。
已知phony目标并非是由其它文件生成的实际文件,make会跳过隐含规则搜索。这就是声明phony目标会改善性能的原因,即使你并不担心实际文件存在与否。

  完整的例子如下:

  .PHONY:
clean

  clean:

  rm*.o
temp
phony目标可以有依赖关系。当一个目录中有多个程序,将其放在一个makefile中会更方便。因为缺省目标是makefile中的第一个目标,通常将这个phony目标叫做"all",其依赖文件为各个程序:

  all:
prog1 prog2 prog3

  .PHONY:
all

  prog1:
prog1.o utils.o

   cc-o
prog1 prog1.o utils.o

  prog2:
prog2.o

   cc-o
prog2 prog2.o

  prog3:
prog3.o sort.o utils.o

   cc-o
prog3 prog3.o sort.o utils.o

假设你的一个项目最后需要产生两个可执行文件。你的主要目标是产生两个可执行文件,但这两个文件是相互独立的——如果一个文件需要重建,并不影响另一个。你可以使用“假象目的”来达到这种效果。一个假象目的跟一个正常的目的几乎是一样的,只是这个目的文件是不存在的。因此,
make总是会假设它需要被生成,当把它的依赖文件更新后,就会执行它的规则里的命令行。

如果在我们的makefile开始处输入:

all: exec1 exec2

其中exec1和exec2是我们做为目的的两个可执行文件。make把这个'all'做为它的主要目的,每次执行时都会尝试把'all'更新。但既然这行规则里没有哪个命令来作用在一个叫'all'的实际文件(事实上
all并不会在磁碟上实际产生),所以这个规则并不真的改变
'all'的状态。可既然这个文件并不存在,所以make会尝试更新all规则,因此就检查它的依靠exec1,exec2
是否需要更新,如果需要,就把它们更新,从而达到我们的目的。

假象目的也可以用来描述一组非预设的动作。例如,你想把所有由make产生的文件删除,你可以在makefile里设立这样一个规则:

veryclean:

rm *.o

rm myprog

前提是没有其它的规则依靠这个'veryclean'目的,它将永远不会被执行。但是,如果你明确的使用命令
'makeveryclean'
,make会把这个目的做为它的主要目标,执行那些rm命令。

如果你的磁碟上存在一个叫veryclean文件,会发生什么事?这时因为在这个规则里没有任何依靠文件,所以这个目的文件一定是最新的了(所有的依靠文件都已经是最新的了),所以既使用户明确命令
make重新产生它,也不会有任何事情发生。解决方法是标明所有的假象目的(用
.PHONY),这就告诉make不用检查它们是否存在于磁碟上,也不用查找任何隐含规则,直接假设指定的目的需要被更新。在
makefile里加入下面这行包含上面规则的规则:

.PHONY: veryclean

就可以了。注意,这是一个特殊的make规则,make知道.PHONY是一个特殊目的,当然你可以在它的依靠里加入你想用的任何假象目的,而
make知道它们都是假象目的
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: