makefile简单初探索_2 结合bsp
Keruone
系统 ubuntu20.04
参考正点原子
只是学习中自己的小记
某种意义上来说也是上一篇的后续
- makefile简单初探索_2 结合bsp
- null
- 1. 工具链统一配置:打造通用 “烹饪工具”
- 2. 路径集中管理:打点食材来源
- 3. 食材预处理:自动生成编译所需的文件列表
- 3.1 头文件路径格式转换
- 3.2 源文件列表自动收集
- 3.3 去除文件路径:提取文件名
- 3.4 生成 .o 文件路径:统一存放目标文件
- 3.5 配置依赖文件搜索路径:告诉 Make 去哪里找源文件
- 4. 实际编译汇编部分:烧大菜
- 5. 辅助操作:清理与调试检测
- 6. 最终完整代码,含注释
- 参考资料
在上篇文章中,我们编写了一个很小的 makefile 也算是作为了 makefile 的小小入门。
但是在上篇文章中,还有几个不可避免的问题:
-
- 编译器可能也会更换,尤其是在交叉编译场景下,不同架构需要不同工具链
-
- 文件不可能永远只有那么几个,随着你的工程项目变大,文件肯定也越来越多,手动列举每个文件会非常繁琐
-
- 汇编过程后的 .o 文件不可能这么随意的放在当前工作环境的根目录,这样太杂乱了,不利于项目管理和清理
-
- 头文件和源文件可能分散在不同目录,需要统一指定搜索路径才能避免编译错误
1. 工具链统一配置:打造通用 “烹饪工具”
首先,为了解决编译器更换的问题,我们可以通过变量统一管理编译工具链,尤其是在嵌入式开发中常用的交叉编译场景。
CROSS_COMPILE ?= arm-linux-gnueabihf-
TARGET ?= keycCC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
:=:在 Makefile 中,:=表示 “立即赋值”,即变量的值在定义时就会被确定,后续即使修改了赋值语句中依赖的其他变量,该变量的值也不会再改变,这种方式可以避免变量值的延迟解析导致意外问题。
这里使用 ?= 赋值,意味着如果外部已经定义了 CROSS_COMPILE(比如通过命令行传入),就会使用外部定义的值,否则使用默认的 arm-linux-gnueabihf-。这样一来,当需要更换编译器(比如切换到其他架构的工具链)时,只需修改 CROSS_COMPILE 变量即可,无需逐个修改后面的编译工具定义,极大提升了灵活性。
TARGET 变量则用于统一指定最终生成的目标文件名,后续所有生成文件(如 .bin、.elf、.dis)都会基于这个变量命名,方便整体修改项目名称。
2. 路径集中管理:打点食材来源
前面我们已经配置好编译工具链这些 “烹饪工具”,接下来就要整理项目所需的 “食材”—— 头文件和源文件的路径。实际项目中,文件常按功能模块分散存放,统一管理路径能让编译过程更有序,也便于后续维护。
-
首先是头文件路径定义:
INCDIRS := imx6u \bsp/clk \bsp/delay \bsp/led \bsp/beep \bsp/key \bsp/gpio \projectINCDIRS变量集中存放所有头文件(.h)的所在目录:imx6u目录通常存放芯片相关的底层头文件,bsp下的子目录(clk、delay 等)对应时钟、延时、LED 等外设的驱动头文件,project目录则用于存放项目业务逻辑相关的头文件。每行末尾的 \ 是续行符,用于将长路径列表拆分成多行,提升代码可读性。
-
接着是源文件路径定义:
SRCDIRS := project \bsp/clk \bsp/delay \bsp/led \bsp/beep \bsp/key \bsp/gpioSRCDIRS变量记录所有源文件(.s汇编文件、.cC 语言文件)的所在目录,与头文件路径一一对应,按功能模块分类存放。这样后续编译时,Makefile 能根据这些路径自动查找需要编译的文件,无需手动逐个指定;若后续新增功能模块,只需在该变量中补充对应目录即可,扩展性极强。
通过这种集中管理方式,我们将分散的文件路径 “收纳” 整齐,为后续自动搜索文件、生成编译列表做好了铺垫。
3. 食材预处理:自动生成编译所需的文件列表
有了头文件和源文件的路径(即 "食材存放位置"),接下来需要对这些 "食材" 进行预处理 —— 也就是通过 Makefile 函数自动生成编译过程中需要的文件列表和路径参数,避免手动逐个指定文件的繁琐操作。
3.1 头文件路径格式转换
首先,我们在使用 gcc 编译时,引用头文件是常态,但编译器默认只会在当前目录和系统默认目录找头文件。如果头文件在其他目录(比如我们定义的 imx6u、bsp/clk),就必须通过 -I 选项告诉编译器 "去这个目录找头文件"。
到目前为止,我们只有 INCDIRS 里的相对路径,没有 -I 前缀,编译器无法识别。其实解决这个问题不用绕弯,只要给每个路径前面加上 -I 即可 —— 而 Makefile 的 patsubst 函数正好能帮我们实现批量替换。
- 其中
patsubst函数语法如下$(patsubst <pattern>,<replacement>,<text>)- 首先,括号内的
patsubst为该函数的函数名。 <text>为 需要处理的 集合<pattern>为 对<text>集合中各元素的匹配条件<replacement>为 对 符合条件的元素 的替换方案
- 首先,括号内的
对此,我们就可以很好的利用这个函数,来达到批量添加 -I 的操作:
INCLUDE := $(patsubst %, -I %, $(INCDIRS))
- 处理逻辑:
%是通配符,匹配INCDIRS中的每一个路径(比如 imx6u、bsp/clk);-I % 表示将匹配到的每个路径替换为 "-I + 该路径"。 - 举个例子:如果
INCDIRS里有imx6u和bsp/clk,最终INCLUDE会变成-I imx6u-I bsp/clk,后续传给 gcc 后,编译器就知道要去这两个目录搜索头文件了。
3.2 源文件列表自动收集
接下来,项目中源文件会分散在 SRCDIRS 的各个目录下,随着文件数量增多,手动列举每个 .s(汇编文件)和 .c(C 语言文件)既繁琐又容易遗漏。我们需要一个能自动遍历所有指定目录、批量收集目标文件的方案 —— 这就需要结合 Makefile 的 foreach 和 wildcard 两个函数来实现。
- 首先介绍 foreach 函数,语法如下
$(foreach <var>,<list>,<text>)- 括号内的 foreach 为该函数的函数名。
<list>为需要遍历的集合(比如我们定义的 SRCDIRS 路径列表)。<var>为循环变量,用于接收<list>中逐一取出的元素。<text>为循环体,每次遍历会将<var>代入<text>中执行表达式。
- 再介绍 wildcard 函数,语法如下
$(wildcard <pattern>)- 括号内的 wildcard 为该函数的函数名。
<pattern>为文件匹配模式(支持通配符 *)。- 核心作用:匹配指定模式的所有文件路径,返回以空格分隔的文件名列表。
结合这两个函数,我们就能实现源文件的自动收集,具体代码如下:
# 包含路径的各个源码文件
SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.s))
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
- 处理逻辑:
- 先执行
foreach dir, $(SRCDIRS), ...:遍历SRCDIRS中的每个目录,将当前目录赋值给循环变量dir(比如第一次取project,第二次取bsp/clk)。 - 再执行
wildcard $(dir)/*.s:针对当前dir,匹配该目录下所有.s后缀的文件($(dir)/*.c同理匹配.c文件),返回带完整路径的文件名(比如project/start.s、bsp/led/led.c)。 - 循环结束后,将所有目录下的目标文件汇总成
SFILES(.s文件列表)和CFILES(.c文件列表)。
- 先执行
- 举个例子:如果
SRCDIRS包含project和bsp/key,SFILES会自动收集project/*.s和bsp/key/*.s所有文件,无需手动添加新文件路径。
3.3 去除文件路径:提取文件名
现在我们已经通过 SFILES 和 CFILES 整理好了所有源文件的"来源路径"(比如 bsp/key/key.c、project/start.s),但后续编译生成的 .o 文件需要统一放在 obj 目录集中管理,此时文件的路径前缀反而成了多余——我们只需要文件名本身,就能对应生成 obj/key.o 这样的目标文件。
因此,我们需要剥离文件路径中的目录部分,只保留纯文件名——这可以通过 Makefile 的 notdir 函数实现。
- 其中
notdir函数语法如下$(notdir <names>)- 括号内的
notdir为该函数的函数名。 为需要处理的文件路径列表(可以是单个路径或多个路径集合)。 - 核心作用:剥离文件路径中的目录部分,只保留纯文件名。
- 括号内的
具体代码实现如下:
# 无路径的各个源码文件
SFILES_NO_DIR := $(notdir $(SFILES))
CFILES_NO_DIR := $(notdir $(CFILES))
- 处理逻辑:将
SFILES和CFILES中带路径的文件列表传入notdir函数,该函数会自动剥离所有目录前缀,只保留文件名。 - 举个例子:
bsp/key/key.c会被剥离成key.c,project/start.s会被剥离成start.s,最终得到只包含纯文件名的列表SFILES_NO_DIR和CFILES_NO_DIR,为后续统一存放.o文件做好准备。
3.4 生成 .o 文件路径:统一存放目标文件
我们已经提取出了源文件的文件名,接下来编译过程会生成 .o 目标文件。这些目标文件不能再随意散放在根目录,否则会让项目结构杂乱无章。因此,我们需要将所有目标文件统一放在 obj 目录下。
要实现这个需求,需要结合 patsubst 函数(语法同 3.1)和 Makefile 自带的后缀替换语法,既完成文件名后缀的转换,又添加统一的目录前缀。
- 其中
patsubst函数语法如下(复习)$(patsubst <pattern>,<replacement>,<text>)- 括号内的
patsubst为该函数的函数名。 为需要处理的集合(这里是纯文件名列表)。 为对 集合中各元素的匹配条件(用通配符 %匹配所有文件名)。为对符合条件的元素的替换方案(这里是添加 obj/目录前缀)。
- 括号内的
具体代码实现如下:
# 源码文件对应的目标文件 .o
SOBJS := $(patsubst %, obj/%, $(SFILES_NO_DIR:.s=.o))
COBJS := $(patsubst %, obj/%, $(CFILES_NO_DIR:.c=.o))
OBJS := $(SOBJS) $(COBJS)
- 处理逻辑:
- 先做后缀替换:
SFILES_NO_DIR:.s=.o表示将纯文件名列表中所有.s后缀(汇编文件)替换为.o后缀(比如start.s→start.o);CFILES_NO_DIR:.c=.o同理,将.c后缀(C语言文件)替换为.o后缀(比如led.c→led.o)。 - 再用
patsubst函数添加路径前缀:patsubst %, obj/%, ...中,%通配符匹配每个转换后的.o文件名,将其替换为obj/ + 文件名(比如start.o→obj/start.o)。 - 最后用
OBJS变量汇总所有目标文件的完整路径,后续链接时直接调用这个列表即可。
- 先做后缀替换:
- 举个例子:
SFILES_NO_DIR中的start.s最终会变成obj/start.o,CFILES_NO_DIR中的led.c会变成obj/led.o,所有.o文件都集中存放在obj目录。
3.5 配置依赖文件搜索路径:告诉 Make 去哪里找源文件
现在我们已经整理好了源文件的存放路径(SRCDIRS)、收集了带路径的源文件列表(SFILES、CFILES),也提取了文件名本身,但 Make 工具默认只会在当前目录查找源文件(.s、.c)。如果源文件不在当前目录,直接编译会提示"找不到文件"。
因此,我们需要明确告诉 Make 工具源文件的具体存放位置,让它能在 SRCDIRS 的各个目录中找到需要的源文件,无需手动写完整路径。这可以通过 Makefile 的内置变量 VPATH 实现。
这里需要特别说明一个关键区别:前面定义的 INCLUDE 变量和现在的 VPATH 看似都是"路径配置",但作用对象和场景完全不同,缺一不可:
INCLUDE(带-I前缀的头文件路径):是给 编译器(gcc) 用的。编译器在编译.c/.s文件时,会遇到#include "xxx.h"这样的头文件引用,此时需要通过-I路径告诉编译器"去哪里找这些.h头文件"。VPATH(源文件搜索路径):是给 Make 工具 用的。Make 在执行编译规则时,需要找到依赖的.c/.s源文件(比如编译obj/led.o时需要依赖led.c),此时需要通过VPATH告诉 Make"去哪里找这些.c/.s源文件"。
简单说:INCLUDE 解决"编译器找头文件(.h)"的问题,VPATH 解决"Make 找源文件(.c/.s)"的问题,二者针对不同工具、不同文件类型,是两个完全独立的路径配置,不能相互替代。
- 其中
VPATH变量的用法如下VPATH := <路径列表>VPATH是 Make 的内置变量,专门用于指定依赖文件的搜索路径,无需额外定义函数。- <路径列表> 为多个目录的集合(用空格分隔,这里直接复用
SRCDIRS的路径集合)。 - 核心作用:当 Make 查找某个依赖文件时,先在当前目录搜索;若未找到,会按照 <路径列表> 中的目录顺序依次查找,直到找到文件或遍历结束。
具体代码实现如下:
# makefile 依赖文件可选的路径
VPATH := $(SRCDIRS)
- 处理逻辑:将
SRCDIRS中所有源文件目录直接赋值给VPATH,Make 会自动识别这份路径清单。 - 举个例子:当需要编译
led.c时,Make 先在当前目录查找;若未找到,会按照SRCDIRS的顺序,自动去bsp/led目录搜索。找到led.c后,就能正常执行编译操作,后续编写编译规则时,直接用纯文件名(如led.c)即可。
通过配置 VPATH,Make 工具彻底明确了源文件的查找范围,结合之前的 INCLUDE 配置,既解决了源文件分散导致的"找不到文件"问题,也解决了头文件引用导致的"编译报错"问题,为后续的编译、链接流程扫清了障碍。
4. 实际编译汇编部分:烧大菜
接着到了我们实际的汇编部分,其中绝大部分只是在原来简单的makefile文件(指名道姓的预处理、编译、汇编、链接)上将对应的名称更换为了前文定义的变量和通配符。
为一需要注意的是静态模式
静态模式的核心语法为 :
:
中,筛选出符合
最终此处代码如下:
$(TARGET).bin: $(OBJS)$(LD) -Timx6u.lds -o $(TARGET).elf $^$(OBJCOPY) -O binary -S $(TARGET).elf $@$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis$(SOBJS):obj/%.o:%.s$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<$(COBJS):obj/%.o:%.c$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<
5. 辅助操作:清理与调试检测
为了方便项目管理和 Makefile 调试,我们还定义了 clean 和 print 两个伪目标,分别用于清理编译产物和检测变量配置是否正确。
.PHONY:clean
clean:rm -rf $(TARGET).bin $(TARGET).elf $(TARGET).dis $(OBJS)
print:@echo INCLUDE = $(INCLUDE) # @是静默符号@echo SFILES = $(SFILES) # @是静默符号@echo CFILES = $(CFILES) # @是静默符号@echo SFILES_NO_DIR = $(SFILES_NO_DIR) # @是静默符号@echo CFILES_NO_DIR = $(CFILES_NO_DIR) # @是静默符号@echo SOBJS = $(SOBJS) # @是静默符号@echo COBJS = $(COBJS) # @是静默符号@echo OBJS = $(OBJS) # @是静默符号@echo $(TARGET).bin
-
clean伪目标:用于一键清理所有编译生成的文件,包括最终二进制文件($(TARGET).bin)、ELF 文件、反汇编文件和所有.o目标文件。执行make clean即可快速清空编译产物,保持项目目录整洁。 -
print伪目标(重点调试工具):核心作用是通过echo命令打印前文定义的所有关键变量值,方便检测 Makefile 配置是否正确。- 每行命令前的
@是静默符号,作用是执行时不显示命令本身,只输出echo后的变量内容,避免输出杂乱。 - 实际用途:当 Makefile 出现“文件找不到”“路径错误”等问题时,执行
make print可直观查看INCLUDE(头文件路径)、SFILES/CFILES(源文件列表)、OBJS(目标文件列表)等变量的实际生成结果,快速定位是否存在路径拼接错误、文件遗漏等问题,是调试 Makefile 的实用工具。
- 每行命令前的
6. 最终完整代码,含注释
CROSS_COMPILE ?= arm-linux-gnueabihf-
TARGET ?= keycCC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump# 头文件路径
INCDIRS := imx6u \bsp/clk \bsp/delay \bsp/led \bsp/beep \bsp/key \bsp/gpio \project# 源码文件路径
SRCDIRS := project \bsp/clk \bsp/delay \bsp/led \bsp/beep \bsp/key \bsp/gpio \# 用于编译的头文件路径
INCLUDE := $(patsubst %, -I %, $(INCDIRS)) # $(patsubst <pattern>,<replacement>,<text>) 将 <text> 中符合 <patern> 的部分,替换为 <replacement># 包含路径的各个源码文件
SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.s)) # $(foreach <var>,<list>,<text>) 把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c)) # $(wildcard <pattern>) 用于匹配指定<pattern>的文件路径,返回所有符合模式的文件名列表(以空格分隔)。[这里通配符是 "*"]# 无路径的各个源码文件
SFILES_NO_DIR := $(notdir $(SFILES))
CFILES_NO_DIR := $(notdir $(CFILES))# 源码文件对应的编译后文件 .o
SOBJS := $(patsubst %, obj/%, $(SFILES_NO_DIR:.s=.o)) # 此外还将所用的 .s 后缀更换为 .o 后缀
COBJS := $(patsubst %, obj/%, $(CFILES_NO_DIR:.c=.o))
OBJS := $(SOBJS) $(COBJS)# makefile 依赖文件可选的路径
VPATH := $(SRCDIRS) # ,make 使用“VPATH”变量来指定“依赖文件”的搜索路径。# 编译汇编链接部分
$(TARGET).bin: $(OBJS)$(LD) -Timx6u.lds -o $(TARGET).elf $^$(OBJCOPY) -O binary -S $(TARGET).elf $@ # objcopy [选项] 源文件 目标文件$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis$(SOBJS):obj/%.o:%.s # 静态模式<list>:<pattern>:<prereq-pattern>,[<list>:<pattern>]先筛选出<list>中符合<pattern>的目标文件,然后将对应的通配符"%"所占位置打包,输出到<prereq-pattern>的"%"中$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<$(COBJS):obj/%.o:%.c # 这里由于不能再通配符"%"里添加"obj/",不然对应的 .c .s 文件不存在$(CC) -Wall -c -nostdlib -O2 $(INCLUDE) -o $@ $<.PHONY:clean
clean:rm -rf $(TARGET).bin $(TARGET).elf $(TARGET).dis $(OBJS)
print:@echo INCLUDE = $(INCLUDE) # @是静默符号@echo SFILES = $(SFILES) # @是静默符号@echo CFILES = $(CFILES) # @是静默符号@echo SFILES_NO_DIR = $(SFILES_NO_DIR) # @是静默符号@echo CFILES_NO_DIR = $(CFILES_NO_DIR) # @是静默符号@echo SOBJS = $(SOBJS) # @是静默符号@echo COBJS = $(COBJS) # @是静默符号@echo OBJS = $(OBJS) # @是静默符号@echo $(TARGET).bin
参考资料
- 正点原子参考资料
- 跟我一起写makefile