首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Linux系统编程:(九)从零开始,手把手教你学会自动化构建 - make/Makefile

Linux系统编程:(九)从零开始,手把手教你学会自动化构建 - make/Makefile

作者头像
_OP_CHEN
发布2026-01-14 11:09:19
发布2026-01-14 11:09:19
410
举报
文章被收录于专栏:C++C++

前言

在 Linux 开发的世界里,当我们面对一个包含数十个甚至上百个源文件的大型项目时,如果还靠手动敲入gcc命令来编译每个文件,不仅效率低下,还极易因为依赖关系出错导致编译失败。这时候,make/Makefile就成了我们的 “救星”—— 它能实现项目的自动化构建,只需一条make命令,就能让整个工程按照预定的规则完成编译、链接,甚至清理操作。今天,我们就来深入聊聊 Linux 下的自动化构建工具 make/Makefile,从基础概念到高级语法,带你彻底搞懂这个开发必备的工具。下面就让我们正式开始吧!


一、为什么需要 make/Makefile?

会不会写 Makefile,从一个侧面反映了一个开发者是否具备完成大型工程的能力。在讲解具体用法之前,我们先搞清楚:为什么我们需要 Makefile?

1.1 手动编译的痛点

假设我们有一个简单的 C 语言程序myproc.c,手动编译只需要一行命令:

代码语言:javascript
复制
#include <stdio.h>
int main()
{
    printf("hello Makefile!\n");
    return 0;
}

编译指令:gcc -o myproc myproc.c,执行后生成可执行文件myproc,看起来很简单。

但如果项目变得复杂呢?比如一个项目包含a.cb.cc.c三个源文件,且a.c依赖b.hb.c依赖c.h。手动编译时,我们需要依次执行:

代码语言:javascript
复制
gcc -c a.c -o a.o
gcc -c b.c -o b.o
gcc -c c.c -o c.o
gcc a.o b.o c.o -o myapp

一旦某个文件被修改,我们还得记住重新编译对应的文件,再重新链接。如果源文件数量达到几十个,手动编译的工作量和出错概率会呈指数级增长。

1.2 Makefile 的核心价值

Makefile 的本质是定义一套编译规则,它会告诉make命令:

  1. 项目的最终目标文件是什么;
  2. 每个目标文件依赖哪些源文件;
  3. 如何从依赖文件生成目标文件。

一旦写好 Makefile,我们只需在终端输入makemake命令就会自动解析 Makefile 中的规则,检查文件的修改时间,只重新编译被修改过的文件,最终生成可执行程序。而make clean则能一键清理编译生成的中间文件和可执行文件,让项目目录保持整洁。

简单来说,Makefile 带来的最大好处就是自动化编译,它把开发者从繁琐的编译命令中解放出来,专注于代码本身。

二、make/Makefile 的基础概念

在正式写 Makefile 之前,我们需要先理解几个核心概念:makeMakefile依赖关系依赖方法

2.1 make 与 Makefile 的关系

  • make:是一个命令工具,负责解析 Makefile 中的指令,执行编译、链接等操作;
  • Makefile:是一个文本文件,定义了项目的构建规则,包括目标、依赖和执行命令。

两者是工具与配置文件的关系,就像git命令与.gitignore文件的关系一样 ——make是执行者,Makefile 是指挥者。

2.2 依赖关系与依赖方法

这是 Makefile 中最核心的两个概念,我们用一个生活中的例子来理解:月底向父母要生活费

  • 依赖关系:你能拿到生活费,依赖于 “月底” 这个时间点,以及父母的经济支持;
  • 依赖方法:你需要给父母发消息、打电话,才能拿到生活费。

对应到编程中,比如生成可执行文件myproc

  • 依赖关系myproc依赖于myproc.c这个源文件;
  • 依赖方法:通过gcc -o myproc myproc.c命令,从myproc.c生成myproc

再举个例子,生成目标文件myproc.o

  • 依赖关系myproc.o依赖于myproc.s汇编文件;
  • 依赖方法gcc -c myproc.s -o myproc.o

Makefile 的每一条规则,都是由目标依赖命令三部分组成,格式如下:

代码语言:javascript
复制
目标: 依赖文件列表
    执行命令(必须以Tab键开头)

这里要注意:命令行必须以 Tab 键开头,这是 Makefile 的语法要求,很多初学者因为用了空格而导致make命令报错,一定要记住!

三、Makefile 的基本使用

理解了核心概念后,我们从最简单的例子开始,一步步写一个 Makefile,感受自动化构建的魅力。

3.1 第一个 Makefile:编译单个源文件

我们以之前的myproc.c为例,编写第一个 Makefile:

代码语言:javascript
复制
# 定义目标myproc,依赖于myproc.c
myproc: myproc.c
    gcc -o myproc myproc.c  # 依赖方法:编译命令

# 定义伪目标clean,用于清理编译产物
.PHONY:clean
clean:
    rm -f myproc  # 清理可执行文件
3.1.1 执行 Makefile

在终端中进入 Makefile 所在目录,执行make命令:

代码语言:javascript
复制
$ make
gcc -o myproc myproc.c

make会找到 Makefile 中的第一个目标myproc,检查myproc是否存在,以及myproc.c的修改时间是否比myproc新。如果myproc不存在,或者myproc.c被修改过,就执行后面的编译命令。

执行make clean则会清理可执行文件:

代码语言:javascript
复制
$ make clean
rm -f myproc
3.1.2 伪目标.PHONY

为什么要给clean加上.PHONY?这涉及到 Linux 文件的时间属性

在 Linux 中,每个文件都有三个时间属性:

  • Access (atime):文件最近一次被访问的时间;
  • Modify (mtime):文件内容被修改的时间;
  • Change (ctime):文件属性(如权限、所有者)被修改的时间。

make命令判断是否需要重新编译,依据的是目标文件和依赖文件的 mtime 对比:如果依赖文件的 mtime 比目标文件新,就重新编译。

如果当前目录下恰好有一个名为clean的文件,那么执行make clean时,make会认为clean是一个普通目标,检查其依赖(这里没有依赖),发现clean文件已经存在且没有更新,就不会执行rm -f myproc命令。

.PHONY:clean会将clean声明为伪目标,伪目标的特性是总是被执行,无论是否存在同名文件,make clean都会执行对应的清理命令。

我们也可以将myproc声明为伪目标测试一下:

代码语言:javascript
复制
.PHONY:myproc clean
myproc: myproc.c
    gcc -o myproc myproc.c
clean:
    rm -f myproc

此时每次执行make,即使myproc.c没有被修改,make也会重新编译,因为myproc变成了伪目标。

3.2 项目清理的重要性

在开发过程中,我们经常需要重新编译项目,而编译生成的.o目标文件、可执行文件会占用磁盘空间,也可能因为残留的旧文件导致编译错误。因此,Makefile 中几乎都会定义clean伪目标,用于清理这些产物。

常见的清理命令包括:

代码语言:javascript
复制
clean:
    rm -f *.o myproc  # 清理所有.o文件和可执行文件

如果项目中有预处理生成的.i文件、汇编生成的.s文件,也可以一起清理:

代码语言:javascript
复制
clean:
    rm -f *.i *.s *.o myproc

四、Makefile 的推导过程:从源文件到可执行文件

我们知道,gcc 编译 C 程序分为四个阶段:预处理编译汇编链接。对应的命令分别是:

  1. 预处理gcc -E myproc.c -o myproc.i
  2. 编译gcc -S myproc.i -o myproc.s
  3. 汇编gcc -c myproc.s -o myproc.o
  4. 链接gcc myproc.o -o myproc

Makefile 的强大之处在于,它能自动推导依赖关系和命令,我们不需要手动写出每一个阶段的规则。

4.1 手动编写全阶段 Makefile

先看一个手动编写的、包含所有编译阶段的 Makefile:

代码语言:javascript
复制
# 最终目标:myproc
myproc: myproc.o
    gcc myproc.o -o myproc

# 汇编阶段:生成myproc.o
myproc.o: myproc.s
    gcc -c myproc.s -o myproc.o

# 编译阶段:生成myproc.s
myproc.s: myproc.i
    gcc -S myproc.i -o myproc.s

# 预处理阶段:生成myproc.i
myproc.i: myproc.c
    gcc -E myproc.c -o myproc.i

# 伪目标:清理所有产物
.PHONY:clean
clean:
    rm -f *.i *.s *.o myproc

执行make时,make会按照堆栈式的依赖推导来执行:

  1. 首先找第一个目标myproc,发现它依赖myproc.o
  2. 检查myproc.o是否存在,若不存在,找myproc.o的依赖myproc.s
  3. 检查myproc.s是否存在,若不存在,找myproc.s的依赖myproc.i
  4. 检查myproc.i是否存在,若不存在,找myproc.i的依赖myproc.c
  5. myproc.c是源文件,存在,执行预处理命令生成myproc.i
  6. 回到myproc.s的规则,执行编译命令生成myproc.s
  7. 回到myproc.o的规则,执行汇编命令生成myproc.o
  8. 回到myproc的规则,执行链接命令生成可执行文件myproc

这个过程就像 “剥洋葱”,从最终目标一层层找到最底层的源文件,再从源文件一步步生成最终目标。

4.2 Makefile 的自动推导(隐式规则)

实际上,我们不需要手动写出每个阶段的规则,因为make隐式规则(也叫自动推导规则)。比如,make知道.c文件可以生成.o文件,默认使用gcc -c xxx.c -o xxx.o命令;.o文件可以生成可执行文件,默认使用gcc xxx.o -o xxx命令。

因此,我们只需要写最简单的规则,make会自动补全其他步骤:

代码语言:javascript
复制
myproc: myproc.o
    gcc myproc.o -o myproc

myproc.o: myproc.c  # 这里可以省略,make会自动推导

.PHONY:clean
clean:
    rm -f *.o myproc

甚至可以简化为:

代码语言:javascript
复制
myproc: myproc.c
    gcc -o myproc myproc.c

.PHONY:clean
clean:
    rm -f myproc

这就是我们最开始写的 Makefile,make会自动处理中间的预处理、编译、汇编步骤。

4.3 make 的工作流程总结

默认方式下,输入make命令后,make的工作流程是:

  1. 在当前目录下查找名为Makefilemakefile的文件;
  2. 找到后,读取文件中的第一个目标(称为 “终极目标”);
  3. 检查终极目标是否存在,或其依赖文件的修改时间是否更新;
  4. 如果需要生成终极目标,检查其依赖文件的依赖关系,逐层推导,直到找到源文件;
  5. 执行每一层的命令,生成中间文件,最终生成终极目标;
  6. 如果推导过程中发现依赖文件不存在,make直接报错退出;如果命令执行失败(如语法错误),make会忽略错误,继续执行后续规则(除非设置了-k参数)。

五、Makefile 的进阶语法:变量、函数与模式规则

当项目的源文件越来越多时,手动写每个文件的规则会变得繁琐。Makefile 提供了变量函数模式规则等进阶语法,让我们能更灵活地编写构建规则。

5.1 变量的定义与使用

Makefile 中的变量类似于 Shell 脚本中的变量,用于存储重复使用的内容,比如编译器、源文件列表、编译选项等。变量的定义格式为:变量名=值,使用时用(变量名)或{变量名}。

5.1.1 基本变量示例

我们用变量重构之前的 Makefile:

代码语言:javascript
复制
# 定义变量:编译器
CC=gcc
# 定义变量:可执行文件名称
BIN=myproc
# 定义变量:源文件列表
SRC=myproc.c
# 定义变量:目标文件列表(将.c替换为.o)
OBJ=$(SRC:.c=.o)
# 定义变量:清理命令
RM=rm -f

# 终极目标:依赖于OBJ
$(BIN): $(OBJ)
    $(CC) -o $@ $^  # $@代表目标文件,$^代表所有依赖文件

# 模式规则:将所有.c文件编译为.o文件
%.o: %.c
    $(CC) -c $< -o $@  # $<代表第一个依赖文件

# 伪目标:清理
.PHONY:clean
clean:
    $(RM) $(OBJ) $(BIN)

我们来解释其中的变量和特殊符号:

  • CC=gcc:定义编译器为 gcc,后续可以通过$(CC)调用;
  • BIN=myproc:定义可执行文件名称,方便后续修改;
  • SRC=myproc.c:源文件列表,如果有多个源文件,用空格分隔(如SRC=a.c b.c c.c);
  • OBJ=$(SRC:.c=.o):将SRC中的.c后缀替换为.o,生成目标文件列表;
  • RM=rm -f:定义清理命令,避免重复写rm -f
  • $@:自动变量,代表当前规则的目标文件;
  • $^:自动变量,代表当前规则的所有依赖文件;
  • $<:自动变量,代表当前规则的第一个依赖文件。
5.1.2 自动变量详解

Makefile 中的自动变量是简化规则的关键,常用的自动变量有:

自动变量

含义

$@

目标文件的名称

$^

所有依赖文件的列表,以空格分隔,重复的依赖只保留一次

$<

第一个依赖文件的名称

$?

所有比目标文件新的依赖文件列表

$*

匹配模式中的字符串(用于模式规则)

比如,若规则为app: a.o b.o c.o,则:

  • $@代表app
  • $^代表a.o b.o c.o
  • 如果a.oapp新,$?代表a.o

5.2 函数的使用

Makefile 提供了丰富的函数,用于处理字符串、文件列表等。常用的函数有wildcardpatsubst等。

5.2.1 wildcard函数:获取文件列表

wildcard函数用于匹配指定模式的文件,返回符合条件的文件列表。格式:$(wildcard 模式)

比如,获取当前目录下所有的.c文件:

代码语言:javascript
复制
SRC=$(wildcard *.c)  # 等价于手动写SRC=a.c b.c c.c(如果有这些文件)
5.2.2 patsubst函数:字符串替换

patsubst函数用于将字符串中的指定模式替换为新的模式。格式:$(patsubst 原模式, 新模式, 字符串)

比如,将.c文件列表替换为.o文件列表:

代码语言:javascript
复制
SRC=$(wildcard *.c)
OBJ=$(patsubst %.c, %.o, $(SRC))  # 等价于$(SRC:.c=.o)
5.2.3 函数结合使用示例

我们用函数重构 Makefile,使其能自动适配任意数量的.c文件:

代码语言:javascript
复制
# 编译器
CC=gcc
# 可执行文件名称
BIN=proc.exe
# 获取当前目录下所有.c文件
SRC=$(wildcard *.c)
# 将.c文件替换为.o文件
OBJ=$(patsubst %.c, %.o, $(SRC))
# 链接选项
LFLAGS=-o
# 编译选项
FLAGS=-c
# 清理命令
RM=rm -f

# 终极目标:依赖于所有.o文件
$(BIN): $(OBJ)
    $(CC) $(LFLAGS) $@ $^  # 链接命令:gcc -o proc.exe a.o b.o c.o
    @echo "链接完成:$^ → $@"  # @表示不回显命令本身

# 模式规则:编译所有.c文件为.o文件
%.o: %.c
    $(CC) $(FLAGS) $< -o $@  # 编译命令:gcc -c a.c -o a.o
    @echo "编译完成:$< → $@"

# 伪目标:清理
.PHONY:clean
clean:
    $(RM) $(OBJ) $(BIN)
    @echo "清理完成"

# 伪目标:测试变量(可选)
.PHONY:test
test:
    @echo "源文件列表:$(SRC)"
    @echo "目标文件列表:$(OBJ)"

执行make test可以查看变量的值:

代码语言:javascript
复制
$ make test
源文件列表:myproc.c
目标文件列表:myproc.o

执行make则会自动编译并链接:

代码语言:javascript
复制
$ make
gcc -c myproc.c -o myproc.o
编译完成:myproc.c → myproc.o
gcc -o proc.exe myproc.o
链接完成:myproc.o → proc.exe

5.3 模式规则

模式规则是一种通用的规则,用于匹配一组文件的构建。格式为:%.目标后缀: %.依赖后缀,其中%是通配符,匹配任意字符串。

比如,我们之前写的%.o: %.c就是一个模式规则,它表示:所有.o文件依赖于同名的.c文件,编译命令为gcc -c < -o

模式规则的优势在于,不需要为每个.c文件单独写规则,make会自动匹配所有符合条件的文件。

5.4 注释与回显控制

  • 注释:Makefile 中用#表示注释,从#开始到行尾的内容都会被忽略;
  • 回显控制:命令前加@make执行时不会输出命令本身,只输出命令的执行结果

比如:

代码语言:javascript
复制
test:
    @echo "Hello Makefile"  # 只输出"Hello Makefile",不输出"echo "Hello Makefile""
    echo "Hello Makefile"   # 会输出"echo "Hello Makefile""和"Hello Makefile"

总结

make/Makefile 是 Linux 开发中不可或缺的工具,它通过定义依赖关系构建规则,实现了项目的自动化编译,极大地提高了开发效率。掌握 make/Makefile 不仅能让我们更高效地开发 Linux 项目,也是理解大型工程构建流程的关键。希望本文能帮助你彻底搞懂 Makefile,让自动化构建成为你的开发利器!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、为什么需要 make/Makefile?
    • 1.1 手动编译的痛点
    • 1.2 Makefile 的核心价值
  • 二、make/Makefile 的基础概念
    • 2.1 make 与 Makefile 的关系
    • 2.2 依赖关系与依赖方法
  • 三、Makefile 的基本使用
    • 3.1 第一个 Makefile:编译单个源文件
      • 3.1.1 执行 Makefile
      • 3.1.2 伪目标.PHONY
    • 3.2 项目清理的重要性
  • 四、Makefile 的推导过程:从源文件到可执行文件
    • 4.1 手动编写全阶段 Makefile
    • 4.2 Makefile 的自动推导(隐式规则)
    • 4.3 make 的工作流程总结
  • 五、Makefile 的进阶语法:变量、函数与模式规则
    • 5.1 变量的定义与使用
      • 5.1.1 基本变量示例
      • 5.1.2 自动变量详解
    • 5.2 函数的使用
      • 5.2.1 wildcard函数:获取文件列表
      • 5.2.2 patsubst函数:字符串替换
      • 5.2.3 函数结合使用示例
    • 5.3 模式规则
    • 5.4 注释与回显控制
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档