参考文档:https://www.gnu.org/software/make/manual/make.html
1、Makefile的引入及规则
核心问题:为什么需要 Makefile?
图片中提到的问题非常关键:"使用 Keil、MDK、AVR 等 IDE 时点点鼠标就可以编译,它的内部机制是什么?" 这些 IDE 的背后,其实就是一个自动化构建系统。它帮你管理了:
-
哪些文件需要编译。
-
编译的顺序(如先编译哪个模块)。
-
文件之间的依赖关系 (例如,
main.c修改了,需要重新编译main.o,并重新链接;而utils.c没改,就不用重新编译utils.o)。
当我们脱离 IDE,在 Linux 环境下使用命令行编译时,就需要一个工具来扮演这个"自动化构建系统"的角色。这就是 make工具和 Makefile。
从简单到复杂
图片中给出的 gcc -o test a.c b.c命令虽然简单,但暴露了问题:
-
效率低下:每次编译都需要处理所有源文件,即使只改动了一个。
-
容易出错:项目庞大后,手动输入复杂的编译命令不现实。
-
依赖混乱:无法自动处理源文件与头文件之间的依赖。
Makefile 的基本规则
一个 Makefile 由一系列"规则"构成,每条规则定义了如何以及何时构建一个目标。其基本语法是:
目标: 依赖文件1 依赖文件2 ...
[TAB]命令1
[TAB]命令2
...
-
目标 :通常是要生成的文件名(如
test),也可以是一个"动作标签"(如clean)。 -
依赖 :生成这个目标所需要的文件 或其他目标。
-
命令 :一系列 shell 命令,描述如何从依赖文件生成目标。必须用 Tab 键缩进。
示例解释:
# 规则1:最终目标 test 依赖于两个 .o 文件
test: a.o b.o
gcc -o test a.o b.o
# 规则2:a.o 依赖于 a.c 和某个头文件 common.h
a.o: a.c common.h
gcc -c a.c -o a.o
# 规则3:b.o 依赖于 b.c 和 common.h
b.o: b.c common.h
gcc -c b.c -o b.o
工作原理 :当你在终端输入 make test(或直接 make)时,make工具会:
-
检查目标
test是否存在。 -
如果不存在,或者
test比它的依赖(a.o或b.o)旧 ,就执行后面的命令来重新生成test。 -
在生成
test之前,会先去检查a.o和b.o的规则,递归地保证它们也是最新的。 -
若修改了a.c,则会执行 gcc -c a.c -o a.o 和 gcc -o test a.o b.o,同理修改了b.c也是只执行对应的链接
这就是 **"增量编译"** 的核心,它极大提升了大型项目的编译效率。
2、Makefile的语法
掌握了基本规则后,我们需要更强大的语法来编写简洁、灵活的 Makefile。
a. 即时变量与延时变量
变量用于存储文本,使 Makefile 易于维护和修改。两者的核心区别在于变量值被展开(使用)的时机不同。
-
即时变量 :使用
:=定义。在定义时,其值就被立即确定。A := $(C) # 此时C的值为空 B := 100 C := 200 # A的值在定义时已确定为"空",所以最终A为空,B为100,C为200 -
延时变量 :使用
=定义。其值在被引用时才确定。A = $(C) # A的值会等到被使用时才去查找C B = 100 C = 200 D = $(A) # 此时A展开为$(C),而C的值为200,所以D=200应用:延时变量常用于定义递归展开的变量,比如工具链前缀。
CROSS_COMPILE = arm-linux-gnueabihf- CC = $(CROSS_COMPILE)gcc # CC的实际值取决于运行时CROSS_COMPILE -
?= 延时变量,如果是第一次定义才起效,如果在前面该变量已定义则忽略这句
-
+= 附加,它是即使变量还是延时变量,取决于前面的定义
-
export:用于将变量从 Makefile 导出到子make进程 。在编译 Linux 内核或复杂项目时,顶层 Makefile 通过export将架构(ARCH)、工具链(CROSS_COMPILE)等关键变量传递给子目录的 Makefile。
示例:makefile:

执行结果:

也可以执行make D=123456,则会输出D=123456
b. 通配符与模式规则
-
%.o:这是 Makefile 中最重要的通配符之一,用于模式规则 。它表示"所有以.o结尾的文件"。# 静态模式规则:告诉make,所有的 .o 文件都依赖于同名的 .c 文件 objects = a.o b.o main.o $(objects): %.o: %.c $(CC) -c $< -o $@-
$<代表第一个依赖文件(这里是%.c)。 -
$@代表目标文件(这里是%.o)。 -
$^代表所有的依赖文件(这里是%.o)。 -
这等价于为每个
.c文件写了一条编译规则,但简洁得多。
-


这两张图片表达的意义为:
第一条规则(第1-2行):定义一个生成可执行文件
test的规则。
目标 :
test依赖项 :
a.o,b.o,c.o命令 :
gcc -o test a.o b.o c.o
将a.o b.o c.o改为$^,表示 将所有依赖文件都链接到test第二条规则(第4-5行):一个模式规则,定义了如何生成
.o文件。
目标 :
%.o(匹配任何.o文件)依赖项 :
%.c(对应的.c源文件)命令 :
gcc -c -o $@ $<
$@: 代表当前规则中的目标文件名 (例如a.o)。
$<: 代表当前规则中的第一个依赖文件名 (例如a.c)。
*.c,*.o:在命令或变量中使用的 shell 通配符。
# 清除所有目标文件和可执行程序
clean:
rm -f *.o test
c. 假想目标:.PHONY
如果一个目标不是要生成一个真正的文件,而只是一个标签 (如 clean, all),那么就应该将其声明为假想目标。
.PHONY: clean all
all: test
clean:
rm -f *.o test
-

-
为什么需要? :假设你的目录里恰好有一个叫
clean的文件,当你执行make clean时,make会发现clean文件已经存在且没有依赖,就认为它"是最新的",从而拒绝执行rm命令。用.PHONY声明后,make就知道clean是一个动作指令,无论文件是否存在都会执行其命令。
5、Makefile函数
1. $(foreach var, list, text)
这是一个循环函数,用于遍历一个用空格分隔的单词列表。
-
作用 :将列表
list中的每个单词依次取出,赋值给变量var,然后执行text所描述的表达式的展开,最终将所有展开的结果用空格连接成一个字符串。 -
参数:
-
var:一个临时变量名,代表列表中的每个元素。 -
list:要遍历的列表。 -
text:对每次取出的var进行处理的表达式。
-
-
示例:
# 假设有三个目录 DIRS = src drivers include # 为每个目录名加上 "-I" 前缀,生成编译器的 include 路径选项 CFLAGS += $(foreach dir, $(DIRS), -I$(dir)) # 执行过程: # 1. dir=src => 得到 "-Isrc" # 2. dir=drivers => 得到 "-Idrivers" # 3. dir=include => 得到 "-Iinclude" # 最终结果:CFLAGS 增加了 "-Isrc -Idrivers -Iinclude"
示例:


2. $(filter pattern..., text)和 $(filter-out pattern..., text)
这是一对过滤函数,用于根据模式筛选列表中的单词。
-
$(filter pattern..., text):-
作用 :从
text中保留 所有匹配任一 给定pattern的单词。模式可以使用通配符%。 -
示例:
FILES = foo.c bar.h foo.o main.c utils.h # 只保留所有 .c 和 .h 文件 SOURCE_AND_HEADERS = $(filter %.c %.h, $(FILES)) # 结果:SOURCE_AND_HEADERS = foo.c bar.h main.c utils.h
-
-
$(filter-out pattern..., text):-
作用 :与
filter相反。从text中移除 所有匹配任一 给定pattern的单词。 -
示例:
OBJECTS = main.o utils.o lib.a helper.c helper.o # 移除所有不是 .o 结尾的文件(即保留 .o 文件) ONLY_OBJS = $(filter-out %.a %.c, $(OBJECTS)) # 结果:ONLY_OBJS = main.o utils.o helper.o # 注意:helper.c 也被移除了,因为它匹配了 %.c 模式
-


3. $(wildcard pattern)
这是一个通配符展开函数 ,用于获取与模式匹配的、实际存在于文件系统中的文件列表。
-
作用 :
pattern是一个文件名匹配模式(可包含*,?,[]等通配符)。该函数会返回当前目录下所有匹配该模式且真实存在的文件的路径列表。 -
与直接使用通配符的区别 :在 Makefile 的变量赋值或规则中,
*等通配符可能不会被自动展开。使用$(wildcard)函数是获取文件列表的安全且推荐的方法。 -
示例:
# 错误用法:这仅仅是把字符串 "*.c" 赋给变量 # SRCS = *.c # 正确用法:获取当前目录下所有存在的 .c 文件 SRCS = $(wildcard *.c) # 获取特定子目录下的 .c 文件 SRC_SUBDIR = $(wildcard src/*.c utils/*.c) # 结合 foreach 获取多个子目录的文件 ALL_SRCS = $(foreach dir, src drivers, $(wildcard $(dir)/*.c))


4.$(patsubst pattern,replacement,text)
-
pattern :要查找的模式,可以包含通配符
% -
replacement :替换的模式,也可以包含
% -
text:要处理的文本(通常是变量展开后的值)
-
示例:
-

-

-
注意:这个函数是从列表中取出每一个值,如果如何pattern,则替换为replacement
总结与核心应用场景
-
$(foreach):当你需要对一个列表中的每个元素执行相同的转换或添加操作时使用(如为目录加前缀)。 -
$(filter)/$(filter-out):当你需要从一个混合的文件列表或字符串中根据类型进行筛选或排除时使用(如分离源文件和目标文件)。 -
$(wildcard):这是你自动发现项目源文件的主要工具,使得 Makefile 无需在每次增删文件时都手动更新文件列表,极大提高了可维护性。
这三个函数组合使用,可以构建出非常灵活和强大的自动化构建规则,是编写专业级 Makefile 的基石。
4、Makefile实例
初始版 Makefile:
test: main.o utils.o
gcc -o test main.o utils.o
main.o: main.c utils.h
gcc -c main.c
utils.o: utils.c utils.h
gcc -c utils.c
clean:
rm -f *.o test
a. 改进:支持头文件依赖
可参考此文章更加详细了解:
首先,创建您描述的源文件:
-
defs.h
#ifndef _DEFS_H_ #define _DEFS_H_ #define NAME "makefile" #endif /* _DEFS_H_ */ -
main.c
#include <stdio.h> #include "defs.h" int main(int argc, char *argv[]) { printf("Hello, %s!\n", NAME); return 0; }
1. -M与 -MM选项对比
这两个选项用于仅生成依赖关系,不进行编译。
-
gcc -M main.c此命令将
main.c依赖的所有文件(包括系统头文件stdio.h及其递归包含的所有头文件)以 Makefile 规则格式输出到终端。注意 :因为
-M默认打开了-E(预处理后停止),所以不会生成main.o文件。$ gcc -M main.c main.o: main.c defs.h /usr/include/stdio.h /usr/include/stdlib.h ... (非常多行) -
gcc -MM main.c此命令与
-M类似,但过滤掉了系统头文件,只输出项目自身的头文件依赖,结果更清晰,更适合放入 Makefile。$ gcc -MM main.c main.o: main.c defs.h结论 :在生成供 Makefile 使用的依赖关系时,
-MM比-M更常用,可以避免引入大量不必要且稳定的系统库依赖。
2. -MF与 -MD/-MMD选项的应用
这些选项用于将生成的依赖关系保存到文件,是实现自动化依赖管理的关键。
-
gcc -MM -MF main.d main.c将
-MM生成的依赖规则保存到main.d文件中,同时仍只预处理,不编译。$ gcc -MM -MF main.d main.c $ cat main.d # 查看生成的文件 main.o: main.c defs.h -
gcc -c -MMD -MF main.d main.c(最常用组合)关键 :
-MMD选项与-MD类似,但它不阻止正常编译 ,并且会像-MM一样排除系统头文件。这个命令会同时完成两件事:
-
-c:编译main.c生成目标文件main.o。 -
-MMD -MF main.d:生成并保存依赖关系到main.d文件。gcc -c -MMD -MF main.d main.c ls
main.c defs.h main.d main.o # main.o 和 main.d 同时生成
$ cat main.d
main.o: main.c defs.h
在 Makefile 中,我们通常写成:
%.o: %.c $(CC) -c $(CFLAGS) $< -o $@ -MMD -MF $*.d -
3. -MP选项的重要性
这个选项为每个依赖的头文件生成一个"伪目标"(phony target)规则,防止因头文件被删除而导致的 Make 错误。
没有 -MP的情况:
$ gcc -MM -MF main.d main.c
$ cat main.d
main.o: main.c defs.h
如果此时我们不小心把 defs.h文件删除了,然后运行 make,Make 会试图去构建 main.o。它会检查到 defs.h这个依赖项不存在,又找不到任何如何构建 defs.h的规则,于是报错:make: *** No rule to make target 'defs.h', needed by 'main.o'. Stop.。
使用 -MP的情况:
$ gcc -MM -MF main.d -MP main.c
$ cat main.d
main.o: main.c defs.h
defs.h: # <- 这就是-MP生成的伪目标规则
多出来的 defs.h:规则是一个没有前置条件和命令的规则。如果 defs.h文件不存在,这条规则的目标会被认为是"已更新",从而不会导致上述构建错误,Make 会转而执行其他需要真正更新的规则。这是一个防御性编程的好习惯。
4. 一个完整的、生产环境可用的 Makefile 示例
综合以上选项,并结合您资料末尾的示例,我们可以写出一个非常健壮的 Makefile:
# 编译器定义
CC = gcc
# 编译选项:开启警告,生成调试信息,定义NAME宏(这里覆盖了defs.h的定义)
CFLAGS = -Wall -Wextra -g -DNAME=\"AutoMake\"
# 链接选项
LDFLAGS =
# 自动查找所有 .c 文件
SRCS = $(wildcard *.c)
# 将 .c 文件列表转换为对应的 .o 文件列表
OBJS = $(SRCS:.c=.o)
# 将 .c 文件列表转换为对应的 .d 依赖文件列表
DEPS = $(SRCS:.c=.d)
# 最终目标
TARGET = myapp
.PHONY: all clean
all: $(TARGET)
# 链接最终程序
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $@ $(LDFLAGS)
# 核心编译规则:一条命令同时生成 .o 文件和 .d 依赖文件
%.o: %.c
$(CC) -c $(CFLAGS) $< -o $@ -MMD -MP -MF $*.d
# 包含所有自动生成的依赖文件
# 开头的'-'表示:如果某些.d文件不存在(比如第一次编译),不要报错,继续执行
-include $(DEPS)
clean:
rm -f $(TARGET) $(OBJS) $(DEPS)
# 调试:查看变量内容
debug:
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
@echo "DEPS = $(DEPS)"
总结
您提供的资料中,最核心、最实用的组合是 -MMD -MP -MF <filename>.d。
-
-MMD:生成依赖文件(排除系统头文件),且不中断编译流程。 -
-MP:为每个头文件添加伪目标,防止头文件删除引发构建错误。 -
-MF:指定依赖文件输出名称(通常为$*.d)。
b. 添加 CFLAGS
CFLAGS(C Flags)是控制编译器行为的关键变量。在嵌入式开发中尤其重要。
CC = gcc
# 重要:添加常用的编译选项
CFLAGS = -I. -Wall -O2 -g -fPIC
-
-Wall:开启大部分警告,帮助发现代码问题。 -
-O2:启用2级优化,在性能和代码大小间取得良好平衡。 -
-g:生成调试信息,用于 GDB 调试。 -
-fPIC:生成位置无关代码,常用于编译动态库。
c. 编写裸板(或交叉编译)Makefile
这是嵌入式开发的核心。我们需要使用交叉编译工具链,为 ARM 等目标板生成程序。
假设目标板是 IMX6ULL,工具链为 arm-buildroot-linux-gnueabihf-。
关键变化:
-
CROSS_COMPILE:定义了工具链前缀,所有工具(gcc,ld等)都基于此前缀。 -
ARCH:在编译内核或U-Boot时,该变量会传递给其顶层 Makefile,决定编译哪部分架构的代码。 -
CFLAGS中的架构选项 :如-march,-mfpu, 这些是针对目标 CPU 的优化选项,对性能影响重大,需要查阅芯片手册或工具链文档进行设置。
5、通用makefile
-
顶层目录的
Makefile(配置中心)-
核心作用 :定义全局环境 和最终目标。
-
关键配置:
-
CROSS_COMPILE:指定交叉编译器前缀(如arm-linux-gnueabihf-),是实现交叉编译的关键。 -
CFLAGS:全局C编译选项(如-I./include -Wall -O2),作用于所有.c文件。 -
LDFLAGS:全局链接选项(如-lm -lpthread),用于最终链接成应用程序。 -
TARGET:最终生成的应用程序名称(如my_app)。 -
obj-y:指定根目录下 需要被编译进程序的文件(.o)和子目录(/)。
-
-
关键动作 :使用
export导出这些变量,使其在递归调用make时能传递到所有子目录。
-
-
顶层目录的
Makefile.build(构建引擎)-
核心作用 :这是一个通用、固定 的构建脚本。它负责递归地进入
obj-y指定的子目录,编译所有源文件,并将它们合并打包成该目录下的built-in.o文件。 -
工作流程(简化理解):
-
进入一个目录(如
src/)。 -
读取该目录下的
Makefile,获取本地的obj-y列表。 -
对于列表中的每一个
.o文件,根据对应的.c文件进行编译。 -
对于列表中的每一个子目录(如
subdir/),递归地调用自身Makefile.build,最终会生成该子目录下的built-in.o。 -
将本目录下编译出的所有
.o文件和子目录的built-in.o链接合并,生成本目录 的built-in.o。
-
-
最终结果 :在顶层目录会生成一个总的
built-in.o,它包含了整个项目所有已编译的代码。
-
-
各子目录的
Makefile(清单文件)-
核心作用 :极其简单,仅需声明本目录下哪些文件或子目录需要被编译。
-
内容示例:
obj-y += main.o # 编译本目录的 main.c 为 main.o obj-y += utils.o # 编译本目录的 utils.c 为 utils.o obj-y += network/ # 进入 network 子目录继续编译 -
可选设置:
-
EXTRA_CFLAGS:为本目录下所有 .c文件添加额外的编译选项(如-I../include)。 -
CFLAGS_xxx.o:为特定 的xxx.o文件添加独有的编译选项。
-
-
如何使用这套系统:六步法
第1步:放置基础文件
将 Makefile(顶层配置)和 Makefile.build(构建引擎)拷贝到项目的顶层根目录 。然后在每一个需要编译的子目录 中,创建一个内容可能为空的 Makefile。
第2步:指定要编译的内容
从顶层目录开始,编辑每一层目录的 Makefile,在 obj-y中添加条目。
-
obj-y += foo.o表示编译当前目录下的foo.c。 -
obj-y += subdir/表示进入subdir子目录,并继续使用其下的Makefile进行编译。
第3步:配置编译和链接选项
-
在顶层
Makefile中设置全局的CFLAGS(编译选项)和LDFLAGS(链接选项)。 -
在某个子目录 的
Makefile中,如需特殊设置,可以使用EXTRA_CFLAGS或CFLAGS_xxx.o。
第4步:指定工具链
修改顶层 Makefile中的 CROSS_COMPILE变量。例如,为ARM开发板编译则设置为 CROSS_COMPILE = arm-linux-gnueabihf-。
第5步:设置程序名
修改顶层 Makefile中的 TARGET变量,如 TARGET = my_program。
第6步:执行构建
在顶层目录执行命令:
-
make:开始构建,最终生成$(TARGET)指定的程序。 -
make clean:清除已编译的目标文件(.o)和built-in.o。 -
make distclean:进行彻底清理,通常会删除最终生成的可执行文件和所有构建中间文件。