欢迎光临
我们一直在努力

【Linux学习】Linux学习第九弹——从一次“链接失败”到一键 make:搞懂 make/makefile 自动构建

在这里插入图片描述

🎬 个人主页:MSTcheng · CSDN 🌱 代码仓库 :MSTcheng · Gitee

🔥 精选专栏: 《C语言》 《数据结构》 《算法学习》 《C++由浅入深》

《Linux操作系统》

💬座右铭
路虽远行则将至,事虽难做则必成!


上一篇文章中我们分析了编译器gcc/g++在程序的预处理,编译,汇编,链接都做了什么,本篇文章继续介绍工具链中的make/makefile。

工具篇: 【Linux学习】Linux学习第七弹——搞懂包管理器与 vim,Linux 开发工具不再难

【Linux学习】Linux学习第八弹——从源码到可执行程序:gcc 到底偷偷做了什么?

文章目录

  • 一、`make / makefile` 自动化构建
    • 1.1 开场:一次"链接失败"引出 `make`
    • 1.2 见一见 `make / makefile`
    • 1.3 核心思想:依赖关系 与 依赖方法
    • 1.4 详细学习 `makefile` 的基本语法(自动推导 / 推导栈 / 函数递归)
    • 1.5 最佳实践:`clean`目标→`.PHONY`伪目标
    • 1.6 `.PHONY`:伪目标
    • 1.7 `make` 如何判断“要不要重新编译”:时间戳
    • 1.8 `stat` 看三个时间:`Access / Modify / Change`
    • 1.9`Makefile` 进阶:变量与自动变量
    • 1.10多文件编译:编译 + 链接
  • 二、全文总结

一、make / makefile 自动化构建

1.1 开场:一次"链接失败"引出 make

mstcheng@ubuntu-dev:~/117/code/test1$ gcc test.c -o test.exe -static
/usr/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status

  • -static 表示 以静态方式链接,需要系统里有 C 标准库的静态库(libc.a)。(动静态链接会在后面动静态库介绍!)
  • 报错 cannot find -lc / collect2: error: ld returned 1 exit status = 找不到 C 标准库

看到这里,大家可能会疑惑:为什么我们在 Windows 上用 VS 写 C 语言时,从来没有遇到过这种链接问题?

其实不是 VS 不需要链接,而是 VS帮我们做了自动化构建——它把编译、链接、库的查找等步骤全部封装在后台自动完成了,我们只需要点一下 「运行」 按钮即可。

  • 那么,Linux 下对应的自动化构建工具是什么呢? 答案就是 make / makefile 自动化项目的构建。

  • 💡 注意:make 是一条命令,makefile 是一个文件。

1.2 见一见 make / makefile

先写一个最简单的 makefile,在当面目录下vim makefile即创建一个makefile写入下面代码:

test.exe:test.c
gcc -o test.exe test.c

mstcheng@ubuntu-dev:~/117/code/test1$ make
gcc -o test.exe test.c #make之后自动帮我们执行makefile里面的编译代码

mstcheng@ubuntu-dev:~/117/code/test1$ ll
total 864
drwxrwxr-x 2 mstcheng mstcheng 4096 Sep 19 16:04 ./
drwxrwxr-x 9 mstcheng mstcheng 4096 Sep 11 11:17 ../
-rw-rw-r– 1 mstcheng mstcheng 40 Sep 19 16:04 makefile
-rw-rw-r– 1 mstcheng mstcheng 104 Sep 19 16:02 test.c
//编译好后就形成了可执行文件
-rwxrwxr-x 1 mstcheng mstcheng 15952 Sep 19 16:04 test.exe*

//运行可执行程序
mstcheng@ubuntu-dev:~/117/code/test1$ ./test.exe
a+b=30

  • 只要敲 make,makefile 里写好的编译命令就被自动执行,不用每次手敲一长串 gcc。

  • makefile 中 「规则行」 和 「命令(依赖方法)行」 之间必须用 Tab 缩进,不能用空格。用空格会报 missing separator. Stop.,这是新手最常见的错误。 例如:

    mstcheng@ubuntu-dev:~/117/code/test1$ make
    makefile:2: *** 缺失分隔符。 停止。

1.3 核心思想:依赖关系 与 依赖方法

test.exe:test.c # ← 依赖关系
gcc -o test.exe test.c # ← 依赖方法

  • 冒号左边:目标文件(test.exe)
  • 冒号右边:依赖文件列表(test.c)
  • 下一行(Tab 开头):由「依赖文件」生成「目标文件」

这两个概念可以用一个很通俗的例子来理解:

  • “儿子对父亲说:我是你儿子”,表达的是“我依赖你”这层父子关系。没有这层关系,后面的话就无从谈起,所以它就是依赖关系。
  • “儿子跟父亲说:给我打钱”,则是 在这层关系成立后,说清楚具体要做什么。光有“儿子”身份还不够,必须主动开口要钱,所以它就是依赖方法。

在这里插入图片描述

对应到上面的 makefile:

  • test.exe:test.c 就是依赖关系,说明 test.exe 依赖 test.c;
  • 下一行 gcc -o test.exe test.c 就是依赖方法,说明具体怎么用 test.c 生成 test.exe。
  • 注意:所以makefile 的本质就是 描述文件之间的依赖关系,并给出生成目标的方法;make 据此决定「哪些目标要重建、按什么顺序重建」。

如果把这个比方再进一步概括:

「这个世界,要完成任何一件事情,都是依赖关系和依赖方法的关系!」 「做任何事,既要知道“我依赖谁”,也要知道“具体怎么做”!」

1.4 详细学习 makefile 的基本语法(自动推导 / 推导栈 / 函数递归)

我们只写 test.exe:test.c,但 make 的输出却是完整四步:

mstcheng@ubuntu-dev:~/117/code/test1$ make
gcc -E test.c -o test.i # 预处理
gcc -S test.i -o test.s # 编译
gcc -c test.s -o test.o # 汇编
gcc test.o -o test.exe # 链接

也就是说,make 自己"脑补"出了中间过程。如果把完整链条全部写出来:

test.exe:test.o
gcc test.o -o test.exe
test.o:test.s
gcc -c test.s -o test.o
test.s:test.i
gcc -S test.i -o test.s
test.i:test.c
gcc -E test.c -o test.i

关键理解:

  • make 解析规则时,会从目标文件出发不断追问“它依赖谁”,由此形成一个推导栈,栈里装的正是一条条依赖方法。
  • 例如 test.exe:test.o,当 make 发现 test.o 不存在时,就会先把「生成 test.o 的依赖方法」压入栈;接着继续追问 test.o 又依赖谁,一路向下,直到最底层的 test.c 已经存在为止。
  • 这种“先一路向下追问依赖,再逐层回溯执行”的过程,本质上就像函数递归调用。</fon

1.5 最佳实践:clean目标→.PHONY伪目标

先看一个最佳实践:声明一个符合 clean / clear / cls 的伪目标。

test.exe:test.c
gcc -o test.exe test.c

.PHONY:clean
clean:
rm -f test.exe test.i test.s test.o

mstcheng@ubuntu-dev:~/117/code/test1$ make
gcc -o test.exe test.c

mstcheng@ubuntu-dev:~/117/code/test1$ ll
total 864
drwxrwxr-x 2 mstcheng mstcheng 4096 Sep 19 22:38 ./
drwxrwxr-x 9 mstcheng mstcheng 4096 Sep 11 11:17 ../
-rw-rw-r– 1 mstcheng mstcheng 98 Sep 19 22:38 makefile
-rw-rw-r– 1 mstcheng mstcheng 104 Sep 19 16:02 test.c
-rwxrwxr-x 1 mstcheng mstcheng 15952 Sep 19 22:38 test.exe*

mstcheng@ubuntu-dev:~/117/code/test1$ make clean
rm -f test.exe test.i test.s test.o

⚠️ 注意:rm -f 的 -f 不能省。若写成 rm test.exe …,当文件不存在时 rm 会报错并返回非 0,make clean 就直接中断

1.6 .PHONY:伪目标

.PHONY 用来修饰目标文件是一个伪目标,其本质是总是被执行的。先看现象对比:

[whb@bite-alicloud lesson9]$ make
gcc -o test.exe test.c
[whb@bite-alicloud lesson9]$ make # 再来一次
make: 'test.exe' is up to date. # 不重新编译!

但加了 .PHONY:clean 之后,make clean 每次都会执行。为什么之前 gcc 无法二次编译老代码?答案在于时间戳:

在 make 眼里,test.exe、test.c 这些本质上都只是文件,而每个文件都有自己的修改时间(Mod 时间)。make 判断“要不要重新编译”的依据,就是去对比目标文件和依赖文件的 Mod 时间:只有当依赖文件比目标文件新时,才会重新生成目标文件。(在后面make判断要不要重新编译就会说明)

  • 普通目标(如 test.exe):make 比较 目标文件 与 依赖文件 的 Mod 时间**,只有"依赖比目标更新"时才重建 → 所以第二次 make 显示 up to date。
  • 伪目标(.PHONY 修饰的目标):make 不做时间比较,无条件执行

⚠️注意:.exe 为什么不用 PHONY 修饰?—— 加速编译的效率!!! 即:可执行文件不能加 .PHONY。增量编译的优势就是“源文件没变就不重编”;一旦给 .exe 也声明为伪目标,make 就不会再比较时间戳,而是每次都无条件重跑 gcc,自然就失去了增量编译带来的加速效果。

1.7 make 如何判断“要不要重新编译”:时间戳

先看一个现象:修改 test.c 之后再执行 make,这次就会重新编译。

mstcheng@ubuntu-dev:~/117/code/test1$ make
gcc -o test.exe test.c

mstcheng@ubuntu-dev:~/117/code/test1$ make
gcc -o test.exe test.c # 修改 test.c 后,又重新编译

判断的关键并不在于“敲了几次 make”,而在于 test.c 和 test.exe 这两个文件的 修改时间(Mod 时间 / mtime)。

  • 目标文件比依赖文件新:说明源文件没有被改过,不需要重新编译。
  • 依赖文件比目标文件新:说明源文件被改过了,才需要重新生成目标文件。

⚠️注意:make 通过比较目标文件和依赖文件的修改时间(mtime)来决定是否重新编译:目标比依赖新 → 不编译;依赖比目标新 → 重新编译。 这就是增量编译的核心。

一张图总结复盘: 在这里插入图片描述

知道了make判断要不要重新编译看到其实是两个文件的时间关系,那我们怎么去查看到这个时间呢?下面就来介绍一下使用stat查看三个时间。

1.8 stat 看三个时间:Access / Modify / Change

mstcheng@ubuntu-dev:~/117/code/test1$ stat test.c
File: test.c
size: 78 Blocks: 8 IO Block: 4096 regular file
Device: 8,2Inode: 1491331 Links: 1
Access: (0664/-rw-rw-r–) Uid: ( 1000/mstcheng) Gid: ( 1000/mstcheng)
Access: 2026-09-20 15:17:42.471182817 +0800
Modify: 2026-09-20 15:17:38.424526405 +0800
Change: 2026-09-20 15:17:38.426045536 +0800
Birth: 2026-09-20 15:17:38.424526405 +0800

stat test.c 中,与“文件时间”直接相关的主要是下面三行:

Access: 2026-09-20 15:17:42.471182817 +0800
Modify: 2026-09-20 15:17:38.424526405 +0800
Change: 2026-09-20 15:17:38.426045536 +0800

在 Linux 看来,文件 = 文件内容 + 文件属性:内容变化会影响文件大小,而 Modify 时间等元数据本身也属于文件属性。这三个时间分别表示:

时间含义什么时候会变
Access(Atime) 文件被访问的时间 读取文件内容时会更新
Modify(Mtime) 文件内容被修改的时间 写入内容时会更新
Change(Ctime) 文件属性(inode)被修改的时间 修改权限、大小、文件名等属性时会更新

其中 Access 时间有一个重要的“惰性更新”机制:

如果每读一次文件内容,都要立刻写一次磁盘去更新 Atime,就会频繁访问外设,明显拖慢系统整体效率。 因此 OS 做了优化:并不是每次访问都会更新 Atime,而是 采用惰性更新策略,常见的有 relatime、noatime 等挂载选项。

最后再补几个和时间有关的命令:touch test.c,cat / stat test.c。

  • touch 会把 test.c 的时间属性刷新为当前时间,并写回磁盘;
  • 因此它常被用来让 test.c 显得“比 test.exe 更新”,从而触发 make 重新编译。这正好和前面 1.6 节“依赖比目标新才重新编译”的规则对应起来。
  • cat / stat 之类的读操作会改变 Access 时间,但不会触发 make 重新编译——因为 make 只看Modify时间。
  • 只改文件的权限(chmod)会改 Change/Ctime,但不一定改 Mtime。

1.9Makefile 进阶:变量与自动变量

前面我们写的规则,基本都把 test.exe、test.c 这些名字直接写死。文件一多,每次改名都要逐个替换,很容易漏改。makefile 支持用变量把常用的名字集中管理起来,这样写起来也更清晰:

BIN=hello.exe
SRC=test.c

$(BIN):$(SRC)
@echo "开始编译代码"
@gcc -o $@ $^
@echo "编译完成"

这里引入了三类很实用的写法:

  • 变量:BIN=hello.exe、SRC=test.c,使用时用 $(BIN)、$(SRC) 引用。这样以后要换目标名或源文件名,只需要改最上面两行,避免硬编码、便于统一维护。

  • 自动变量:

    • $@:当前规则中的 目标文件名,这里是 hello.exe;
    • $^:所有 依赖文件,这里是 test.c;
    • $<:第一个依赖文件。
  • @ 前缀:命令前加 @,表示执行时不再回显这条命令本身,只显示命令真正产生的输出,比如 echo 的内容,终端界面会更干净。 就像这样:

    mstcheng@ubuntu-dev:~/117/code/test1$ make
    开始编译代码
    编译完成
    mstcheng@ubuntu-dev:~/117/code/test1$ ll
    total 880
    drwxrwxr-x 2 mstcheng mstcheng 4096 Sep 20 15:41 ./
    drwxrwxr-x 9 mstcheng mstcheng 4096 Sep 11 11:17 ../
    -rwxrwxr-x 1 mstcheng mstcheng 15952 Sep 20 15:41 hello.exe*

💡 记忆法:$@(at,目标)/ $^(所有依赖)/ $<(第一个依赖)。

1.10多文件编译:编译 + 链接

前面 1.9 节我们学会了用变量管理目标名和依赖文件。实际工程里不可能只有一个 test.c,往往是几十上百个源文件一起参与构建:

mstcheng@ubuntu-dev:~/117/code/lesson9$ ls
main.c Makefile src1.c src2.c ... src99.c

这些文件会经历两个阶段:

在这里插入图片描述

  • 编译:每个 .c 分别翻译成对应的 .o 目标文件;
  • 链接:把一堆 .o 和所需的 库 合并成一个可执行程序。

对应到 makefile,可以用变量把一堆 .o 收集起来:

BIN=main
OBJ=main.o src1.o src2.o … src99.o

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

🎯 "编译"和"链接"的区别——编译是把单个源文件翻译成目标文件(.o),链接是把多个目标文件和库合并成可执行文件。分开的好处:改一个 .c 只需重编这一个 .o 再链接,而不必把 100 个文件全部重编 → 这正是 make + 时间

二、全文总结

回顾全文,核心其实是一条线:makefile 描述“依赖关系 + 依赖方法”,make 负责按需执行这些规则。

  • makefile 的本质就是回答「目标是谁、依赖谁、怎么生成」。
  • make 会从目标出发自动追问依赖链,像递归一样先向下推导,再逐层回溯执行。
  • make 通过比较目标文件和依赖文件的 Modify 时间决定是否重新编译:目标新就不编,依赖新才重编,这就是增量编译。
  • clean、变量、自动变量、多文件编译这些技巧,最终都是为了让构建更自动化、更高效。

最后记住四个容易忽略的细节:依赖关系必须存在,但依赖文件列表可以为空;依赖方法可以是任意 shell 命令;make 后面跟哪个目标就解析哪个目标;并且默认只推导第一条完整链路!

赞(0)
未经允许不得转载:171主机测评 » 【Linux学习】Linux学习第九弹——从一次“链接失败”到一键 make:搞懂 make/makefile 自动构建
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址