文章目录
c文件的编译原理
- 原理流程
说到Makefile,不得不谈一谈C源文件的编译过程啦,整个过程使用下图足以说明一切。
- 功能
- 上述编译流程针对每个源文件而言,在一个项目的编译工作中,一般使用linux GNU make管理和构建自己的工程,如果不使用make,可想而知,编译上百个源文件得编译到何年马月,所以Makefile对于一个linux开发人员是何等的重要。
什么是Makefile
1、make如何工作的
make工具依赖Makefile或着makefile文件,执行make命令时,make会去当前工作目录下寻找Makefile或makefile文件,然后根据目标和依赖项按顺序执行任务。首次make将执行所有任务,如果后面某个依赖项被更新,make只执行被更新过的依赖项,减少不必要的系统开销,节省编译时间。
2、为什么要使用Makefile
Makefile的最大作用就是自动化编译,只要写好了Makefile文件,终端键入make命令就可以执行Makefile文件里面的指令。在实际项目中,一个工程包含几十个,甚至几百个源文件,如果手动gcc编译一个个源文件,即使你有使不完的力气,和你协作共事的小伙伴也没有那个耐心等你gcc编译完所有的源文件。所以,Makefile不仅可以提高个人的专业技能,更重要的是大大提高了工作的效率。
一次编写Makefile,终生受用,源文件被修改后,只需一键make就搞定。
3、Makefile与shell
Makefile类似于shell脚本,都可以执行操作系统的命令。Makefile和shell是两种不同的脚本,Makefile专门用于编译的专用工具,shell相当于一个命令行终端,是一个通用工具。当然用shell脚本也可以实现自动化编译,但相对Makefile来说,无论从执行效率、编程难易上来说都显得复杂很多。所以在工程编译工作中,开发人员都比较喜欢使用Makefile,有时结合shell脚本做一些简单的工作,比如程序稳定性测试、黑白盒测试、压力测试等。
Makefile中的shell命令必须以[tab]键开头。
Makefile的核心思想
target...: dependency...
command
...
...
Makefile的核心思想如上面的代码所示,其实很简单,主要由目标项、依赖项和shell命令组成。
-
目标项(target)
目标项执行这条shell命令要生成的目标文件,比如gcc hello.c -o hello,hello就是一个目标项。这个目标项不一定是最终的可执行文件hello,也可使是中间目标文件hello.o,还可以是一个标签,如伪目标。 -
依赖项(dependency)
依赖项就是生成这个目标项所必要的文件。 -
shell命令(command)
shell对于我们来说再熟悉不过了,我们在linux终端使用的一切命令都是shell命令。
逐步了解Makefile
初始版示例
-
工作目录下的文件
对client.h文件的说明:在这个目录下,我将所有头文件都包含在client.h中,单独一个文件管理着所有文件,当增加头文件时,只需在这个文件中添加,不必去c源文件中添加。
-
Makefile文件
1
2 client_main:client_main.o domain_parse.o get_temp.o log.o
3 gcc -o client_main client_main.o domain_parse.o get_temp.o log.o
4
5 client_main.o:client_main.c client.h
6 gcc -c client_main.c
7
8 domain_parse.o:domain_parse.c client.h
9 gcc -c domain_parse.c
10
11 get_temp.o:get_temp.c client.h
12 gcc -c get_temp.c
13
14 log.o:log.c client.h
15 gcc -c log.c
16
17 clean:
18 rm -rf client_main.o domain_parse.o get_temp.o log.o
解析:
2、5、8、11、14、17一共有6个目标,其中第17行clean不是总目标的依赖项,make不会自动执行,只有输入
make clean才执行它。
第一步:输入make命令后,make机制开始在当前目录下寻找Makefile文件,并找到总目标,如client_main。
第二步:然后按总目标的依赖项的先后顺序找到依赖项,如client_main.o。
第三步:如果该依赖项也是一个目标项,程序就跳到相应依赖项作为目标项的地方,如第5行client_main.o:。
第四步:如果新依赖项满足新目标项,程序开始执行shell命令,如gcc -c client_main.c 。
第五步:重复第二步~第五步,直到所有任务执行完成。
-
make结果
此时可以看到目录中多了 *.o文件和client_main,make并没有自动执行clean,因为clean并不是总目标里面的依赖项。所以手动make clean
此时make删除所有.o文件。
改进版
一级示例
1
2 OBJS=client_main.o domain_parse.o get_temp.o log.o
3
4 client_main:$(OBJS)
5 gcc -o client_main $(OBJS)
6
7 client_main.o:client.h
8
9 domain_parse.o:client.h
10
11 get_temp.o:client.h
12
13 log.o:client.h
14
15 .PHONY:clean
16 clean:
17 -rm client_main $(OBJS)
从上面的Makefile文件中可以看出,代码比初始版本简洁许多,在此版本中增加了以下内容:
-
变量
类似于c语言中的宏定义,它代表文本字串。第2行定义OBJS变量并赋值为所有的*.o文件,在后面的语句中所有使用.o文件的地方用$(OBJS)代替,这样做的好处是,增加或删除某个.o文件时,只需修改变量OBJS的值。 -
自动推导功能
make会根据.o文件自动推到出.c文件,并根据总目标中的shell命令执行次级目标任务,比如在此版本中将所有次级目标的依赖文件.c文件删除。 -
伪目标
伪目标不是文件,可有可无依赖文件,是一个标签,第15行.PHONY是伪目标关键词,clean使用伪目标的原因是,如果当前工作目下有一个文件名为clean,执行make就会出错,所以使用伪目标用于出区分。 -
-rm
如果文件出错,不管,继续往后执行。
二级示例
1 OBJS:=$(patsubst %.c,%.o,$(wildcard *.c))
2 CC:=gcc
3 CFLAGS:=-g
4 TARGETS:=client_main
5 RM:=-rm
6 #终极目标
7 $(TARGETS):$(OBJS)
8 $(CC) -o $@ $^ $(CFLGS) $(LDFLAGES)
9
10 #清除命令
11 .PHONY:clean cleanall
12 clean:
13 $(RM) $(OBJS)
14 cleanall:
15 $(RM) $(OBJS) $(TARGETS)
解析:
- 第1行 调用函数将当前工作目录下的所有c文件替换为中间目标文件.o文件。简单对函数说明一下:
1、patsubst
调用:$(patsubst pattern,replacement,text)
参数:查找text中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式pattern,如果匹
配的话,则以replacement替换。这里,pattern可以包括通配符“%”,表示任意长度的字串。如果
replacement中也包含“%”,那么,replacement中的这个“%”将是pattern中的那个“%”所代表的字
串。(可以用“\”来转义,以“%”来表示真实含义的“%”字符)
返回 :函数返回被替换过后的字符串。
2、wildcard 获取当前目录下的所有c文件
- 第2行~第5行:对环境变量重新定义,使之不适用默认值
- 第7行:总目标和依赖
- 第8行:执行shell命令。这里去掉了一级版本中的语句,通过使用make的隐含规则,自动推导c文件所需要的头文件和编译总目标需要的中间目标文件。
- 第11行~第15行:使用伪目标做删除文件处理,在这里主要删除中间目标文件和最终的可执行文件。
高级版
前面的示例是在同一个目录下编写Makefile,但在linux内核源码中,有多个子目录,每个子目录下存在着一个管理文件的Makefile,决定哪些文件需要编译,哪些文件不需要编译。在顶层根目录下有一个总控Makefile,这个Makefile的作用就是管理着其他子目录下的Makefile。
- 先在根目录下创建目录树和定义好测试文件。比如:
- 简单介绍一下目录中的文件,print1.c、print2.c、print3.c三个文件内程序是一样的,函数名和头文件不同而已:
#include "../../include/print1.h"
void print1(void)
{
printf("here is print1\n");
}
- print1、print2、print3目录下是Makefile是一样的,用于管理自己目录下c文件的编译:
#遍历目录下的c文件
SRC:=$(wildcard *.c)
#更改文件名后缀
OBJS:=$(patsubst %.c,%.o,$(SRC))
../../$(OBJS_DIR)/$(OBJS):$(SRC)
$(CC) -c $^ -o $@
- src目录下的Makefile用于管理自己的子目录,决定make应该进去哪个子目录里面,在这一层中子目录是print1、print2、print3:
#定义子目录
SUBDIRS := print1 print2 print3
all:compile_src
#set命令-e,若指令传回值不等于0,则立即退出shell
#for循环依次进入子目录
compile_src:
set -e; for i in $(SUBDIRS); do $(MAKE) -C $$i; done
- 顶层中的Makefile,也可以叫它总控Makefile,它控制着子目录中make的走向,功能和src中的Makefile是一样的:
#定义gcc
CC := gcc
#定义子目录
SUBDIRS := main src obj
#定义bin目录
BIN_DIR := bin
#定义最终得可执行文件
BIN := my_app
#定义.o文件目录
OBJS_DIR := obj
#定义清除命令
RM := rm
#传参下一层Makefile
export CC OBJS_DIR BIN_DIR BIN
#总目标
all:check_bin compile_src
#创建bin目录
check_bin:
mkdir -p $(BIN_DIR)
#编译子目录中的源码
compile_src:
set -e; for i in $(SUBDIRS); do $(MAKE) -C $$i; done
#清除文件
.PHONY:clean cleanall
clean:
$(RM) -rf $(OBJS_DIR)/*.o
cleanall:
$(RM) -rf $(OBJS_DIR)/*.o $(BIN_DIR)/$(BIN)
- 说一下export
export的作用是总控Makefile向下一层Makefile中传递参数。
调用格式
export <variable …>
在本示例中总控Makefile
export CC OBJS_DIR BIN_DIR BIN
下层的Makefile加$直接使用,不必向c语言中一样定义参变量。
- 测试
在顶层目录下输入make,回车
运行一下最终的可执行文件
总结一下
通过学习了Makefile,我想说一下自己的感受,Makefile说简单也简单,说难也难,说简单是因为它的编程思想简单,一个目标项,一个依赖项,再加上shell命令,它没有c/c++、java中的算法,也没有很多的函数。说难是因为平时自己写程序都是几个文件而已,少则一个c文件,多则十几个,写个Makefile很简单,但是有的工程,目录加子目录几十个,文件上百个,可能每个目录下都有Makefile,一层嵌套一层,就像在c程序中大量使用go to语句一样,很难受。可能我还是小白的原因,我相信只要矜持学习,一切都会变得简单起来的。