拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Re:Linux系统篇(七) 开发工具篇 Chapter3:Makefile 从入门到精通 —— 依赖关系、伪目标、栈式推导与自动化构建全解

观众老爷们大家好 这里是邪修KING的独家频道本文属于系列Linux系统篇 ——操作指令一起学Linux的小伙伴可订阅专栏 Linux系统篇上一篇我们学习了 gcc 编译流程知道了单个 .c 文件如何一步步变成可执行程序。但真实项目中往往有成百上千个源文件分布在不同目录、不同模块如果每次修改都手动敲 gcc 命令编译不仅繁琐易错还无法做到「只重新编译修改过的文件」效率极低。Makefile 就是为解决这个问题而生的自动化构建工具。会不会写 Makefile从一个侧面反映了开发者是否具备大型工程的构建能力。本篇我们从最简写法入手逐层深入依赖原理、伪目标本质、栈式推导过程、变量与模式规则彻底搞懂 Makefile 的每一个符号、每一行逻辑。一、为什么需要 Makefile自动化构建的意义1.1 手动编译的痛点假设一个项目有 10 个 .c 源文件我们可以手动一条条 gcc 命令编译gcc-cmain.c-omain.o gcc-ctool.c-otool.o gcc-cutil.c-outil.o# ... 省略7个文件gcc main.o tool.o util.o...-omyproc这样写有三个致命问题太繁琐文件越多命令越长每次编译都要敲一大串极易写错效率低只改了一个文件也要所有文件全部重新编译大项目编译一次要等几十分钟易出错手动管理编译顺序、依赖关系很容易漏编译、版本不一致1.2 Makefile 是什么make 是一条命令是一个解释执行 Makefile 中指令的构建工具Makefile 是一个文件里面定义了一整套编译规则哪些文件需要编译、编译顺序、依赖关系、清理规则等二者配合使用实现自动化编译一旦写好 Makefile只需要一个 make 命令整个工程自动编译极大提升开发效率。 类比理解Makefile 就像一张「施工图纸」写清楚了每一步怎么做、谁先谁后make 命令就是施工队拿着图纸自动把整个项目盖起来。二、Makefile 核心基础目标、依赖与命令2.1 三要素目标文件、依赖文件、依赖方法Makefile 的基本单元是一条「规则」每条规则由三部分组成目标文件: 依赖文件列表 依赖方法也就是要执行的命令前面必须是Tab缩进不能是空格目标文件这条规则最终要生成的文件或者要执行的动作名依赖文件生成目标文件所需要的原材料依赖方法从依赖生成目标所要执行的 shell 命令2.2 最简版 Makefile我们用一个最简单的单文件项目入门# Makefile 内容myproc: myproc.c gcc-omyproc myproc.c clean:rm-fmyproc对应两条规则目标 myproc依赖 myproc.c执行 gcc 编译生成可执行文件目标 clean无依赖执行 rm 删除可执行文件2.3 基本使用# 执行 make默认构建第一个目标make# 执行指定目标 cleanmakeclean2.4 两个基础结论make 默认执行第一个目标make 自上而下扫描 Makefile 文件把找到的第一个目标文件作为「终极目标」默认只构建它。所以通常把最终生成的可执行文件放在第一条。显式执行其他目标像 clean 这种不和终极目标产生依赖关系的目标默认不会自动执行必须用 make clean 显式调用。三、make 执行原理时间戳对比与增量编译很多初学者都会有两个疑问为什么代码没改的情况下第二次 make 会提示「已是最新」不重新编译make 是怎么知道哪个文件改了、哪个没改的这就要深入 make 的核心机制基于文件修改时间的增量编译。3.1 文件 内容 属性Linux 下每个文件都包含两部分内容和属性。属性中记录了三个关键时间执行 stat 文件名 可以查看statmyprocAccess:2024-10-2319:04:18# 访问时间最近一次读取文件内容的时间Modify:2024-10-2319:04:18# 修改时间最近一次修改文件内容的时间Change:2024-10-2319:04:18# 状态改动时间最近一次修改文件属性的时间make 判断是否需要重新编译看的就是 Modify 时间M 时间。3.2 核心判断逻辑对于一条 目标: 依赖 规则make 执行前会做一次时间对比如果 依赖文件的 M 时间 比 目标文件的 M 时间 新 → 说明依赖改过了目标过期了 → 执行依赖方法重新生成目标如果 目标文件的 M 时间 比 所有依赖都新 → 说明目标是最新的 → 跳过不执行这就是增量编译的本质只重新编译那些依赖发生了变化的目标没改过的文件直接复用大大节省编译时间。3.3 验证实验我们可以用 touch 命令手动修改文件时间验证这个逻辑# 第一次 make正常编译make# 第二次 make提示 up to date不编译make# touch 一下源文件模拟代码修改touchmyproc.c# 再 make自动重新编译maketouch 会更新文件的 M 时间为当前时间让依赖比目标新触发重新编译。四、伪目标 .PHONY为什么 clean 总是能执行4.1 问题引入clean 目标没有任何依赖按上面的逻辑如果当前目录没有叫 clean 的文件 → 目标不存在 → 每次 make clean 都会执行如果哪天目录下碰巧出现了一个叫 clean 的普通文件 → 目标存在且没有依赖 → make 会认为目标永远是最新的 → make clean 失效提示已是最新这显然不合理我们希望 clean 无论如何都能执行不受同名文件干扰。4.2 .PHONY 的作用.PHONY 用来声明一个目标是「伪目标」它的特性是被 .PHONY 修饰的目标make 会跳过时间戳对比永远认为它是过期的每次调用都会强制执行。标准写法.PHONY: clean clean:rm-fmyproc声明后哪怕目录下有 clean 文件make clean 也会正常执行删除命令。4.3 一句话总结本质.PHONY:xxx 的本质就是让 make 忽略 xxx 文件的存在不做 M 时间对比xxx 目标永远被执行。思考为什么编译目标一般不设为伪目标答编译目标我们需要增量编译能力只有修改了才重新编译如果设成伪目标每次都会全量重新编译失去了增量编译的优势。只有 clean、test 这种动作型目标才需要每次都执行。五、依赖链的栈式推导make 是如何层层找到源文件的单条规则很简单但真实的 Makefile 往往是多层依赖形成一条依赖链。make 是如何处理多层依赖的这个过程非常像「栈」的工作方式。5.1 多依赖规则示例我们把编译四阶段拆解开写出完整依赖链# 终极目标可执行文件依赖 .omyproc: myproc.o gcc myproc.o-omyproc# .o 依赖 .smyproc.o: myproc.s gcc-cmyproc.s-omyproc.o# .s 依赖 .imyproc.s: myproc.i gcc-Smyproc.i-omyproc.s# .i 依赖 .cmyproc.i: myproc.c gcc-Emyproc.c-omyproc.i .PHONY: clean clean:rm-f*.i *.s *.o myproc5.2 栈式推导过程详解make 处理依赖链的过程可以分为「向下压栈找依赖」和「向上弹栈执行命令」两个阶段完全符合栈「先进后出」的特性。阶段一自顶向下逐层压栈查找make 从第一个目标 myproc 开始检查依赖 myproc.o发现 myproc.o 不存在 / 过期就在 Makefile 里找有没有生成 myproc.o 的规则找到 myproc.o: myproc.s继续检查依赖 myproc.s发现 myproc.s 不存在继续找生成它的规则依赖 myproc.i继续往下直到最底层 myproc.i: myproc.c依赖是源文件 myproc.c真实存在至此整条依赖链全部压入栈中查找结束这个过程就像递归深入一层层往下找依赖直到触底找到真实存在的源文件。阶段二自底向上逐层弹栈执行从最底层开始对比 myproc.i 和 myproc.c 的时间需要更新就执行 gcc -E 生成 .i上弹一层对比 myproc.s 和 myproc.i 的时间需要更新就执行 gcc -S 生成 .s继续上弹生成 .o最后弹到栈顶生成终极目标 myproc执行顺序和查找顺序正好相反先找的后执行后找的先执行完美契合栈的先进后出特性。栈式推导示意图5.3 两个补充规则找不到依赖规则就报错如果某一层依赖文件不存在且 Makefile 里没有生成它的规则make 直接报错退出。make 只管依赖不管命令对错make 只负责判断要不要执行、按什么顺序执行命令本身编译失败、语法错误make 不负责直接终止。六、进阶语法变量、通配符与模式规则学会了基础规则我们写的 Makefile 还是「写死文件名」的版本加一个源文件就要改好几处维护性很差。进阶语法就是用来解决这个问题让 Makefile 更通用、更好维护。6.1 自定义变量Makefile 支持定义变量类似 C 语言的宏本质是字符串替换。定义后用 $(变量名) 使用。Makefile# 定义变量BINmyprocCCgccRMrm-f# 使用变量$(BIN):myproc.c$(CC)-o$(BIN)myproc.c clean:$(RM)$(BIN)变量的好处修改一处所有用到的地方自动生效比如想换编译器只改 CC 一行就行。6.2 wildcard自动获取源文件$(wildcard 模式) 是 Makefile 的内置函数可以获取当前目录下所有匹配模式的文件名。# 获取当前目录下所有 .c 文件赋值给 SRCSRC$(wildcard *.c)等价于自动把所有 .c 文件名列出来不用手动一个个写新增源文件自动包含进去。6.3 替换引用批量改后缀$(变量名:旧后缀新后缀) 可以把变量中所有文件名的后缀批量替换。SRC$(wildcard *.c)# 假设得到 a.c b.c c.cOBJ$(SRC:.c.o)# 替换为 a.o b.o c.o非常方便地从源文件列表得到目标文件列表。6.4 模式规则一条规则匹配所有同类目标%.o: %.c 是模式规则% 是通配符代表「同名匹配」。含义所有的 .o 文件都依赖同名的 .c 文件一条规则就能覆盖所有 .c → .o 的编译不用每个文件写一条%.o: %.c gcc-c$-o$配合上面的 OBJ 变量再多文件也只需要这一条通用规则。七、自动变量全解$ / $^ / $ 每个符号的含义Makefile 提供了一组特殊的自动变量在规则的命令中使用代表固定含义写通用规则时必不可少。逐个举例理解# 例1链接阶段多个依赖myproc: main.o tool.o util.o gcc $^-o$# $ → myproc# $^ → main.o tool.o util.o# 例2编译阶段单个依赖%.o: %.c gcc-c$-o$# 匹配到 main.o: main.c 时# $ → main.o# $ → main.c# 例3加 不回显命令test: echo正在编译...# 执行 make test 时只打印 正在编译...不显示 echo 这行命令本身八、工业级通用 Makefile 逐行拆解有了上面所有知识点我们来看一个完整的、可直接用于多文件项目的通用版 Makefile逐行拆解每一行的作用。完整代码# 1. 定义最终生成的可执行文件名BINproc.exe# 2. 定义编译器CCgcc# 3. 获取当前目录所有 .c 源文件SRC$(wildcard *.c)# 4. 将所有 .c 替换为 .o得到目标文件列表OBJ$(SRC:.c.o)# 5. 链接选项LFLAGS-o# 6. 编译选项FLAGS-c# 7. 删除命令RMrm-f# 8. 终极目标生成可执行文件依赖所有 .o$(BIN):$(OBJ)echolinking ... $^ to$$(CC)$(LFLAGS)$$^# 9. 模式规则所有 .o 都由同名 .c 编译而来%.o: %.c echocompling ... $ to$$(CC)$(FLAGS)$# 10. 声明 clean 为伪目标.PHONY: clean# 11. 清理目标删除所有 .o 和可执行文件clean:$(RM)$(OBJ)$(BIN)# 12. 测试用伪目标打印变量调试用.PHONY:testtest: echo$(SRC)echo$(OBJ)使用方法# 一键编译make# 清理工程makeclean# 调试查看变量maketest这个版本的 Makefile 具备很强的通用性普通多文件 C 项目直接拿来就能用新增源文件不需要修改 Makefile自动识别编译。九、总结与最佳实践9.1 核心知识体系复盘基本构成每条规则 目标 依赖 命令Tab 缩进不能错执行原理基于 Modify 时间戳对比实现增量编译依赖比目标新才重新生成伪目标 .PHONY跳过时间对比目标永远执行适合 clean、test 等动作型目标栈式推导自顶向下查找依赖链压栈自底向上弹栈执行命令先进后出进阶语法变量简化维护、wildcard 自动扫源文件、替换引用批量改后缀、模式规则统一同类编译自动变量 目标、 目标、目标、^ 所有依赖、$ 第一个依赖是写通用规则的核心9.2 最佳实践建议终极目标放在 Makefile 最上方作为默认构建目标清理、测试等动作目标一律加 .PHONY 声明多用变量、模式规则少写死文件名提升可维护性合理利用自动变量让规则更通用大型项目可以进一步拆分多级 Makefile递归调用到这里Linux 基础开发工具的核心内容就全部覆盖了。从 vim 编辑源码到 gcc 四阶段翻译再到 Makefile 自动化构建我们打通了从写代码到构建项目的完整流程。下一篇我们将深入链接阶段讲解动态库与静态库的原理、制作与使用彻底搞懂程序链接的底层逻辑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门