ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Makefile的使用

2022-08-25 07:30:47  阅读:194  来源: 互联网

标签:SUB make Makefile echo 编译 使用 test


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中使用。

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视频教程

标签:SUB,make,Makefile,echo,编译,使用,test
来源: https://www.cnblogs.com/sureZ-learning/p/16622972.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有