Linux操作系统从入门到实战(十)Linux开发工具(下)make/Makefile的推导过程与扩展语法

Linux操作系统从入门到实战(十)Linux开发工具(下)make/Makefile的推导过程与扩展语法

  • 前言
  • [一、 make/Makefile的推导过程](#一、 make/Makefile的推导过程)
    • [1. 先看一个完整的Makefile示例](#1. 先看一个完整的Makefile示例)
    • [2. make的工作流程](#2. make的工作流程)
      • (1)寻找Makefile文件
      • (2)确定最终目标
      • [(3) 检查最终目标是否需要更新](#(3) 检查最终目标是否需要更新)
      • [(4) 从最终目标到中间文件](#(4) 从最终目标到中间文件)
      • [(5) 反向执行命令:从源文件到最终目标](#(5) 反向执行命令:从源文件到最终目标)
      • [(6) 遇到错误立即停止](#(6) 遇到错误立即停止)
      • [(7) 核心逻辑:只做"必要的事"](#(7) 核心逻辑:只做“必要的事”)
  • 二、make/Makefile的扩展语法
    • [1. 变量的妙用](#1. 变量的妙用)
    • [2. 自动变量](#2. 自动变量)
    • [3. 批量处理](#3. 批量处理)
    • [4. 高级小技巧](#4. 高级小技巧)
    • [5. 完整示例](#5. 完整示例)

前言

  • 前面的博客里我们讲解了Linux开发工具自动化构建-make/Makefile里的基础知识
  • 接下来我们继续讲解Linux开发工具自动化构建-make/Makefile里的细节,make/Makefile的推导过程与扩展语法

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的Linux知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12879535.html?spm=1001.2014.3001.5482


一、 make/Makefile的推导过程

1. 先看一个完整的Makefile示例

假设我们有一个名为myproc的程序,对应的Makefile内容如下

makefile 复制代码
# 最终目标:生成可执行文件myproc,依赖于中间文件myproc.o
myproc: myproc.o
    gcc myproc.o -o myproc  # 通过链接myproc.o生成可执行文件myproc

# 中间目标:生成目标文件myproc.o,依赖于汇编文件myproc.s
myproc.o: myproc.s
    gcc -c myproc.s -o myproc.o  # 将汇编文件myproc.s编译为目标文件myproc.o

# 中间目标:生成汇编文件myproc.s,依赖于预处理文件myproc.i
myproc.s: myproc.i
    gcc -S myproc.i -o myproc.s  # 将预处理文件myproc.i转换为汇编文件myproc.s

# 中间目标:生成预处理文件myproc.i,依赖于源文件myproc.c
myproc.i: myproc.c
    gcc -E myproc.c -o myproc.i  # 对源文件myproc.c进行预处理,生成myproc.i

# 伪目标:清理所有编译过程中产生的中间文件和可执行文件
.PHONY: clean
clean:
    rm -f *.i *.s *.o myproc  # 删除所有.i、.s、.o文件及myproc

执行make命令的输出

  • 当我们在终端输入make后,会看到如下执行过程:
bash 复制代码
$ 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       # 第四步:链接生成可执行文件

整个过程就像"剥洋葱"------从最终目标出发,逐层拆解依赖,直到找到最原始的源文件,再反向执行编译命令。

  • 下面我们详细解释make是如何一步步完成这个过程的。

2. make的工作流程

在默认情况下(即直接输入make命令),工具的执行逻辑可以拆解为以下8个核心步骤:

(1)寻找Makefile文件

  • make首先会在当前目录下搜索名为Makefilemakefile的文件(注意大小写敏感,推荐统一使用Makefile)。
  • 如果找不到这两个文件,会直接报错"没有规则可制作目标"。

(2)确定最终目标

找到Makefile后,make会将文件中第一个目标作为"最终目标"。

  • 在上面的例子中,第一个目标是myproc(可执行文件)。
  • 因此make的最终任务就是生成myproc

(3) 检查最终目标是否需要更新

确定最终目标后,make会通过两个条件判断是否需要生成/更新myproc

  • myproc不存在:直接执行后续命令生成它;
  • myproc已存在:比较myproc和它的依赖文件myproc.o修改时间
  • 如果myproc.o的修改时间比myproc晚(即myproc.o被更新过),则需要重新生成myproc
  • 反之,若myprocmyproc.o新,说明myproc已是最新,无需操作。

小技巧:可以用touch 文件名命令手动更新文件的修改时间(比如touch myproc.o),测试make是否会重新执行命令。

(4) 从最终目标到中间文件

如果myproc需要更新(或不存在),make会检查它的依赖myproc.o

  • myproc.o不存在:在Makefile中寻找以myproc.o为目标的规则(即myproc.o: myproc.s这一行),然后根据规则生成myproc.o
  • myproc.o已存在:同样比较myproc.o和它的依赖myproc.s的修改时间,判断是否需要重新生成myproc.o

这个过程会逐层递归:

  • 检查myproc.s是否存在/需要更新 → 依赖myproc.i
  • 检查myproc.i是否存在/需要更新 → 依赖myproc.c(源代码文件)。

直到找到最底层的依赖myproc.c------这是我们手动编写的源文件,必须存在(如果myproc.c缺失,make会直接报错退出)。

(5) 反向执行命令:从源文件到最终目标

当确认所有依赖都已处理后,make会按照依赖链的反向顺序执行命令:

  1. 先执行gcc -E myproc.c -o myproc.i:将源文件myproc.c预处理为myproc.i
  2. 再执行gcc -S myproc.i -o myproc.s:将myproc.i编译为汇编文件myproc.s
  3. 接着执行gcc -c myproc.s -o myproc.o:将myproc.s汇编为目标文件myproc.o
  4. 最后执行gcc myproc.o -o myproc:将myproc.o链接为可执行文件myproc

整个过程就像"链式反应"------只有前一个中间文件生成后,才能执行下一个步骤。

(6) 遇到错误立即停止

在依赖检查或命令执行过程中,若出现以下情况,make会直接退出并报错:

  • 某个依赖文件(如myproc.c)不存在;
  • 命令执行失败(如编译错误,返回非0状态码)。

但需要注意:make只负责检查"依赖是否存在"和"命令是否执行",不负责检查命令的语法正确性(比如把gcc写成g++,make会执行命令但因错误退出)。

(7) 核心逻辑:只做"必要的事"

make的高效性体现在"增量编译"------它只会重新生成"过时"的文件。例如:

  • 若只修改了myproc.c:make会重新生成myproc.imyproc.smyproc.omyproc
  • 若只修改了myproc.s:make只会重新生成myproc.omyproc,无需处理myproc.imyproc.c
  • 若所有文件都未修改:make会直接提示"myproc已是最新",不执行任何命令。

二、make/Makefile的扩展语法

  • 刚开始写 Makefile 时,我们可能会像下面这样写
makefile 复制代码
code: code.o
    gcc code.o -o code  # 链接:把 .o 变成可执行文件

code.o: code.s
    gcc -c code.s -o code.o  # 汇编:.s 变 .o

code.s: code.i
    gcc -S code.i -o code.s  # 编译:.i 变 .s

code.i: code.c
    gcc -E code.c -o code.i  # 预处理:.c 变 .i

clean:
    rm -f *.i *.s *.o code  # 清理垃圾文件

这看起来还行,但问题大了:

  • 如果你把 code.c 改名叫 main.c,上面所有提到 code 的地方都得改,漏一个就报错。
  • 要是我们加了个新文件 tool.c,又得复制粘贴一堆规则,累得慌。

1. 变量的妙用

如果把经常用的文件名、命令起个外号,改的时候只改外号,是不是就方便了?

  • 这就是变量的作用

比如下面这样:

makefile 复制代码
BIN=code  # 给可执行文件起个外号叫 BIN
CC=gcc    # 给编译器起个外号叫 CC
SRC=code.c  # 给源文件起个外号叫 SRC
FLAGS=-o   # 给输出参数起个外号叫 FLAGS
RM=rm -f   # 给删除命令起个外号叫 RM

$(BIN):$(SRC)  # 用外号代替具体名字
    $(CC) $(FLAGS) $(BIN) $(SRC)  # 相当于 gcc -o code code.c

clean:
    $(RM) $(BIN)  # 相当于 rm -f code
  • 现在如果要改文件名,比如把 code.c 改成 main.c,只需要改 SRC=main.c 就行,其他地方不用动。

2. 自动变量

有时候规则里的文件名会重复。

  • 比如 gcc -o code code.o 里,code 出现了两次。要是文件名很长,写起来超麻烦。这时候"自动变量"就派上用场了,它们能自动代表规则里的目标或依赖文件。

常用的有三个:

  • $@:代表当前规则的"目标文件"(比如上面的 code)。
  • $^:代表当前规则的"所有依赖文件"(比如上面的 code.o)。
  • $<:代表当前规则的"第一个依赖文件"(比如只有一个依赖时,和 $^ 一样)。

举个例子:

makefile 复制代码
$(BIN):$(OBJ)    
    $(CC) -o $@ $^  # 相当于 gcc -o code code.o($@ 是 code,$^ 是 code.o)
    @echo "正在把 $^ 变成 $@"  # 会打印:正在把 code.o 变成 code

%.o:%.c  # 后面会讲这个,先关注自动变量
    $(CC) -c $<  # 相当于 gcc -c code.c($< 是 code.c)
    @echo "正在把 $< 变成 $@"  # 会打印:正在把 code.c 变成 code.o

是不是像用了"占位符"?不用手写具体文件名,Makefile 自动帮你填,少写好多字。

3. 批量处理

如果你的项目有多个 .c 文件(比如 a.cb.cc.c),总不能每个都写一条编译规则吧?这时候就需要"模式规则"和"通配符"来批量干活。

(1)模式规则:一键编译所有 .c 文件

模式规则用 % 当通配符,比如 %.o: %.c 表示:"所有 .o 文件都由对应的 .c 文件生成"。

makefile 复制代码
%.o: %.c  # 只要有 x.c,就自动生成 x.o
    $(CC) -c $< -o $@  # 对每个 .c 文件执行:gcc -c x.c -o x.o
bash 复制代码
%.o: %.c  # 只要有 x.c,就自动生成 x.o

现在不管你有 a.cb.c 还是 c.c,这条规则都能自动处理,不用一个一个写!

(2)通配符函数:自动找文件、改名字

还有两个超实用的工具:

  • wildcard:帮你找出所有符合条件的文件。比如 SRC=$(wildcard *.c),它会自动收集当前文件夹里所有 .c 文件,不管有多少个。
  • patsubst:帮你批量改文件名。比如 OBJ=$(patsubst %.c,%.o,$(SRC)),意思是"把 SRC 里所有 .c 结尾的文件,改成 .o 结尾"。

举个例子:如果当前有 a.cb.c,那么:

makefile 复制代码
SRC=$(wildcard *.c)  # SRC 就等于 "a.c b.c"
OBJ=$(SRC:.c=.o)     # 这是上面那句的简写,OBJ 就等于 "a.o b.o"

这下不用手动列所有文件了,Makefile 会自动帮你找齐。

4. 高级小技巧

命令前的 @ 和 -

  • 命令前加 @:只显示命令的结果,不显示命令本身。比如 @echo "编译中...",只会打印"编译中...",不会显示 echo "编译中...",看起来更清爽。
  • 命令前加 -:就算命令执行失败,也继续往下跑。比如 -rm -f *.o,就算没有 .o 文件,也不会报错中断。

5. 完整示例

最后来看一个能应付大多数小项目的 Makefile,我们一步步拆开看:

makefile 复制代码
BIN=proc  # 可执行文件名叫 proc
CC=gcc    # 用 gcc 编译
SRC=$(wildcard *.c)  # 自动找所有 .c 文件
OBJ=$(SRC:.c=.o)     # 把 .c 换成 .o,比如 a.c → a.o
LFLAGS=-o  # 链接时的输出参数
FLAGS=-c   # 编译时的参数(-c 表示只编译不链接)
RM=rm -f   # 删除命令

# 第一步:链接所有 .o 文件,生成可执行文件 proc
$(BIN):$(OBJ)    
    @$(CC) $(LFLAGS) $@ $^  # 相当于 gcc -o proc a.o b.o(自动找所有 .o)
    @echo "链接完成:把 $^ 变成了 $@"    

# 第二步:编译每个 .c 文件成 .o 文件
%.o:%.c                            
    @$(CC) $(FLAGS) $<  # 相当于 gcc -c a.c(自动处理每个 .c)
    @echo "编译中:$< → $@"

# 声明伪目标
.PHONY: clean test
clean:  # 清理垃圾文件
    $(RM) $(OBJ) $(BIN)  # 删除所有 .o 和 proc
    @echo "清理完毕!"
    
test:  # 测试变量内容
    @echo "源文件列表:$(SRC)" 
    @echo "目标文件列表:$(OBJ)"

这个 Makefile 会干以下事情:

  1. 自动找出当前文件夹所有 .c 文件(比如 a.cb.c)。
  2. 自动算出需要生成的 .o 文件(a.ob.o)。
  3. 逐个把 .c 编译成 .o(不用手动写每个规则)。
  4. 把所有 .o 链接成可执行文件 proc
  5. 提供 make clean 清理垃圾,make test 查看文件列表。

以上就是这篇博客的全部内容,下一篇我们将继续探索Linux的更多精彩内容。

我的个人主页

欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343

我的Linux知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12879535.html?spm=1001.2014.3001.5482

|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |

相关推荐
Yana.nice1 小时前
openssl将证书从p7b转换为crt格式
java·linux
AI逐月1 小时前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
想逃离铁厂的老铁1 小时前
Day55 >> 并查集理论基础 + 107、寻找存在的路线
java·服务器
小白跃升坊1 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey2 小时前
【Linux】线程同步与互斥
linux·笔记
杨江2 小时前
seafile docker安装说明
运维
舰长1152 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
好好沉淀2 小时前
Docker开发笔记(详解)
运维·docker·容器
zmjjdank1ng2 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.2 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居