当前位置: 首页 > news >正文

硬件嵌入式工程师学习路线终极总结(二):Makefile用法及变量——你的项目“自动化指挥官”!

嵌入式工程师学习路线大总结(三):Makefile用法及变量——你的项目“自动化指挥官”!

引言:Makefile——大型项目的“智能管家”!

兄弟们,想象一下,你正在开发一个复杂的嵌入式系统,比如一个智能家居网关。这个项目可能包含:

  • 几十个C语言源文件(.c),分散在 srcdriversprotocol 等多个目录下。

  • 几十个头文件(.h),定义了各种接口和数据结构。

  • 依赖于各种第三方库(如网络协议栈库、加密库)。

  • 需要针对不同的ARM芯片(比如Cortex-M4、Cortex-A7)进行交叉编译。

  • 还需要区分调试版本(带调试信息)和发布版本(优化代码)。

面对这样的“巨无霸”项目,你还能手动敲 gcc -I... -L... -l... src/a.c drivers/b.c ... -o app 吗?

  • 每次修改一个文件,难道要重新编译所有文件吗?那编译一次得等多久?

  • 哪个文件依赖哪个头文件?哪个C文件需要先编译?这些依赖关系怎么维护?

  • 编译选项一改,所有文件都要跟着改,容易出错怎么办?

这就是 Makefile 登场的时刻!

Makefile,顾名思义,就是“制造文件”的规则文件。它是一个文本文件,其中包含了编译、链接等构建项目所需的所有规则和指令。它就像你的项目“自动化指挥官”或“智能管家”,能够:

  • 自动化编译:你只需敲一个 make 命令,它就能自动完成编译、链接所有必要的文件。

  • 智能增量编译:它能识别哪些文件被修改过,只重新编译那些修改过的文件及其依赖的文件,大大节省编译时间。

  • 管理复杂依赖:清晰地定义文件之间的依赖关系,确保编译顺序正确。

  • 灵活配置:通过变量和条件判断,轻松切换编译选项、目标平台、调试/发布模式。

在嵌入式开发中,Makefile几乎是所有项目的标配!从Linux内核、U-Boot等大型开源项目,到你日常的应用程序开发,都离不开Makefile。掌握它,你就掌握了大型项目构建的“命脉”!

今天,咱们就来彻底搞懂Makefile的方方面面,让你把这个“自动化指挥官”玩得炉火纯青!

第一阶段:Makefile基础——认识你的“指挥官”!(建议2-3周)

这个阶段,咱们先认识Makefile的基本结构和核心概念,就像学习如何给你的“指挥官”下达最简单的命令。

3.1 Makefile的核心概念:规则、目标、依赖与命令

一个Makefile文件由一系列的**规则(Rules)组成。每个规则都定义了如何从一个或多个依赖(Prerequisites)文件生成一个目标(Target)**文件。

规则的基本格式:

# 这是一个Makefile规则的通用格式
# 注意:命令(command)行必须以 Tab 键开头,而不是空格!这是Makefile最常见的“坑”!target: prerequisitescommandcommand...
  • 目标(Target)

    • 通常是要生成的文件名(如可执行文件 app,或中间目标文件 main.o)。

    • 也可以是一个伪目标(Phony Target),它不对应实际的文件,只表示一个动作(如 clean 清理文件)。

  • 依赖(Prerequisites)

    • 生成目标文件所需要的文件列表。

    • 当依赖文件比目标文件新,或者目标文件不存在时,Make工具就会执行规则中的命令来重新生成目标。

  • 命令(Command)

    • Make工具为了生成目标而执行的Shell命令。

    • 切记:每条命令前必须是一个 Tab 字符,而不是空格!

逻辑分析:Make工具的工作原理

当你输入 make 命令时,Make工具会:

  1. 查找默认目标:如果没有指定目标,Make会执行Makefile中定义的第一个目标。

  2. 检查目标

    • 如果目标文件不存在,或者

    • 目标文件存在,但它的任何一个依赖文件比目标文件更新(通过文件的时间戳判断),

    • 那么Make就会认为目标是“过时”的,需要重新生成。

  3. 递归处理依赖:为了生成“过时”的目标,Make会首先递归地检查其所有依赖文件。如果依赖文件本身也是某个规则的目标,Make会先尝试生成这些依赖文件。

  4. 执行命令:当所有依赖文件都已最新或已生成后,Make就会执行当前规则下的所有命令,从而生成目标文件。

这个过程就是Make实现增量编译的核心机制。它只编译需要重新编译的部分,大大节省了时间。

代码示例:最简单的Makefile

我们从一个最简单的C程序开始,看看如何用Makefile编译它。

文件结构:

.
├── main.c
└── Makefile

main.c

// main.c
#include <stdio.h>int main() {printf("Hello, Makefile!\n");return 0;
}

Makefile

# Makefile
# 这是一个最简单的Makefile示例# 目标:all (伪目标,通常用于编译所有内容)
# 依赖:app (表示all依赖于app这个可执行文件)
all: app# 目标:app (可执行文件)
# 依赖:main.o (表示app依赖于main.o这个目标文件)
app: main.o# 命令:使用gcc链接main.o生成app可执行文件# 注意:这一行前面必须是Tab键!gcc main.o -o app# 目标:main.o (目标文件)
# 依赖:main.c (表示main.o依赖于main.c这个源文件)
main.o: main.c# 命令:使用gcc编译main.c生成main.o目标文件# -c 表示只编译不链接# 注意:这一行前面必须是Tab键!gcc -c main.c -o main.o# 伪目标:clean (用于清理生成的文件)
# .PHONY 声明clean是一个伪目标,避免与实际文件冲突
.PHONY: clean
clean:# 命令:删除app可执行文件和main.o目标文件# -f 表示强制删除,不提示rm -f app main.o

逻辑分析:

  1. 当你执行 make 时,Make会默认执行第一个目标 all

  2. all 依赖于 app。所以Make会先去检查 app 这个目标。

  3. app 依赖于 main.o。所以Make会再检查 main.o 这个目标。

  4. main.o 依赖于 main.c

    • 如果 main.o 不存在,或者 main.cmain.o 新,Make就会执行 gcc -c main.c -o main.o 命令来生成 main.o

    • 如果 main.o 已经存在且比 main.c 新,Make就认为 main.o 是最新的,不需要重新编译。

  5. main.o 准备好后,Make会回到 app 目标。

    • 如果 app 不存在,或者 main.oapp 新,Make就会执行 gcc main.o -o app 命令来生成 app

    • 如果 app 已经存在且比 main.o 新,Make就认为 app 是最新的,不需要重新链接。

  6. 最后,all 目标完成。

当你执行 make clean 时,Make会直接执行 clean 规则下的 rm -f app main.o 命令,清理生成的文件。

3.2 Makefile中的变量:让你的Makefile更灵活!

在Makefile中,你可以定义和使用变量,这大大增加了Makefile的灵活性和可维护性。想象一下,如果你想把编译器从 gcc 换成 arm-linux-gnueabihf-gcc,或者添加一个编译选项,你只需要修改一个变量的值,而不需要修改所有规则中的命令。

变量的定义和使用:

# 变量定义
VAR_NAME = value
ANOTHER_VAR := another_value# 变量使用
$(VAR_NAME)
${ANOTHER_VAR}
  • 定义:变量名通常是大写,使用 =:= 进行赋值。

  • 使用:使用 $(VAR_NAME)${VAR_NAME} 来引用变量的值。通常推荐使用 $(VAR_NAME)

变量的分类:

Makefile中的变量根据其展开方式和来源,可以分为:

  1. 自定义变量:由用户在Makefile中定义。

  2. 自动变量:由Make工具在执行规则时自动设置的特殊变量。

  3. 隐含变量:Make工具内置的,与特定命令(如编译、链接)相关的变量。

3.2.1 自定义变量详解:你的“自定义参数”

自定义变量是你在Makefile中最常用的变量类型。它们有不同的赋值方式,理解这些区别对于编写健壮的Makefile至关重要。

1. 递归展开变量 (=)
  • 特点:在变量被使用时才进行展开。如果变量的值中包含对其他变量的引用,这些引用会在使用时递归地展开。

  • 优点:可以引用后续定义的变量。

  • 缺点:可能导致无限递归(如果变量循环引用自身),或者在复杂情况下难以预测其最终值。

# 递归展开变量示例
# 文件名: vars_recursive.mk# 变量 A 引用了变量 B
A = $(B) World
# 变量 B 在 A 之后定义
B = Hello# 当引用 A 时,B 才会被展开
# 预期输出: Hello World
print_A:@echo "A = $(A)"# 另一个例子:可能导致无限递归
# X = $(Y)
# Y = $(X)
# make print_X 会报错:Recursive variable 'X' references itself (eventually)

代码示例:递归展开变量

# Makefile
# 文件名: recursive_vars_demo.mk# 定义一个递归展开变量 MESSAGE
# 它引用了另一个变量 GREETING,而 GREETING 在 MESSAGE 之后定义
MESSAGE = $(GREETING) World!
GREETING = Hello# 定义一个目标,用于打印 MESSAGE 的值
# 当 make print_message 时,MESSAGE 会被展开,此时 GREETING 已经定义
print_message:@echo "MESSAGE = $(MESSAGE)" # 预期输出: MESSAGE = Hello World!# 演示递归引用自身导致的问题
# X = $(Y)
# Y = $(X)
# print_recursive_error:
#	@echo "X = $(X)"
# 运行 make print_recursive_error 会报错:Recursive variable 'X' references itself (eventually).PHONY: print_message # 声明伪目标

运行 make print_message 结果:

MESSAGE = Hello World!

逻辑分析: MESSAGE 在定义时并没有立即计算 $(GREETING) 的值,而是保留了对 GREETING 的引用。直到 print_message 目标中的 $(MESSAGE) 被实际使用时,GREETING 才被查找并展开为 Hello,最终 MESSAGE 的值变为 Hello World!

2. 简单展开变量 (:=)
  • 特点:在定义时立即展开。如果变量的值中包含对其他变量的引用,这些引用会在定义时立即展开。

  • 优点:不会导致无限递归,值是确定的,更易于理解和调试。

  • 缺点:不能引用后续定义的变量。

# 简单展开变量示例
# 文件名: vars_simple.mk# 变量 A 引用了变量 B
A := $(B) World
# 变量 B 在 A 之后定义
B = Hello# 当引用 A 时,B 在 A 定义时就已经展开了(此时 B 还没定义,所以是空)
# 预期输出: A =  World
print_A:@echo "A = $(A)"# 另一个例子:引用已定义的变量
C := Static Value
D := $(C) Dynamic Value
C = New Static Value # 这里的修改不会影响 D,因为 D 在定义时已经展开了 C 的值
# 预期输出: D = Static Value Dynamic Value
print_D:@echo "D = $(D)"

代码示例:简单展开变量

# Makefile
# 文件名: simple_vars_demo.mk# 定义一个简单展开变量 MESSAGE_SIMPLE
# 它引用了另一个变量 GREETING_SIMPLE,但 GREETING_SIMPLE 在 MESSAGE_SIMPLE 之后定义
MESSAGE_SIMPLE := $(GREETING_SIMPLE) World!
GREETING_SIMPLE = Hello# 定义一个目标,用于打印 MESSAGE_SIMPLE 的值
# 当 make print_message_simple 时,MESSAGE_SIMPLE 在定义时就已展开,此时 GREETING_SIMPLE 还没定义
print_message_simple:@echo "MESSAGE_SIMPLE = $(MESSAGE_SIMPLE)" # 预期输出: MESSAGE_SIMPLE =  World! (GREETING_SIMPLE为空)# 演示简单展开变量的确定性
VAR1 := Initial Value
VAR2 := $(VAR1) Appended Value
VAR1 = Changed Value # VAR1 的改变不会影响 VAR2,因为 VAR2 在定义时已经展开了 VAR1 的值print_var2:@echo "VAR2 = $(VAR2)" # 预期输出: VAR2 = Initial Value Appended Value.PHONY: print_message_simple print_var2

运行 make print_message_simple 结果:

MESSAGE_SIMPLE =  World!

运行 make print_var2 结果:

VAR2 = Initial Value Appended Value

逻辑分析: MESSAGE_SIMPLE 在定义时就立即计算了 $(GREETING_SIMPLE) 的值。由于此时 GREETING_SIMPLE 尚未定义,所以它被展开为空字符串。VAR2 同理,在定义时就固定了 VAR1 的值。

总结:= vs :=

特性

= (递归展开)

:= (简单展开)

展开时机

使用时展开

定义时立即展开

引用后续变量

可以

不可以

递归问题

可能导致无限递归

不会

确定性

结果可能不确定,取决于使用时的上下文

结果确定,易于预测

性能

每次使用都重新展开,可能稍慢

定义时一次性展开,后续使用更快

推荐用法

较少使用,除非需要引用后续定义的变量,且能确保无递归

推荐使用,尤其是在定义复杂变量或避免副作用时

3. 条件赋值变量 (?=)
  • 特点:如果变量没有被定义过,则进行赋值;如果已经定义过,则不做任何操作。

  • 用途:为变量提供默认值。

# 条件赋值变量示例
# 文件名: vars_conditional.mk# 如果 CC 没有定义,则赋值为 gcc
CC ?= gcc# 如果 CC 已经通过命令行或环境变量定义了,这里就不会覆盖
# 例如:make CC=clang print_cc
# 或者:export CC=clang; make print_ccprint_cc:@echo "CC = $(CC)"# 再次尝试定义,不会生效
CC ?= clang # 此时 CC 已经定义为 gcc,所以这里不会生效print_cc_again:@echo "CC (again) = $(CC)"

代码示例:条件赋值变量

# Makefile
# 文件名: conditional_vars_demo.mk# 1. 第一次定义 CC,如果 CC 未定义,则赋值为 gcc
CC ?= gcc
print_cc_1:@echo "CC (第一次定义) = $(CC)"# 2. 再次使用 ?= 赋值,此时 CC 已经定义,所以不会改变
CC ?= clang
print_cc_2:@echo "CC (第二次定义) = $(CC)"# 3. 演示命令行参数优先
# 运行:make print_cc_cmd CC_CMD=arm-gcc
CC_CMD ?= default-gcc
print_cc_cmd:@echo "CC_CMD = $(CC_CMD)".PHONY: print_cc_1 print_cc_2 print_cc_cmd

运行 make print_cc_1 结果:

CC (第一次定义) = gcc

运行 make print_cc_2 结果:

CC (第二次定义) = gcc

运行 make print_cc_cmd CC_CMD=arm-gcc 结果:

CC_CMD = arm-gcc

逻辑分析: ?= 只有在变量未定义时才赋值。这在Makefile中非常有用,可以为用户提供灵活的配置接口,同时提供合理的默认值。命令行传入的变量会覆盖Makefile中的定义。

4. 追加赋值 (+=)
  • 特点:向变量的当前值追加内容。

  • 用途:向编译选项、源文件列表等变量中添加新的值。

# 追加赋值变量示例
# 文件名: vars_append.mkCFLAGS = -Wall -Wextra# 追加新的编译选项
CFLAGS += -O2 -gprint_cflags:@echo "CFLAGS = $(CFLAGS)" # 预期输出: CFLAGS = -Wall -Wextra -O2 -g

代码示例:追加赋值变量

# Makefile
# 文件名: append_vars_demo.mk# 定义初始编译选项
CFLAGS = -Wall -Wextra -std=c99# 追加新的编译选项
CFLAGS += -O2 # 优化级别2
CFLAGS += -g  # 添加调试信息# 定义源文件列表
SRCS = main.c module1.c# 追加新的源文件
SRCS += module2.c module3.cprint_vars:@echo "CFLAGS = $(CFLAGS)" # 预期输出: CFLAGS = -Wall -Wextra -std=c99 -O2 -g@echo "SRCS = $(SRCS)"     # 预期输出: SRCS = main.c module1.c module2.c module3.c.PHONY: print_vars

运行 make print_vars 结果:

CFLAGS = -Wall -Wextra -std=c99 -O2 -g
SRCS = main.c module1.c module2.c module3.c

逻辑分析: += 运算符非常方便,它允许你在不覆盖原有值的情况下,向变量中添加新的元素。这在构建复杂的编译选项列表或文件列表时非常实用。

5. Shell赋值 (!=)
  • 特点:将Shell命令的执行结果赋值给变量。

  • 用途:获取系统信息、执行一些外部工具的命令结果。

# Shell赋值变量示例
# 文件名: vars_shell.mk# 获取当前日期和时间
CURRENT_DATETIME != date "+%Y-%m-%d %H:%M:%S"# 获取当前目录下的所有.c文件
C_FILES != find . -name "*.c"print_info:@echo "当前时间: $(CURRENT_DATETIME)"@echo "当前目录下的C文件: $(C_FILES)"

代码示例:Shell赋值变量

# Makefile
# 文件名: shell_vars_demo.mk# 获取当前日期和时间
BUILD_DATE_TIME != date "+%Y-%m-%d %H:%M:%S"# 获取当前工作目录
CURRENT_DIR != pwd# 获取系统CPU核心数
CPU_CORES != nproc# 模拟一个复杂的命令输出,并赋值给变量
# 假设有一个脚本 get_version.sh 会打印版本号
# 创建一个模拟脚本
.PHONY: create_mock_script
create_mock_script:@echo "#!/bin/bash" > get_version.sh@echo "echo 'V1.2.3-beta'" >> get_version.sh@chmod +x get_version.sh# 确保模拟脚本存在
VERSION_INFO != ./get_version.shprint_shell_vars: create_mock_script@echo "构建日期和时间: $(BUILD_DATE_TIME)"@echo "当前工作目录: $(CURRENT_DIR)"@echo "系统CPU核心数: $(CPU_CORES)"@echo "版本信息: $(VERSION_INFO)".PHONY: print_shell_vars clean_mock_script
clean_mock_script:@rm -f get_version.sh

运行 make print_shell_vars 结果:

构建日期和时间: 2024-07-04 22:30:00 (具体时间)
当前工作目录: /path/to/your/directory
系统CPU核心数: 8 (取决于你的CPU)
版本信息: V1.2.3-beta

逻辑分析: != 运算符允许Makefile在构建过程中执行Shell命令,并将命令的标准输出作为变量的值。这在需要动态获取信息(如日期、版本号、文件列表)时非常有用。

变量总结:

赋值符

名称

展开时机

特点

适用场景

=

递归展开

使用时

引用后续变量,可能递归

较少使用,除非特殊需求

:=

简单展开

定义时

立即展开,值确定,无递归

最常用,定义确定值的变量

?=

条件赋值

定义时

变量未定义时才赋值

提供默认值,用户可覆盖

+=

追加赋值

定义时

向变量追加内容

累加编译选项、源文件列表

!=

Shell赋值

定义时

执行Shell命令,将输出作为变量值

动态获取系统信息、命令结果

http://www.dtcms.com/a/267598.html

相关文章:

  • 深度学习5(深层神经网络 + 参数和超参数)
  • Ubuntu 20.04 编译安装FFmpeg及错误分析与解决方案
  • 数据结构:数组:插入操作(Insert)与删除操作(Delete)
  • PageRank:互联网的马尔可夫链平衡态
  • 利用已有的 PostgreSQL 和 ZooKeeper 服务,启动dolphinscheduler-standalone-server3.1.9 镜像
  • Redis基础(6):SpringDataRedis
  • Java创建型模式---工厂模式
  • java多线程--死锁
  • CppCon 2018 学习:Standard Library Compatibility Guidelines (SD-8)
  • 未成功,做个记录,SelfHost.HttpSelfHostServer 如何加载证书
  • 【Prometheus】Grafana、Alertmanager集成
  • 小架构step系列05:Springboot三种运行模式
  • 理想汽车6月交付36279辆 第二季度共交付111074辆
  • 基于微信小程序的校园跑腿系统
  • MySQL——9、事务管理
  • Java-继承
  • 远程协助软件:Git的用法
  • STM32第15天串口中断接收
  • 数据结构:数组抽象数据类型(Array ADT)
  • oracle的内存架构学习
  • Hashcat 最快密码恢复工具实践指南
  • jvm架构原理剖析篇
  • C++ Qt 基础教程:信号与槽机制详解及 QPushButton 实战
  • virtualbox+vagrant私有网络宿主机无法ping通虚拟机问题请教
  • Apache 配置文件提权的实战思考
  • 数据库-元数据表
  • docker容器中Mysql数据库的备份与恢复
  • Java的AI新纪元:Embabel如何引领智能应用开发浪潮
  • 一文讲清楚React中setState的使用方法和机制
  • 应用标签思路参考