前言
在 Linux 开发中,make和Makefile是提升项目构建效率的神兵利器。从单个文件的小程序到百万行代码的大型工程,掌握它们能让你从重复的编译命令中解放出来。本文将带你从零开始理解make、makefile 的工作原理,并通过实战案例掌握其应用技巧。
📚 Linux 入门篇
🔧 Linux 工具篇
目录
一、背景:为什么需要 make/Makefile?
二、核心概念:依赖关系与依赖方法
【案例】:一个简单的 C 项目
【问题】:文件名必须是makefile吗?
【问题】:如果打乱依赖关系的顺序会正常执行吗?
三、make 的工作原理:如何判断 “是否需要编译”?
【stat指令】
四、伪目标与项目清理
五、特殊符号
六、依赖关系缺失问题
七、如何在使用make后不显示依赖方法
八、make工作原理总结
想象一下,你有一个由多个.c文件组成的项目,每次修改代码后都要手动执行gcc -c file1.c、gcc -c file2.c、gcc -o target file1.o file2.o…… 这样的重复劳动不仅低效,还容易出错。
make是一个命令工具,Makefile是一个规则文件,二者配合实现了自动化编译:只需编写一次规则,后续执行make命令即可自动完成 “哪些文件需要编译、哪些需要重编译” 的判断,极大提升开发效率。
对于大型工程,会不会写 Makefile 甚至成为衡量工程师能力的一个侧面标准 —— 毕竟它能清晰管理 “文件依赖、编译顺序、清理逻辑” 等复杂场景。
makefile 的核心是 “依赖关系”和“依赖方法”:
hello依赖于目标文件hello.o,hello.o依赖于汇编文件hello.s,以此类推。我们以输出 “hello Makefile!” 的小程序为例,逐步构建 Makefile。
【步骤 1】:编写 C 代码(hello.c)
#include <stdio.h>
int main()
{
printf("hello Makefile!\n");
return 0;
}【步骤 2】:编写 Makefile 规则
一个完整的 Makefile 规则格式为:
目标文件: 依赖文件
依赖方法(编译命令,注意开头必须是Tab缩进)针对hello.c,我们可以编写多层依赖的 Makefile:
# 最终可执行文件hello,依赖于hello.o
hello: hello.o
gcc hello.o -o hello
# 目标文件hello.o,依赖于hello.s
hello.o: hello.s
gcc -c hello.s -o hello.o
# 汇编文件hello.s,依赖于hello.i
hello.s: hello.i
gcc -S hello.i -o hello.s
# 预处理文件hello.i,依赖于hello.c
hello.i: hello.c
gcc -E hello.c -o hello.i【步骤 3】:执行 make命令
在终端中输入make时,它会仅以 Makefile 中第一个定义的目标为起点,沿着该目标的完整依赖链(例如hello→hello.o→hello.s→hello.i→hello.c)执行构建。
整个过程中,make只会处理这个 “第一个目标” 及其所有依赖项(包括直接依赖和间接依赖),其他未被该依赖链关联的目标(即使在 Makefile 中定义)也不会被自动执行。
同时,它会通过比较 “目标文件” 与 “依赖文件” 的修改时间实现 “按需编译”:只有当依赖文件更新过(修改时间更新),才会重新编译对应的目标,否则直接跳过,大幅提升效率。

make是命令,makefile是一个文件,当前目录下的文件
文件名不一定必须是 Makefile,但 make 命令有默认的文件名查找规则,常用的合法文件名有两种:
Makefile(首字母大写)makefile(全小写)这两个文件名是 make 命令的默认查找对象。当在终端执行 make 时,它会优先先在当前目录下寻找这两个文件,找到后按其中的规则执行构建。
【如果想用其他文件名怎么办?】
如果你的想将构建规则文件命名为其他名称(比如 mybuild),可以通过 make 的 -f 或 --file 参数指定文件名,例如:
make -f mybuild # 告诉make使用mybuild文件作为规则文件【为什么推荐用 Makefile?】
在实际开发中,更推荐使用 Makefile(首字母大写),原因是:
总结:默认情况下,make 只认 Makefile 或 makefile,但通过 -f 参数可以指定任意文件名作为构建规则文件。
在 Makefile 中,规则的顺序不影响依赖关系的解析,因为 make 是基于 “依赖树” 的拓扑顺序来执行编译的,而非按照规则在文件中的书写顺序。
以提供的 Makefile 为例,即使将规则顺序打乱,比如:
# 汇编文件hello.s,依赖于hello.i
hello.s: hello.i
gcc -S hello.i -o hello.s
# 最终可执行文件hello,依赖于hello.o
hello: hello.o
gcc hello.o -o hello
# 预处理文件hello.i,依赖于hello.c
hello.i: hello.c
gcc -E hello.c -o hello.i
# 目标文件hello.o,依赖于hello.s
hello.o: hello.s
gcc -c hello.s -o hello.omake 依然会自动梳理依赖关系的层级:
hello,需要先有 hello.o;hello.o,需要先有 hello.s;hello.s,需要先有 hello.i;hello.i,需要先有 hello.c。最终会按照 hello.c → hello.i → hello.s → hello.o → hello 的正确顺序执行编译命令。
这是因为 make 的核心逻辑是 “先处理依赖项,再处理目标项”—— 它会递归地查找每个目标的依赖,直到找到最底层的源文件(如 hello.c),再从下往上依次执行编译。
因此,只要依赖关系的定义是正确的,规则在 Makefile 中的书写顺序可以任意调整,make 都能正确识别并按依赖层级执行构建。
make 的核心逻辑是比较 “目标文件” 和 “依赖文件” 的 “最近修改时间”:
【stat指令】 语法:
stat [选项] [文件]功能:查看文件的详细元数据(包括 ACM 时间、大小、权限、inode 等) 常用选项:
-c %y [文件]:仅查看文件的内容修改时间(Modify)-c %z [文件]:仅查看文件的属性变更时间(Change)-c %x [文件]:仅查看文件的最近访问时间(Access)
【示例1】:目标文件最近修改时间比依赖文件最近修改时间新

这个时候不会触发编译,因为test是最新的
【示例2】:目标文件最近修改时间比依赖文件最近修改时间旧

从图中可以看到重新进行编译了
【注意】:我们比较的是Modify时间,也就是文件内容最近修改的时间
在开发中,我们常需要 “清理编译生成的中间文件”,这就需要用到伪目标(用.PHONY修饰)。伪目标的特性是 “总是被执行”,不会受文件修改时间的影响。
以clean规则为例:
.PHONY: clean
clean:
rm -f hello.i hello.s hello.o hello执行make clean时,无论这些文件是否存在,都会执行rm命令清理它们,方便我们重新编译项目。
在 Makefile 中,@ 和 ^ 是自动变量,用于简化命令书写,分别代表不同的含义:
$@:表示当前规则中的目标文件(即规则中 : 左边的文件名)。$^:表示当前规则中的所有依赖文件(即规则中 : 右边的所有文件名,去重后的值)。

缺少test.s生成方式:


make后说没有规则可制作目标“test.s”,该目标是“test.o”所需要的,这个时候就会编译失败

在所有依赖方法前加上@符号后,再次make就会发现依赖方式不回显了

make 是如何工作的 , 在默认的方式下,也就是我们只输入 make 命令。
