makefile个人理解
makefile
makefile抽象层面的理解
学习某一样东西之前一定要明确学习的目的,即学习了这项工具能解决一些什么问题,其优势是什么?
makefile的优势就是能够动态根据文件的新旧来决定是否编译对应的文件,倘若每次编译一个项目都重新编译,特别是大项目的时候,岂不是很浪费时间?makefile能自动根据依赖关系解决这个问题,对于已经编译过的文件不再重新编译,而只选择编译尚未编译(或者新)的文件。
那么如何实现自动编译新文件呢?makefile是如何识别哪些是新的文件哪些是旧的文件的呢?
这就是makefile的核心--依赖关系
makefile中的语句就是为描述各种依赖关系而建立的。
例如下面这个例子
main:arithmetic.o main.o myprint.o
main.o:main.c
gcc -c main.c -o main.o
arithmetic.o:arithmetic.c
gcc -c arithmetic.c -o arithmetic.o
myprint.o:myprint.c
gcc -c myprint.c -o myprint.o
翻译过来,用通俗的话说就是:
- 如果main的修改时间在arithmetic.o main.o myprint.o的修改时间之前,那么就更新arithmetic.o main.o myprint.o (实际上就是确保main是最新的,如果不是,就更新冒号右边的文件。下面的语句也是类似的)
- 保证main.o是最新的
- 保证arithmetic.o是最新的
- 保证myprint.o是最新的
那么如何更新这个文件呢?就通过冒号:
下面一行的语句(有时可省略)来进行更新的操作(gcc编译出新文件覆盖掉旧文件)
显然,如果每一个源程序都需要一一给出依赖关系那么这个工作量无疑是巨大的。为此makefile提供了一些更简便的操作方法来管理,如提供变量,嵌入shell语句等。这些操作方法使得makefile管理文件变得简单的同时,也使得makefile的学习成本增加。但是只要理解了上面makefile的基本原理,那么makefile中的其他高级操作不过是对这种建立依赖关系的方法进行了扩充,只要多背几条语句/语法规则即可。
makefile简单例子
下面,通过一个具体的实例来说明更新的过程。
main.c
#include <stdio.h>
#include "arithmetic.h"
#include "myprint.h"
int main()
{
int a=1;
int b=1;
printf("a+b=%d\n",add(a,b));
myprint();
printf("hello world!\n");
return 0;
}
myprint.h
#ifndef __MYPRINT
#define __MYPRINT
#include <stdio.h>
void myprint(void);
#endif
myprint.c
#include "myprint.h"
void myprint(void)
{
printf("my print\n");
}
arithmetic.h
#ifndef __ARITHMETIC
#define __ARITHMETIC
int add(int a ,int b);
#endif
arithmetic.c
#include "arithmetic.h"
int add(int a ,int b)
{
return a+b;
}
makefile
版本1
main:arithmetic.o main.o myprint.o
main.o:main.c
gcc -c main.c -o main.o
arithmetic.o:arithmetic.c
gcc -c arithmetic.c -o arithmetic.o
myprint.o:myprint.c
gcc -c myprint.c -o myprint.o
.PHONY:clean
clean:
rm -rf *.o
版本2
main:arithmetic.o main.o myprint.o
main.o:main.c
gcc -c $^ -o $@
arithmetic.o:arithmetic.c
gcc -c $^ -o $@
myprint.o:myprint.c
gcc -c $^ -o $@
.PHONY:clean
clean:
rm -rf *.o
版本3
main:arithmetic.o main.o myprint.o
%.o:%.c
gcc -c $^ -o $@
.PHONY:clean
clean:
rm -rf *.o
版本4
OBJS = main.o\
myprint.o\
arithmetic.o
main:$(OBJS)
%.o:%.c
gcc -c $^ -o $@
.PHONY:clean
clean:
rm -rf *.o
语法讲解:
%.o
:表示当前目录下所有的.o文件
%.c
:表示当前目录下所有的.c文件
$^
:表示目标文件,即:
左边的文件
$@
:表示所有的依赖文件,即:
右边的所有文件
.PHONY:clean
:命令行输入make clean
时会执行clean中的语句,但不会生成clean目标
一些常用的结论:
-
可以将所有的目标-依赖语句(不包括前面部分的变量初始化语句)都抽象称为如下的表示
目标文件:依赖文件 执行语句
那么所有的
执行语句
执行与否取决于目标文件和依赖文件的更新时间的比较(为了简化思考可以直接认为是一定会执行的) -
执行语句
实际上就是shell命令
一般能理解并能手动写出版本4的makefile就足够用了。上面例程是仿照下面的博客做的,并加入了自己的一些理解和修改,感谢下面博客博主的热心分享。
具体的语法这里也不再给出具体说明,因为这些资源网络上各处都有。更重要的是对makefile思想以及抽象层面上的理解。
实战:对实际的代码进行分析
要分析的代码:
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := kernel_hello.o
modules:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
@make clear
clear:
@rm -f *.o *.cmd *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
clean:
@rm -f hello.ko
分析:
-
等号的区别
var ?= will_not_excute var = first_assign var = second_assign result1 = $(var) result2 := $(var) result3 += $(var) result3 += $(var) var = final_assign main: @echo var final val: $(var) @echo result1: $(result1) @echo result2: $(result2) @echo result3: $(result3) 结果: $ make var final val: final_assign result1: final_assign result2: second_assign result3: final_assign final_assign
从代码的执行结果分析,可以得出几点结论.
?=
只有该变量在整个makefile都没有被赋值的时候才进行赋值:=
右边部分调用$(var)
会得到var上一次的值=
右边部分调用$(var)
会得到var在整个makefile中最后的值+=
不用多说,就是追加操作
-
@
语句表示不把正在执行的语句打印出来(但仍然会打印语句执行的结果)
main: echo will display this statement @echo will not display this statement $ make echo will display this statement will display this statement will not display this statement
-
$(MAKE)
解析出来就是"make" -
-C
指明跳转到内核源码目录下读取那里的MakefileM=$(PWD)
表明然后返回到当前目录继续读入、执行当前的Makefile这两个参数怎么理解呢?
一般来说,一些大的工程会有几百个文件夹,而且每个文件夹里面都可能存在Makefile,这些Makefile记录着它所在文件夹下的源文件的编译步骤,但仅仅这样是不够的,这些Makefile只是做着自己当前目录下的事情,它们之间还需要进行一定程度的交流,那么,顶层Makefile就出现了,这个顶层Makefile不断地通过
-C M=
参数执行其管理的子Makefile,就像一个统筹全局的军师.有了顶层Makefile那么不管多大工程都能顺利编译出来了,而且还对一些模块进行了解耦,方便管理与修改.