小小de风呀头像
关注
de风——【从零开始学习Linux】(六):自动化构建 make与Makefile封面图

de风——【从零开始学习Linux】(六):自动化构建 make与Makefile

环境说明:本系列使用 Xshell 7 连接 腾讯云服务器 Ubuntu 22.04 进行操作演示。如果你还没有搭建好环境,请先阅读本系列前面的文章。


前言

在上一篇文章中,我们学会了使用 GCC 编译器编译代码。但那个例子只用了一个源文件,几行命令就能搞定。可是真实的项目是什么样的呢?

一个真实的 C/C++ 项目,源文件可能有几百甚至上千个,它们分散在不同的目录里,相互之间还存在依赖关系。如果每次都手动敲 gcc xxx.c yyy.c zzz.c -o app,不仅麻烦,而且——

  • 改一个文件要重新编译全部文件:浪费时间
  • 文件多了根本记不住依赖关系:容易出错
  • 每次编译都要手敲一长串命令:效率极低

幸好,Linux 世界早就有了解决方案——make 和 Makefile。它们能让整个项目"一键编译",是每个 Linux 程序员必须掌握的技能。

一句话总结:会不会写 Makefile,从侧面反映了一个人是否具备完成大型工程的能力。


思维导图

下面是本文内容的完整思维导图,建议先整体浏览一遍,建立全局认知后再逐节学习。


一、make 与 Makefile 简介

1.1 什么是 make 和 Makefile?

语法形式
make            # 执行 Makefile 中的第一个目标
make 目标名      # 执行指定的目标
功能

这里要先区分两个东西:

名称

是什么

作用

make

一个命令工具

解释并执行 Makefile 中的指令

Makefile

一个文件

定义项目的编译规则

它们是搭配使用的:make 是命令,Makefile 是规则文件。你在终端敲 make,make 工具就会去读当前目录下的 Makefile 文件,然后按照里面定义的规则自动编译项目。

类比理解:把 Makefile 想象成一份"菜谱",make 就是"厨师"。菜谱(Makefile)写清楚每道菜需要什么食材、怎么做;厨师(make)照着菜谱一步步操作,最后端出成品(可执行程序)。

Makefile 带来的好处:

  1. 自动化编译:一旦写好,只需一个 make 命令,整个工程完全自动编译2.
  2. 增量编译:只重新编译修改过的文件,未修改的文件不重复编译,大大节省时间
  3. 依赖管理:自动维护文件之间的依赖关系,指定谁先编译、谁后编译
  4. 跨平台:几乎所有 IDE 都支持 make,Linux 下的 GNU make 更是标配
示例

——演示 make 的基本使用流程

# 1. 先创建一个简单的 C 程序
vim myproc.c


// myproc.c
#include <stdio.h>

int main()
{
    printf("hello Makefile!\n");
    return 0;
}


# 2. 创建 Makefile 文件(注意:文件名必须是 Makefile 或 makefile)
vim Makefile


myproc:myproc.c
	gcc -o myproc myproc.c


# 3. 直接执行 make 命令
make
# 输出:
# gcc -o myproc myproc.c

# 4. 运行生成的可执行文件
./myproc
# 输出:hello Makefile!

新手注意:make 会自动在当前目录找名为 Makefile(首字母大写)或 makefile(全小写)的文件。文件名千万别写错,否则 make 会报错 make: *** No targets specified and no makefile found. Stop.


二、Makefile 的基本语法规则

Makefile 的核心就是一条条规则(Rule)。每条规则告诉 make:"要生成什么,需要什么,怎么生成"。

2.1 规则的三要素

语法形式
目标(target): 依赖(dependency)
	命令(command)

注意:命令前面必须是 Tab 键缩进(不能用空格),这是新手最容易踩的坑!

功能

一条 Makefile 规则由三部分组成:

组成部分

英文

含义

目标

target

要生成的文件(冒号左边)

依赖

dependency

生成目标所需要的文件(冒号右边)

命令

command

具体的生成动作(下一行 Tab 缩进)

规则的含义:目标 依赖于 依赖,如果 依赖 比 目标 新,就执行 命令 来生成 目标。

一句话理解:规则格式可以读作——"目标 由 依赖 生成,生成方法是执行 命令"。

示例

——演示最基本的 Makefile 规则:依赖关系与依赖方法

# Makefile 内容
myproc:myproc.c
	gcc -o myproc myproc.c
# 逐行理解这条规则:

# 【目标】myproc         —— 要生成的可执行文件
# 【依赖】myproc.c        —— 生成 myproc 需要 myproc.c 这个源文件
# 【命令】gcc -o myproc myproc.c  —— 用这条命令从 myproc.c 生成 myproc

# 执行 make
make
# make 发现 myproc 不存在(或 myproc.c 比 myproc 新),于是执行命令
# 输出:gcc -o myproc myproc.c

# 再次执行 make(此时 myproc 已经是最新的)
make
# 输出:make: 'myproc' is up to date.
# make 发现 myproc 比 myproc.c 新,不需要重新编译!

关键概念:

  • 依赖关系:myproc 依赖 myproc.c
  • 依赖方法:gcc -o myproc myproc.c 一进一出的对应关系,就是 Makefile 的最小单元。

——演示 Tab 缩进的坑(新手必看)

# 错误示范:命令前面用的是空格而不是 Tab
vim Makefile


myproc:myproc.c
    gcc -o myproc myproc.c    # 这行用的是 4 个空格,不是 Tab!


make
# 报错:
# Makefile:2: *** missing separator.  Stop.
#                  ↑ 缺少分隔符,就是因为你用了空格而不是 Tab
# 正确做法:命令前面必须是 Tab 键
# 在 Vim 中,先按 Tab 键,再输入命令
# 可以用 cat -A Makefile 查看缩进(Tab 会显示为 ^I)
cat -A Makefile
# myproc:myproc.c$
# ^Igcc -o myproc myproc.c$    ← ^I 就是 Tab

新手避坑:"missing separator" 是新手最常遇到的 Makefile 报错,99% 的原因是命令前用了空格而不是 Tab。记住:Makefile 中命令必须用 Tab 缩进!在 Vim 里如果 Tab 键被设置成了空格,可以先去设置里确认,或者用 cat -A 检查一下。


2.2 多规则与依赖链

语法形式
目标1: 依赖1
	命令1

目标2: 依赖2
	命令2
功能

一个 Makefile 可以包含多条规则。当目标的依赖本身又是另一条规则的目标时,就形成了依赖链,make 会一层层递归地处理。

示例

——演示完整编译流程的 Makefile(从 .c 到可执行文件)

还记得上一篇文章讲的 GCC 编译四大步骤吗?我们用 Makefile 把每一步都写进规则里:

# 完整版 Makefile
myproc: myproc.o
	gcc myproc.o -o myproc

myproc.o: myproc.s
	gcc -c myproc.s -o myproc.o

myproc.s: myproc.i
	gcc -S myproc.i -o myproc.s

myproc.i: myproc.c
	gcc -E myproc.c -o myproc.i
# 执行 make
make
# 输出:
# gcc -E myproc.c -o myproc.i     ← 先生成 .i
# gcc -S myproc.i -o myproc.s     ← 再生成 .s
# gcc -c myproc.s -o myproc.o     ← 再生成 .o
# gcc myproc.o -o myproc          ← 最后链接成可执行文件

理解依赖链:make 从第一个目标 myproc 开始,发现它依赖 myproc.o;而 myproc.o 又依赖 myproc.s……于是一层层往下找,直到找到最底层的 myproc.c,然后反过来一层层生成。这就像一个"堆栈"过程:先压栈找依赖,再出栈执行命令。


三、make 的工作原理

理解 make 的工作机制,是写好 Makefile 的关键。我们分点来讲。

3.1 make 的执行流程

功能

当你直接输入 make(不带参数)时,make 会按照以下步骤工作:

  1. 找文件:在当前目录下找名为 Makefile 或 makefile 的文件
  2. 找目标:找到文件后,把第一个目标作为最终目标(默认目标)
  3. 查依赖:检查这个目标的依赖文件是否存在、是否比目标更新
  4. 递归生成:如果依赖文件不存在,就在 Makefile 中寻找生成该依赖的规则,递归处理
  5. 执行命令:所有依赖都就绪后,执行命令生成目标
  6. 出错退出:如果被依赖的文件找不到,make 会直接退出并报错
示例

——演示 make 的依赖查找与增量编译

# 假设 Makefile 内容是规则链版本(上面 2.2 的完整版)

# 第一次 make:所有文件都不存在,从头生成
make
# 输出:执行了 4 条 gcc 命令

# 文件都在了,再次 make
make
# 输出:make: 'myproc' is up to date.
# 什么都不做,因为都是最新的

# 只修改 myproc.c 文件
touch myproc.c    # 用 touch 更新 myproc.c 的时间戳

# 再次 make
make
# 输出:
# gcc -E myproc.c -o myproc.i
# gcc -S myproc.i -o myproc.s
# gcc -c myproc.s -o myproc.o
# gcc myproc.o -o myproc
# 因为 myproc.c 更新了,所有依赖它的文件都要重新生成

# 只修改中间文件 myproc.s
touch myproc.s

make
# 输出:
# gcc -c myproc.s -o myproc.o     ← 只重新生成 myproc.o 及之后
# gcc myproc.o -o myproc
# myproc.i 不用重新生成,因为 myproc.c 没变!

这就是增量编译的威力:make 会智能判断哪些文件需要重新编译,只做必要的操作,绝不做无用功。这就是它比手敲命令高效的核心原因。


3.2 文件时间戳:理解"是否需要更新"

功能

make 判断"是否需要重新编译"的依据是文件的时间戳(时间戳)。这里的核心是一个知识点:文件的三个时间。

时间

全称

含义

何时更新

Access

访问时间

文件最近一次被读取的时间

文件被访问时

Modify

修改时间

文件内容被修改的时间

内容变更时

Change

状态时间

文件属性(如权限)变更的时间

属性变更时

公式:文件 = 内容 + 属性。Modify 管内容变更,Change 管属性变更。

示例

——演示 stat 命令查看文件的时间戳

# 用 stat 命令查看文件的详细时间信息
stat myproc.c
# 输出示例:
#   File: myproc.c
#   Size: 89          Blocks: 8          IO Block: 4096   regular file
# Device: fd01h/64769d    Inode: 1321125     Links: 1
# Access: (0664/-rw-rw-r--)  Uid: ( 1000/    user)   Gid: ( 1000/    user)
# Access: 2026-10-07 17:05:30.430619002 +0800     ← 访问时间
# Modify: 2026-10-07 17:05:25.940595116 +0800     ← 修改时间
# Change: 2026-10-07 17:05:25.940595116 +0800     ← 状态时间

# make 判断需要更新的核心:比较依赖文件和目标文件的 Modify 时间
# 如果 依赖的Modify时间 > 目标的Modify时间 → 需要重新生成

——演示用 touch 修改时间戳来触发重新编译

# 假设 myproc 已是最新,make 不会做任何事
make
# make: 'myproc' is up to date.

# touch 命令可以"更新文件时间"(不改内容,只改时间戳)
touch myproc.c

# 现在 myproc.c 的 Modify 时间比 myproc 新了
make
# 会重新编译!因为 make 发现依赖比目标新

实用技巧:touch filename 可以不改动文件内容,只更新时间戳,常用于"强制触发重新编译"。


3.3 .PHONY 伪目标

语法形式
.PHONY: 目标名
目标名:
	命令
功能

clean 这类目标很特殊——它不生成任何文件,只是执行一些清理命令。这类目标叫伪目标(Phony Target)。

为什么需要 .PHONY?

如果目录里恰好有个叫 clean 的文件,make 会认为 clean 目标"已经存在且是最新的",于是不执行清理命令!用 .PHONY:clean 声明后,make 就会忽略文件比对,每次都执行这个目标。

.PHONY 的作用:让 make 忽略"源文件和目标文件的 Modify 时间对比",让该目标总是被执行。

示例

——演示 clean 目标:项目清理

# Makefile
myproc:myproc.c
	gcc -o myproc myproc.c

.PHONY:clean
clean:
	rm -f myproc
# 编译
make
# gcc -o myproc myproc.c

# 查看目录
ls
# Makefile  myproc  myproc.c

# 清理项目(删除可执行文件)
make clean
# 输出:rm -f myproc

# 再次查看
ls
# Makefile  myproc.c
# myproc 被删除了

——演示 .PHONY 的必要性(有同名文件时)

# 假设当前目录下有个叫 clean 的文件
touch clean

# 如果 Makefile 中没有 .PHONY:clean
ls
# Makefile  clean  myproc.c

make clean
# 输出:make: 'clean' is up to date.
# !清理命令根本没执行,因为 make 看到有个叫 clean 的文件,以为它是最新的

# 加上 .PHONY:clean 之后
# Makefile 内容:.PHONY:clean
make clean
# 输出:rm -f myproc
# 正常执行清理,因为 .PHONY 让 make 忽略文件比对

结论:像 clean 这种不生成文件的目标,一定要用 .PHONY 声明,否则一旦目录中存在同名文件就会失效。

——演示 .PHONY 让目标总是被执行

# 把普通目标也声明成伪目标测试效果
.PHONY:myproc
myproc:myproc.c
	gcc -o myproc myproc.c
make
# gcc -o myproc myproc.c

make
# gcc -o myproc myproc.c    ← 注意:即使已是最新,也会重新执行!
# 因为 .PHONY:myproc 让它忽略时间比对,总是执行

# 这验证了"伪目标的特性是总是被执行"

四、Makefile 变量与自动化

当项目变大时,重复写 gcc、myproc 这些字符串会很啰嗦。Makefile 提供了变量和自动化变量来简化写法,让 Makefile 更简洁、更好维护。

4.1 自定义变量

语法形式
变量名 = 值           # 定义变量
$(变量名)             # 使用变量
功能

Makefile 中的变量和 Shell 变量类似,用于存储字符串。用 $(变量名) 的方式引用。使用变量能让 Makefile 更容易修改(改一处即可全局生效)。

示例

——演示变量的定义与使用

# 定义变量
CC = gcc
BIN = myproc
SRC = myproc.c

# 使用变量
$(BIN):$(SRC)
	$(CC) -o $(BIN) $(SRC)

.PHONY:clean
clean:
	rm -f $(BIN)

好处:使用变量后,要修改编译器、目标名等信息,只需改定义那一行,不用在每条命令里查找替换。这在大型项目中非常重要。


4.2 wildcard 函数与替换

语法形式
$(wildcard 模式)        # 获取匹配模式的所有文件名
$(变量:.旧后缀=.新后缀)  # 将变量中所有旧后缀替换为新后缀
功能
  • wildcard:自动获取当前目录下符合模式的所有文件(比如所有 .c 文件),不用手动一个个列。
  • 后缀替换:把文件列表中的后缀统一替换(比如把 .c 列表替换成 .o 列表)。
示例

——演示 wildcard 获取所有 .c 文件,并替换为 .o 文件

# Makefile
CC = gcc
BIN = myproc
SRC = $(wildcard *.c)     # 获取当前目录所有 .c 文件
OBJ = $(SRC:.c=.o)        # 把所有 .c 替换成 .o

$(BIN):$(OBJ)
	$(CC) -o $@ $^

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

.PHONY:clean
clean:
	rm -f $(OBJ) $(BIN)

.PHONY:test
test:
	@echo $(SRC)
	@echo $(OBJ)
# 用 test 目标查看变量展开后的内容
make test
# 输出:
# myproc.c                     ← $(SRC) 自动获取了所有 .c 文件
# myproc.o                     ← $(OBJ) 自动把 .c 换成了 .o

# 如果目录里有多个 .c 文件
# SRC 会自动变成:a.c b.c c.c
# OBJ 会自动变成:a.o b.o c.o
# 完全不用手动修改 Makefile!

$(wildcard *.c) 的妙用:项目里新增一个 .c 文件,Makefile 不用改,重新 make 就会自动把它纳入编译。这是大型项目 Makefile 的常见写法。


4.3 自动化变量 $@ $^ $<

语法形式

变量

含义

$@

目标文件名(target)

$^

所有依赖文件列表(dependencies)

$<

第一个依赖文件(first dependency)

功能

自动化变量是 Makefile 的"语法糖",能自动替换成当前规则中的目标名、依赖名,避免重复书写。

示例

——演示 $@ / $^ / $< 的含义与效果

# 不使用自动化变量(啰嗦写法)
myproc:myproc.o
	gcc myproc.o -o myproc
# 这里的 myproc 写了两遍,myproc.o 也写了两遍

# 使用自动化变量(简洁写法)
myproc:myproc.o
	gcc $^ -o $@
# $^ 自动替换为 myproc.o(所有依赖)
# $@ 自动替换为 myproc(目标)
# 展开后等价于:gcc myproc.o -o myproc

# $< 表示第一个依赖
myproc.o:myproc.c myproc.h
	gcc -c $< -o $@
# $< 自动替换为 myproc.c(第一个依赖)
# $@ 自动替换为 myproc.o(目标)
# 展开后:gcc -c myproc.c -o myproc.o
make
# 输出:gcc -c myproc.c -o myproc.o
#       gcc myproc.o -o myproc

——演示 @ 符号:不回显命令

# 命令前加 @ 可以"静默执行",不显示命令本身
test:
	@echo "这条命令不会回显"
	echo "这条命令会回显"

# 对比:
.PHONY:test
test:
	@echo "只显示这行输出"
	echo "命令和执行结果都显示"
make test
# 输出:
# 只显示这行输出
# echo "命令和执行结果都显示"
# 命令和执行结果都显示
# ↑ 加了 @ 的那行,命令本身不显示;没加的会显示命令

自动化变量记忆法:

  • $@ → @ 像"目标靶心",代表目标
  • $^ → ^ 像"一堆东西堆起来",代表所有依赖
  • $< → < 像"箭头指向第一个",代表第一个依赖

4.4 模式规则 %.o:%.c

语法形式
%.o:%.c
	命令
功能

% 是通配符(相当于"任意名字")。%.o:%.c 表示:"任意一个 .o 文件,都可以由同名的 .c 文件生成"。这样一条规则就能自动处理所有 .c → .o 的转换,不用为每个文件单独写规则。

示例

——演示模式规则自动处理多个文件

# Makefile
CC = gcc
BIN = myproc
SRC = $(wildcard *.c)
OBJ = $(SRC:.c=.o)

# 链接规则
$(BIN):$(OBJ)
	$(CC) -o $@ $^
	@echo "linking ... $^ to $@"

# 模式规则:任意 .o 由同名 .c 生成
%.o:%.c
	$(CC) -c $<
	@echo "compiling ... $< to $@"

.PHONY:clean
clean:
	rm -f $(OBJ) $(BIN)
make
# 输出:
# compiling ... a.c to a.o
# compiling ... b.c to b.o
# linking ... a.o b.o to myproc
# 一条 %.o:%.c 规则,自动处理了所有 .c 文件的编译!

总结:%.o:%.c + $(wildcard *.c) + 自动化变量,是 Makefile 的"黄金三件套"。掌握它们,你的 Makefile 就能自动适应项目文件的增减,无需手动维护。


五、完整实战:一个通用的 Makefile

把前面学的知识串起来,这里给出一份经典的"通用 Makefile",可以直接用于大多数小型 C 项目:

# ===== 通用 Makefile 模板 =====

# 1. 定义变量
BIN = proc.exe          # 目标可执行文件名
CC = gcc               # 编译器
SRC = $(wildcard *.c)  # 自动获取所有 .c 文件
OBJ = $(SRC:.c=.o)     # 把 .c 列表替换成 .o 列表
LFLAGS = -o            # 链接选项
FLAGS = -c             # 编译选项
RM = rm -f             # 删除命令

# 2. 链接规则(最终目标)
$(BIN):$(OBJ)
	@$(CC) $(LFLAGS) $@ $^
	@echo "linking ... $^ to $@"

# 3. 模式规则(编译 .c 到 .o)
%.o:%.c
	@$(CC) $(FLAGS) $<
	@echo "compiling ... $< to $@"

# 4. 清理
.PHONY:clean
clean:
	$(RM) $(OBJ) $(BIN)

# 5. 测试(查看变量)
.PHONY:test
test:
	@echo $(SRC)
	@echo $(OBJ)

模板说明:

  • @ 让命令静默执行,只显示自定制的 echo 信息,界面更清爽
  • $(wildcard *.c) 自动获取源文件,新增文件无需改 Makefile
  • $@、$^、$< 自动化变量减少重复输入

六、常用命令与语法速查表

make 命令速查

命令

功能

make

执行第一个目标(默认目标)

make 目标名

执行指定的目标

make clean

清理生成的文件

make -f 文件名

指定 Makefile 文件名(默认是 Makefile)

make -n

只显示要执行的命令,不实际执行(预演)

Makefile 语法速查

语法

说明

目标:依赖

定义一条规则

Tab + 命令

命令必须用 Tab 缩进

.PHONY:目标

声明伪目标,总是被执行

变量 = 值

定义变量

$(变量)

引用变量

$(wildcard *.c)

获取所有 .c 文件

$(SRC:.c=.o)

后缀替换

%.o:%.c

模式规则

$@

目标文件名

$^

所有依赖文件

$<

第一个依赖文件

@命令

不回显该命令

文件时间戳速查

时间

含义

Access

访问时间

Modify

内容修改时间(make 判断依据)

Change

属性变更时间

stat 文件

查看文件详细时间信息

touch 文件

更新时间戳(不改内容)


七、总结

本篇我们学习了 make 和 Makefile 的自动化构建,核心内容回顾:

  1. make 与 Makefile 简介:make 是命令工具,Makefile 是规则文件,二者搭配实现自动化编译
  1. 基本语法规则:目标:依赖 + Tab 缩进的命令,构成一条规则
  1. 工作原理:找第一个目标 → 比较时间戳 → 递归查找依赖 → 增量编译
  1. 伪目标 .PHONY:让 clean 等目标总是被执行,避免同名文件干扰
  1. 变量与自动化:自定义变量(CC、BIN)、wildcard 函数、自动化变量($@、$^、$<)、模式规则(%.o:%.c)

学习建议:

  • 理解"依赖关系 + 依赖方法"是 Makefile 的灵魂,多动手写几遍
  • Tab 缩进是第一大坑,遇到 "missing separator" 先检查缩进
  • make clean 和 .PHONY 是项目管理的标配,务必掌握
  • 熟练后试试 $(wildcard *.c) + 模式规则 + 自动化变量 这套组合,你的 Makefile 就能"以不变应万变"

下一篇我们将开始动手写 Linux 的第一个真正意义上的程序——进度条,把 GCC 和 Makefile 的知识真正用起来,敬请期待!


专栏目录:

  • 第六篇(本篇):make 与 Makefile 自动化构建
  • 第七篇:Linux 第一个系统程序——进度条(敬请期待)
  • ...

如果本文对你有帮助,欢迎点赞收藏关注,你的支持是我持续创作的动力!

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/Xiao_running/article/details/167283104

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--