Linux工程管理文件Makefile规则与目标依赖-基础篇

在程序编译过程中,Makefile的主要作用就是:构建生成可执行文件的依赖关系树。一个可执行文件需要哪些源文件、哪些库,我们都可以在Makefile里指定。在程序编译阶段,make工具会首先解析这个Makefile,根据Makefile里的指定,构建出编译可执行文件所需要的完整依赖关系。

根据这个依赖关系,make工具接下来就可以分别去编译需要的文件;通过时间戳,可以动态识别哪些是新添加的文件,哪些是已经编译过的文件,哪些文件做了新的修改,这样就不用每次都重新编译了,只编译那些新修改或新添加的源文件就可以了,大大提升了编译的效率。

那Makefile是如何构建可执行文件编译所依赖的关系树的呢?很简单,通过规则。

1.规则

规则是Makefile的基本组成单元。一个规则通常由目标、目标依赖和命令三部分构成:

目标:目标依赖    
	命令
a.out:hello.c    
	gcc -o a.out hello.c

如上所示,a.out就是我们要生成的目标,目标一般是一个可执行文件。目标依赖是指生成这个可执行文件所依赖的源文件,如 hello.c。而命令则是如何将这些目标依赖生成对应的目标,一般是gcc命令、链接命令、objcopy命令,一些shell命令等。命令必须使用 tab 键进行缩进,否则Makefile就会报错。

一个规则中的三个部分并不是都必须要有的。一个Makefile文件中可能包含多个规则,有的规则可能无目标依赖,仅仅是为了实现某种操作。如下面的clean命令:

clean:    
	rm -f a.out hello.o

当我们使用make clean命令清理编译的文件时,会调用这个规则中的命令,不需要什么依赖,仅仅是执行删除操作,所以这个规则中并没有目标依赖。

当然,一个规则中也可以没有命令,仅仅包含目标和目标依赖,仅仅用来描述一种依赖关系。但一个规则中一定要有一个目标。

2.默认目标

一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。正常情况下,当你想编译生成a.out时,要使用make a.out命令,但为什么直接使用make,没有指定参数,也能生成a.out的呢?这是因为在上面的Makefile文件中,a.out是文件中的第一个目标,当我们在make编译时没有给make指定要生成的目标,make就会选择Makefile文件中的第一个目标作为默认目标。

3.多目标

一个规则中也可以有多个目标,多个目标具有相同的生成命令和依赖文件。如一个目标文件%.o都是由其对应的源文件%.c编译生成的,生成命令也是相同的:

*.o: *.c    
	gcc -o *.o *.c

4.多规则目标

多个规则可能是同一个目标,make在解析Makefile文件时,会将具有相同目标的规则的依赖文件合并。

a.out: hello.c    
	gcc -o a.out hello.c
a.out: 
	module.c

Make在解析Makefile构建依赖关系树时,会将这两个规则合并为:

a.out: hello.c module.c    
	gcc -o a.out hello.c other.c

5.伪目标

有时候我们设置一个目标,并不是真正生成这个文件,如上面的clean目标,而是仅仅为了执行某个操作。当我们执行make clean时,clean这个目标并没有生成对应的目标文件clean,因此,clean也可以设置为伪目标。伪目标并不是一个真正的文件,可以看做是一个标签。

.PHONY: clean
a.out: hello.o    
	gcc -o a.out hello.o
hello.o: hello.c    
	gcc -c -o hello.o hello.c
clean:    
	rm -f a.out hello.o

伪目标一般没有依赖关系,也不会生成对应的目标文件,可以无条件执行,纯粹是为了执行某一个命令,如clean执行清理工作。

make第一次编译某个项目时,会依次编译所有的源文件。但是当我们修改程序后,再次使用make编译,make并不会重新编译整个源文件,而是只编译你新添加或修改了的源文件,那make是如何做到的呢?

很简单,make是根据时间戳来判断一个规则中的目标依赖文件是否有更新。make在编译程序时,会依次检查依赖关系树中的所有源文件的时间戳,如果发现某个文件的时间戳有更新,会认为这个文件有改动过,会重新编译这个源文件。如果发现文件的时间戳没有更新,就不会再重新编译一次。

但是还有一种情况我们没有考虑到:在Makefile的规则中,一般不会把头文件添加到目标依赖中。当一个.c文件中包含多个头文件时,如果对应的头文件发生了变化,因为头文件没有包含在依赖关系树中,所以这个.c文件就不会重新编译。如下面的程序:

//hello.c
#include <stdio.h>
#include "module.h"

int main(void){    
	printf("PI = %f\\n", PI);    
	func();    
	printf("hello QST!\\n");    
	return 0;
}

//module.c
#include <stdio.h>

void func(void){    
	printf("hello func!\\n");
}

//module.h
#ifndef __MODULE_H__
#define __MODULE_H__
#define PI 3.14
#endif


//Makefile
.PHONY: clean
a.out: hello.o module.o    
	gcc -o a.out hello.o module.o
hello.o: hello.c    
	gcc -c -o hello.o hello.c
module.o: module.c    
	gcc -c -o module.o module.c

clean:    
	rm -f a.out hello.o

第一次使用make编译生成a.out运行无误后,然后修改module.h中的宏定义PI值为3.1415,再次使用make编译程序,你会发现make并没有重新编译:

wit@pc:/home/makefile# makemake: 'a.out' is up to date.

这是因为module.h并没有添加到Makefile的规则依赖目标中,所以无论你怎么修改module.h,都不会重新编译helloworld.c源文件。

6.头文件依赖

其中一个解决方法是将头文件module.h添加到规则的目标依赖列表中:

//Makefile
.PHONY: clean
a.out: hello.o module.o module.h    
	gcc -o a.out hello.o module.o

将头文件添加到规则的目标依赖中,虽然能解决依赖关系的问题,但是也有弊端,当helloworld.c包含几十个头文件时,把包含的这些头文件都手工添加进去,工作量还是蛮大的。如果一个项目中有成百上千个C文件,每个C文件包含几十个头文件,按照这种手工添加的方法,996无疑。

7.自动生成头文件依赖关系

一个更高效的解决方法是:使用gcc -M 命令自动生成头文件依赖关系

wit@pc:/home/makefile# gcc -M hello.c
hello.o: hello.c /usr/include/stdc-predef.h \\ 
/usr/include/stdio.h \\ 
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \\ 
/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \\
/usr/include/x86_64-linux-gnu/bits/wordsize.h \\ 
/usr/include/x86_64-linux-gnu/bits/long-double.h \\ 
/usr/include/x86_64-linux-gnu/gnu/stubs.h \\ 
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \\ 
/usr/lib/gcc/x86_64-linux-gnu/9/include/stddef.h \\ 
/usr/lib/gcc/x86_64-linux-gnu/9/include/stdarg.h \\ 
/usr/include/x86_64-linux-gnu/bits/types.h \\ 
/usr/include/x86_64-linux-gnu/bits/timesize.h \\ 
/usr/include/x86_64-linux-gnu/bits/typesizes.h \\ 
/usr/include/x86_64-linux-gnu/bits/time64.h \\ 
/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h \\ 
/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \\ 
/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h \\ 
/usr/include/x86_64-linux-gnu/bits/types/__FILE.h \\ 
./usr/include/x86_64-linux-gnu/bits/types/FILE.h \\ 
/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h \\ 
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \\ 
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h module.h

如上所示,hello.c源文件中包含了两个头文件:stdio.h和module.h,通过gcc -M命令,我们就可以自动生成一个hello.o目标文件的依赖关系,就不需要我们手动将头文件添加到规则中了。

  • 22
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值