Makefile的使用

1 概要

软件的分层使软件的逻辑关系更清晰,但是也带来一个副作用,即Makefile也变得复杂了。道理显而易见:对于一个简单项目,如果所有文件都放在同一个文件夹内,Makefile写起来也会十分简单,但是我们不能一直停留在原始时代,当复杂项目的源文件按类型、功能、模块等分散到不同路径时,需要我们掌握复杂的Makefile写法来编译它们。

Makefile其实就是一套规则(相当于脚本),make按照这一套规则(相当于解释器),生成最后的结果;其次如果只改了某些文件,重新编译时可以只编译那些改变的部分(即增量编译),这样加快了再次编译速度。所以掌握了Makefile可以实现自动化编译可以提高版本构建效率。

2 Makefile的框架

很多人会被Makefile吓到,归结下来,可能是如下几个原因:
一是因为Makefile有独自的写法,和平时常用的c、python等语法不同;
二是一些IDE屏蔽了工程的构建信息,由于不常使用所以不熟悉;
三是Makefile中涉及到编译的几个步骤以及gcc命令、shell脚本等,需要多方面的知识,更令人迷惑的则是Makefile中的隐式规则。

但把这些知识点一个个弄明白,分而治之,掌握Makefile也非难事。

Makefile入门网上有许多资料,本文只是梳理我认为的几个Makefile常用知识点。以下分别介绍gcc编译C代码的过程以及复杂Makefile的编写。

2.1 gcc编译C代码过程

对于如下c代码:

#include <stdio.h>

int main() 
{
	printf("hello, world\n");
	return 0;
}

其编译过程如下4步(编译环境为Ubuntu):

编译过程

step1: 预编译:

gcc -E hello.c -o hello.i

程序中有以#include #define开头的行,称为预处理语句(C语言的编译预处理命令必须用“#”开头),在编译之前必须由编译预处理将它们替换成C编译程序能够接受的正文。分为:

  1. 宏定义, #define 展开宏定义

  2. 条件编译, 如:“#if” “#ifdef” “#else” “#elif” “#endif”等

  3. 文件包含 #include,将被包含的文件插入到该预编译指令的位置

    还有一些删除注释、添加行号和文件名标识等也是在预处理这一步完成的。

step2: 编译:

gcc -S hello.i -o hello.s

编译是指:将高级语言(C语言)翻译为汇编语言的过程,其中包括翻译和查错(词法分析、语法分析、语义分析生成和优化目标代码,出错时,停止编译)。

step3: 汇编:

gcc -c hello.s -o hello.o

汇编过程是将汇编代码转换为机器代码的过程,每一条汇编语句几乎都对应着一条机器指令。

step4: 链接:

gcc -o hello hello.o

示例中hello.c 程序调用了 printf 函数(存在在libc.a中),链接器将libc.a中的print.o与hello.o重新组织以下,形成最终的可执行程序,这属于静态链接过程(static linking)。

2.2 复杂Makefile的组织方式

只有单个文件的Makefile写起来与直接用手敲2.1节的几条命令区别并不大,并不能体现出Makefile的优势;当项目变得复杂(目录结构很多),Makefile才能体现其作为脚本的优势。

1. make与Makefile与gcc的关系:

gcc是编译器;而make是一个命令工具,用来解析makefile脚本。

可以这么简单的比方:

makefile是像一首歌的曲谱,曲谱中写了怎么调用gcc、 GNU binutils、shell命令等对整个项目的各个文件进行分别编译和链接;

make工具就像指挥家,指挥家根据曲谱指挥演奏者怎么样演奏(make工具就根据makefile中的命令进行编译和链接的);

而gcc、 GNU binutils、shell等像演奏者,实际干活的是它们。

2. 构建的核心:

简单认为:构建就是将库、可链接二进制文件(linux上的.o文件)链接成可执行文件的过程。涉及到几个问题:

  1. 库从什么地方找?(-L指令)
  2. 库的名字是什么(-l指令)
  3. 怎么编译库或者可链接二进制文件?源文件怎么收集?(wildcard、foreach、call等系列函数的灵活使用)
  4. 头文件哪里找?(-I指令 汇编的时候才需要头文件,链接时不需要头文件)

在编写Makefile时,解决上述几个问题,就可写出正确的Makefile。当然还要注意不少的细节。

3. 复杂Makefile组织的方式:

例如:一个多目录结构,文件夹如树状结构组织,源文件分散在其中。Makefile同样需要树状结构组织(这并不绝对,只要Makefile能找到源文件即可)。

例子:引用的是李老师的B站视频(见参考3),虽然例子比较简单,但是复杂的文件结构也可类似处理。例子已推送到github:

https://github.com/sz-ok/Makefile_learning

下图中手动为Makefile指定了一个标记,方便后面表述。

$ tree
.
├── head
│   └── head.h
├── main
│   ├── main.c
│   └── Makefile --------- mk-main
├── Makefile ------------- mk-top
└── tst
    ├── foo
    │   ├── foo.c
    │   └── Makefile ----- mk-foo
    ├── Makefile --------- mk-tst
    └── tst.c

mk-top:

#作用是制定规则来说明当前目录下生成终极目标文件test
TGT = test
#指定子目录
SUB_DIR = main tst
#指定当前目录
export TOP_PATH = $(shell pwd)
#指定头文件目录
export HEAD_PATH = $(TOP_PATH)/head
#指定子目标
export SUB_TGT = bulit_in.o
#CROSS_COMPILER = arm-linux-
export CC = $(CROSS_COMPILER)gcc
#编译选项,指定编译时的头文件路径
export CFLAGS = -I$(HEAD_PATH) -Wall
#指定链接器
export LD = ld
#指定链接器选项
export LDFLAGS = 
#终极目标 (后面表示包括子目录的所有.o)

.PHONY: all clean $(SUB_DIR)

all:$(TGT)
$(TGT): $(SUB_DIR)
        $(CC) $(CFLAGS)  $(^:=/$(SUB_TGT)) -o $@

#下面规则说明进入到生成test所需要依赖的子目录
#-C选项,可以让make进入到后面指定的目录
$(SUB_DIR): 
        make -C $@ 

clean:
        -rm -f $(TGT)
        for dir in $(SUB_DIR); do \
        make -C $$dir clean;    \
        done

mk-main:

SRCS = main.c
SUB_DIR =

all:$(SUB_TGT)

.PHONY: $(SUB_TGT) $(SUB_DIR) clean
#下面的规则说明,如何生成当前目录下的子目标(是由当前目录下的.c生成的.o和当前下的
子目录下的子目标临时打包生成的)
$(SUB_TGT): $(SRCS:.c=.o) $(SUB_DIR)
        $(LD) $(LDFLAGS)  $(SRCS:.c=.o) $(SUB_DIR:=/$(SUB_TGT)) \
        -r -o $@

%.o: %.c
        $(CC) $(CFLAGS) $< -c

%.d:  %.c
        $(CC) $(CFLAGS) $< -MM > $@

#表明main.o编译会关系到main.d, 而main.d又关联到main.c和common.h,所以只要main.c所引用到的头文件有所修改,都会重新编译main.o和main.d
ifneq ($(MAKECMDGOALS), clean)
sinclude $(SRCS:.c=.d)
endif

$(SUB_DIR):
        make -C $@
clean:
        -rm -f *.o  *.d  
        for dir in $(SUB_DIR); do  \
        make -C $$dir clean;  \
        done

mk-tst:

SRCS = tst.c
SUB_DIR = foo

all:$(SUB_TGT)

.PHONY: $(SUB_TGT) $(SUB_DIR) clean
#下面的规则说明,如何生成当前目录下的子目标(是由当前目录下的.c生成的.o和当前下的
子目录下的子目标临时打包生成的)
$(SUB_TGT): $(SRCS:.c=.o) $(SUB_DIR)
        $(LD) $(LDFLAGS)  $(SRCS:.c=.o) $(SUB_DIR:=/$(SUB_TGT)) \
        -r -o $@

%.o: %.c
        $(CC) $(CFLAGS) $< -c

%.d:  %.c
        $(CC) $(CFLAGS) $< -MM > $@

#表明tst.o编译会关系到tst.d, 而tst.d又关联到tst.c和common.h,所以只要tst.c所引用到的头文件有所修改,都会重新编译tst.o和tst.d
ifneq ($(MAKECMDGOALS), clean)
sinclude $(SRCS:.c=.d)
endif

$(SUB_DIR):
        make -C $@

clean:
        -rm -f *.o  *.d  
        for dir in $(SUB_DIR); do  \
        make -C $$dir clean;  \
        done

foo目录与main目录一样,没有子文件夹,所以mk-foo的Makefile与mk-main一致。

其编译过程大略如下: foo目录编译生成bulit_in.o, 与 tst目录下的tst.o 打包生成 bulit_in.o,与 main目录编译生成的bulit_in.o链接得到最终目标文件test。

  • top Makefile:进入各子目录下执行make命令,将各个子目录下的.o文件链接生成可执行文件
  • 子Makefile:将当前目录下的.c文件编译生成.o文件

3 Makefile中的一些知识点

3.1 常见gcc命令

gcc是GNU compiler collection 的缩写,注意gcc是一个编译器集合,gcc工作时需要binutils配合。

gcc调用binutils工具集:

命令 等价命令 用途
-S cc1 仅编译,不进行汇编、链接 编译
-c as (binutils工具集) 汇编
-o ld (binutils工具集) 链接

gcc常用命令:

选项 用途
-E 只进行预处理,不进行编译、汇编、链接
-D 使用-D name[=definition]预定义名为name的宏
-l(小L) 使用-l libname或者-llibname,使链接器在链接时搜索名为libname.a/libname.so(静态/动态)的库文件
-L 使用-Ldir添加搜索目录,即链接器在搜索-l选项指定的库文件时,除了系统的库目录还会(优先)在-L指定的目录下搜索
-I(大写的i) 使用-I dir,将目录dir添加为头文件搜索目录
-include 使用-include file,等效于在被编译的源文件开头添加#include "file"
-static 指定静态链接(默认是动态链接)
-O0~3 开启编译器优化,-O0为不优化,-O3为最高级别的优化
-Os 优化生成代码的尺寸,使能所有-O2的优化选项,除了那些让代码体积变大的
-Og 优化调试体验,在保留调试信息的同时保持快速的编译,对于生成可调试代码,比-O0更合适,不会禁用调试信息。
-Wall 使编译器输出所有的警告信息
-march 指定目标平台的体系结构,如-march=rv32imafdc,常用于交叉编译
-mtune 指定目标平台的CPU以便GCC优化,如-mtune=nuclei-300-series,常用于交叉编译
-M 生成文件关联的信息。包含目标文件所依赖的所有源代码
-MM 生成文件关联的信息。
-MMD 和-MM相同,但是输出将导入到.d的文件里面

make常用命令:

(通过make -h可查看全部指令,这里仅列出2个常用的)

选项 用途
make -f filename 执行指定的Makefile或其他文件名
make -C DIRECTORY 跳到指定目录执行Makefile

3.2 Makefile中变量

变量的赋值方式:

赋值方式 作用
= 延迟赋值 (变量的值是整个makefile中最后被指定的值)
:= 立即赋值(赋予当前位置的值,不受后面值的影响 )
?= 条件赋值(如果之前有赋值,则不会赋值; 否则采用此条赋值)
+= 追加赋值(拼接,以空格隔开,Makefile中变量类型是字符串类型)

注意:?= 与 +=也是默认延迟赋值。

可以结合实例进行理解,如下:

  • 延迟赋值 =

例如:

# test =
A = 2233
B = ${A}
A = 7788

all:
        @echo "test ="
        @echo A = $A, B = $B,

结果为:

test =
A = 7788, B = 7788,

“=”是最普通的等号,然而在Makefile中确实最容易搞错的赋值等号,使用”=”进行赋值,变量的值是整个makefile中最后被指定的值。

  • 立即赋值 :=

类似上述例子,将=换为:=

# test :=
A = 2233
B := ${A}
A = 7788

all:
        @echo "test :="
        @echo A = $A, B = $B,

结果为:

test :=
A = 7788, B = 2233,

”:=”就表示直接赋值,赋予当前位置的值,不受后面值的影响。

  • 条件赋值 ?=

例如:

# test ?=
A = 2233
A ?= 7788

all:
        @echo "test :="
        @echo A = $A,

结果为:

test :=
A = 2233,

“?=”表示如果该变量没有被赋值,则赋予等号后的值。

怎么理解?= 也是默认延迟赋值呢?

同样例如:

# test ?=
A ?= 7788_${B}
B = 'BBBB'

all:
        @echo "test ?="
        @echo A = $A,

结果为:

test ?=
A = 7788_BBBB,
  • 追加赋值 += (以空格隔开)

例如:

# test +=
A = 2233
A += 7788_${B}
B = 'BBBB'

all:
        @echo "test +="
        @echo A = $A,

结果为:

test +=
A = 2233 7788_BBBB,

可见+= 也是默认延迟赋值属性。

但是修改上述例子:

# test +=
A := 2233  # 将A = 2233改为A := 2233, += 好像失去了延迟赋值的属性了
A += 7788_${B}
B = 'BBBB'
all:
        @echo "test +="
        @echo A = $A,

结果为:

test +=
A = 2233 7788_,

通过对比,可大概了解这些算符的差别。(PS: 我无力吐槽,为啥设计出这些令人迷惑的东西?)

Makefile中的特殊变量:

特殊变量 作用
$@ 当前规则的目标
$^ 依赖列表(所有依赖)
$< 第一个依赖
$$ 当前执行的进程的进程编号
$* 模式规则中所有%匹配的部分
$? 模式规则中所有比所在规则中的目标更新文件组成的列表

代表命令的变量:

Makefile书写中,有一些书写约定。比如:与编译器相关的一些命令,可以用变量来表示,其好处是:当换编译工具等,可以仅改变变量(相当于C语言的define的作用),约定并不是强制规则,但是按照约定会给他人阅读你的代码带来方便。

常见约定的变量如下表所示:

变量 含义
CC C编译程序。默认是"cc"
CXX C++编译程序。默认是"g++"
CPP C/C++预处理器。默认是"$(CC) -E"
AR 函数库打包程序,可创建静态库.a文档。默认是"ar"。
AS 汇编程序。默认是"as“
CFLAGS C编译程序的命令行参数
CXXFLAGS C++编译程序的命令行参数
CPPFLAGS C/C++预处理器的命令行参数
ARFLAGS 函数库打包程序的命令行参数。默认值是"rv"
ASFLAGS 汇编程序的命令行参数
LDFLAGS 链接器的命令行参数

一些常用shell命令(如cp ls等)可以直接在Makefile中使用。

Makefile环境变量:

只列几个常用的变量

变量 含义
MAKEFILES make执行时首先将此变量的值作为需要读入的Makefile文件,多个文件之间使用空格分开。
MAKEFILES_LIST make 程序在读取多个 makefile 文件时,读取的文件名将会被自动依次追加到变量MAKEFILE_LIST中。
MAKECMDGOALS 代表了make执行的终极目标
CURDIR 此变量代表 make 的工作目录。当使用“-C”选项进入一个子目录后,此变量将被重新赋值。
.SUFFIXES 可识别的后缀

如下是MAKECMDGOALS应用例子(见2.2节)

意味着,如果最终目标是clean,需要执行sinclude语句。

#表明main.o编译会关系到main.d, 而main.d又关联到main.c和common.h,所以只要main.c所引用到的头文件有所修改,都会重新编译main.o和main.d
ifneq ($(MAKECMDGOALS), clean)
sinclude $(SRCS:.c=.d)
endif

clean:

3.2 Makefile中函数列表

文本处理函数

函数名 作用
$(subst FROM,TO,TEXT) 字符串替换函数:把字串“TEXT”中的“FROM”字符替换为“TO”
$(patsubst PATTERN,REPLACEMENT,TEXT) 支持通配符的字符串替换函数
$(strip STRINT) 去掉字串“STRINT”开头和结尾的空字符,并将其中多个连续空字符合并为一个空字符。
$(findstring FIND,IN) 查找字符串函数,如果在“IN”之中存在“FIND”,则返回“FIND”,否则返回空。
$(filter PATTERN…,TEXT) 过滤函数,空格分割的“TEXT”字串中所有符合模式“PATTERN”的字串
$(filter-out PATTERN...,TEXT) 反过滤函数,和“filter”函数实现的功能相反
$(sort LIST) 排序函数,给字串“LIST”中的单词升序排列,并去掉重复的单词
$(word N,TEXT) 取字串“TEXT”中第“N”个单词(“N”的值从 1 开始)
$(wordlist S,E,TEXT) 从字串“TEXT”中取出从“S”开始到“E”的单词串。“S”和“E” 表示单词在字串中位置的数字
$(words TEXT) 统计单词数目函数
$(firstword NAMES…) 取首单词函数,等效于$(word 1 , NAMES…)

文件名处理函数

函数名 作用
$(dir NAMES…) 取目录函数-从文件名序列“NAMES…”中取出各个文件名的目录部分
$(notdir NAMES…) 取文件名函数-文件名序列“NAMES…”中每一个文件的非目录部分
$(suffix NAMES…) 取后缀函数
$(basename NAMES…) 取前缀函数
$(addsuffix SUFFIX,NAMES…) 加后缀函数
$(addprefix PREFIX,NAMES…) 加前缀函数
$(join LIST1 ,LIST2) 将字串“LIST1”和字串“LIST2”各单词进行对应连接
$(wildcard PATTERN) 获取匹配模式文件名函数,列出当前目录下所有符合模式“PATTERN”格式的文件名。支持通配符

流程相关函数

函数名 作用
$(foreach VAR,LIST,TEXT) 类似于 for VAR in LIST:TEXT
$(if CONDITION,THEN-PART[,ELSE-PART]) 类似于 CONDITION?THEN-PART:ELSE-PART
$(call VARIABLE,PARAM1,PARAM2,...) call”函数是唯一一个可以创建定制化参数函数的引用函数,VARIABLE表达式中的$(1),$(2),$(3)等,会被参数< PARAM1>;,;,依次替代
$(value VARIABLE) 不对变量“VARIBLE”进行任何展开操作,直接返回变量“VARIBALE” 的值。
$(eval VARIABLE) 根据其参数的关系、 结构,对它们进行替换展开。经常搭配call函数使用,见参考2
$(origin VARIABLE) 获取此变量(参数)相关的信息,告诉我们这个变量的定义方式
shell函数 函数“shell”的参数(一个 shell 命令)在 shell 环境中的执行结果

3.3 Makefile中模式规则

Makefile中的隐含规则:

如果一个目标文件在Makefile中没有重建它的明确规则,但make时依旧正确运行,有可能时make用到了隐含规则来重建它。

可以使用make -p打印出make的所有隐含规则,调用顺序:显示规则 > 隐含规则 > 否则报错

规则中的模式替换:

格式为:

< targets …>: < target-pattern>: < prereq-patterns …>
	<commands>

<targets …>:指定一个或多个目标文件,可使用通配符。

<target-pattern...>:指定 <targets …>目标文件的模式,如%.o,表示<targets>集合中都是以.o结尾的文件。

<prereq-patterns …>:指定<targets …>目标文件依赖的文件的模式,如%.c ,表示 <targets …>集合中的目标文件的依赖文件都是以.c结尾的文件

例子:

TARGET = main.o hello.o test.o
all:$(TARGET)
$(TARGET):%.o:%.c
	gcc -c $< -o $@

这个模式规则指明了如何由%.c 来创建%.o,属于makefile中的隐含规则。

3.4 Makefile中的变量(字符串)替换

  1. 后缀字符串替换,将字符串中的后缀字符(串)使用指定的字符(串)进行替代。

    格式为 $(var:a=b)

    意思是:将var表达式中以空格分开所有的子串,以a结尾的字符替换为b。

    VAR := acc bcc ccd
    NEW := $(VAR:cc=aa)
    test:
    	@echo "new is $(NEW)"
    

    make test后结果为“new is aaa baa ccd”,通过例子可以看出这种方法只能处理后缀字符串替换。

    NEW := $(VAR:=aa) 
    

    则结果为“new is accaa bccaa ccdaa”,这种方式可以在变量后添加新的字符串。

  2. 变量中的模式替代

    使用%来匹配模式,%匹配的保留字符,其它为替代字符,较第一种方法更为通用。

    格式为$(var:a%b=x%y)

    VAR := a.c b.c c.c
    NEW := $(VAR:%.c=%.o)
    test:
    	@echo "new is $(NEW)"
    

    make test后结果为“new is a.o b.o c.o”

  3. 模式替代函数

    格式为 $(patsubst pattern, replacement, text)

    搜索text中以空格分开的单词,将符合pattern模式替换为replacement,pattern和replacement支持%通配符。

    VAR := a.c b.c c.c
    NEW := $(patsubst %.c, %.o, $(VAR))
    test:
    	@echo "new is $(NEW)"
    

    make test结果同样为“new is a.o b.o c.o”

    这几种方法得到的效果是相同的:

    $(patsubst %.c, %.o, $(VAR))
    
    $(VAR:%.c=%.o)
    
    $(VAR:.c=.a)
    

3.5 Makefile中的PHONY关键字

Makefile中.PHONY关键字修饰的目标被称之为伪目标。其作用如下:

  1. 避免目标名与文件名重名。用.PHONY修饰后告诉make 目的为了执行执行一些列命令,而不需要创建这个目标。

    如上节例子中的clean,用.PHONY修饰后,无论在当前目录下是否存在“clean”这个文件。我们输入“make clean”之后。“rm”命令都会被执行。而且当一个目标被声明为伪目标后,make 在执行此规则时不会去试图去查找隐含规则来创建它。这样也提高了 make 的执行效率。

  2. 伪目标的另外一种使用场合是在 make 的并行和递归执行过程中。

    并行:

    # 写法1:
    SUBDIRS = foo bar baz
    subdirs:
        for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir; \
        done
    
    # 写法2:这种写法出错更容易定位,且利用了make的并行处理功能。
    SUBDIRS = foo bar baz
    .PHONY: subdirs $(SUBDIRS)
    subdirs: $(SUBDIRS)
        $(SUBDIRS):
        $(MAKE) -C $@
        foo: baz
    

    递归:

    .PHONY: cleanall cleanobj cleandiff
    cleanall : cleanobj cleandiff
    	rm program
    cleanobj :
    	rm *.o
    cleandiff :
    	rm *.diff
    

    当一个伪目标作为另外一个伪目标依赖时,就成了必须执行的部分,如同cleanall调用了cleanobj与cleandiff。

3.6 Makefile 中 echo 和@echo的区别

echo: 会在shell中显示echo这条命令和后面要输出的内容
@echo: 不会显示echo这条命令,只会显示后面要输出的内容

例如:

echo “hello world”  输出为:
echo "hello world"
hello world

@echo "hello world" 输出为:
hello world

3.7 在Makefile打印错误或警告信息

# 在makefile中打印警告或者错误消息的方法:
$(warning xxxxx) 
# 或者
$(error xxxxx) 
# 输出变量方式为:
$(warning $(XXX)) 

参考:

  1. Makefile中文手册

  2. makefile eval函数详解

  3. Makefile视频教程

posted @ 2022-08-25 07:15  sureZ_ok  阅读(482)  评论(0编辑  收藏  举报