逐步学习嵌套Makefile

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语句一样,很难受。可能我还是小白的原因,我相信只要矜持学习,一切都会变得简单起来的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章