首页 > 其他分享 >STM32CubeMX 生成的 Makefile 文件解析

STM32CubeMX 生成的 Makefile 文件解析

时间:2023-09-29 09:15:42浏览次数:46  
标签:文件 依赖 -- Makefile STM32CubeMX BUILD 解析 DIR

STM32CubeMX 生成的 Makefile 文件解析

Makefile 的前置知识

一个 makefile 是由一系列的规则 (rule) 组成的。一条完整的规则包括目标 (target) 、依赖 (prerequistites) 、方法 (recipe) :

target ... : prerequistites ...
    recipe
    ...
    ...

依赖和方法不一定需要同时存在,只要保证至少有一个就行。当要生成目标时, make 会递归地寻找依赖关系,逐步生成目标。如果找到最底层依然无法满足生成条件就会报错。默认情况下, make 会且只会执行第一条规则。如果要执行指定的规则需要显式说明,如 make clean 调用 clean 规则清除文件。注意方法前的空白是一个制表符 TAB ,有些编辑器会自作主张把制表符替换成空格,从而导致 make 执行失败。

解析 Makefile 文件

这次解析的是由 STM32CubeMX 生成的 STM32F030C8 的 Makefile 文件,只使能 SWCLK 和 SWDIO 引脚,其他配置保持原始状态。

我们首先搜索冒号,找到第一条规则

all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin

第一个依赖是 $(BUILD_DIR)/$(TARGET).elf ,把变量替换成实际值,即 build/makefile.elf 。一开始这个 elf 文件是不存在的,所以我们找到生成 elf 的规则。

$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
	$(CC) $(OBJECTS) $(LDFLAGS) -o $@
	$(SZ) $@

发现其第一个依赖是 $(OBJECTS) 变量。找到变量 $(OBJECTS) 的赋值如下

# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))

其中 $(C_SOURCES:.c=.o)C_SOURCES 里的所有 .c 文件后缀改成 .o 。 $(notdir names...) 函数删除 name 路径中的目录名,只保留文件名。 $(addprefix prefix,names … ) 函数为 name 名添加前缀。再加下面一行对 .s 文件的处理,此时 OBJECTS 的值为:

OBJECTS = 
build/main.o \
build/stm32f0xx_it.o \
build/stm32f0xx_ll_gpio.o \
build/stm32f0xx_ll_pwr.o \
build/stm32f0xx_ll_exti.o \
build/stm32f0xx_ll_rcc.o \
build/stm32f0xx_ll_utils.o \
build/system_stm32f0xx.o \
build/startup_stm32f030x8.o

再看 vpath %.c $(sort $(dir $(C_SOURCES)))$(dir names … ) 函数提取 name 中的目录部分; $(sort list) 函数对 list 中的元素排序,并删除重复的元素,我们主要用到它的去重的功能; vpath pattern directories 语句为命名符合 pattern 的文件指定搜索路径 directories 。 vapth 使用方法中的 pattern 需要包含“ % ”字符。“ % ”的意思是匹配零或若干字符,例如,“ %.c ”表示所有以“ .c ”结尾的文件。所以本段第一句表示,所有 .c 文件都在下面的目录中搜索。(如果某文件在当前目录没有找到的话)

Core/Src/
Drivers/STM32F0xx_HAL_Driver/Src/

汇编过程

回到主线,变量 $(OBJECTS) 代表的一系列 .o 文件并不存在,所以跳到下面的规则意图创建 .o 文件。

$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR)
	$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@

让我们先看依赖列表中竖线 | 的作用。 | 用于区分普通依赖和 order-only 依赖(翻译成顺序依赖?)。 | 左边的是普通依赖。普通依赖有两个作用,其一是作为方法的前置条件,要求规则在执行方法前先执行依赖或保证依赖存在;其二是当依赖更新时,指示目标过时需要重新构建。 | 右边的是 order-only 依赖,只要求依赖先于目标构建,不要求当依赖有更新时强制更新目标。即 order-only 依赖只具备普通依赖的第一个作用,不具备第二个作用。第一个依赖是对应的 .c 文件,它总是存在的,满足条件。第二个依赖是 Makefile ,它总是存在的,满足条件。这里的 Makefile 利用普通依赖的第二个作用,当 Makefile 文件有更新时会重新编译 .o 文件。第三个依赖 $(BUILD_DIR)build 不存在。所以跳转到规则

$(BUILD_DIR):
	mkdir $@

这条规则里的方法中自动化变量 $@ 代指规则的目标名,所以这条规则的作用是创建 build 文件夹。

回到创建 .o 文件的规则,这时所有依赖全部满足,开始执行方法。利用 gcc 编译 .o 文件,我们逐个解析 gcc 的参数。

$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@
|     |  |          |                                                   |     |-- 代指规则名,即 .o 文件
|     |  |          |                                                   |-- 代指第一个依赖,即 .c 文件
|     |  |          |-- 对汇编器传递指令,紧接在 -Wa 后面的选项(逗号分割)就是专门传递给汇编器的指令选项。
|     |  |              这里表示生成 lst 文件。可以通过 as --help 查看具体信息
|     |  |-- CFLAGS 的内容比较丰富,我们一个一个过
|     |      |-- MCU MCU 相关的配置,可以查看 https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html
|     |      |   |-- -mcpu=cortex-m0 ,指示 MCU 是 cortex-m0 内核
|     |      |   |-- -mthumb ,指示生成 Thumb 指令的目标文件。如果要和 ARM 指令交叉调用,可以加 -mthumb-interwork
|     |      |   |-- STM32F030 没有浮点运算单元,所以接下来的两个变量为空
|     |      |-- C_DEFS 定义宏并传递给编译器,宏名前都需要加 -D 前缀
|     |      |-- C_INCLUDES 指定头文件路径,宏名前都需要加 -I 前缀
|     |      |-- OPT, 优化等级( -O0 -O1 -O2 -O3 -Os -Ofast -Og -Oz )
|     |      |-- -Wall, 开启大部分警告提示
|     |      |-- -fdata-sections ,数据项单独作为成段,链接时配合 -gc-sections 去掉不用的段,达到减小程序体积的效果
|     |      |-- -ffunction-sections ,函数单独作为成段,链接时配合 -gc-sections 去掉不用的段,达到减小程序体积的效果
|     |      |-- -g ,生成调试信息
|     |      |-- -gdwarf-2 ,生成 DWARF 格式的调试信息(如果支持的话)
|     |      |-- -MMD -MP -MF"$(@:%.o=%.d)" ,将不包括标准头文件的依赖关系写入 .d 文件
|     |-- 编译或汇编源文件,但没有链接
|-- arm-none-eabi-gcc

这时就编译汇编完所有 .c 文件。接下来汇编 .s 文件。

$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
	$(AS) -c $(CFLAGS) $< -o $@
    |-- arm-none-eabi-gcc -x assembler-with-cpp

汇编 .s 文件的命令比较有意思。语句展开来是 arm-none-eabi-gcc -x assembler-with-cpp 。参数 -x assembler-with-cpp 指示到下一个 -x 选项之前的所有文件都当成 assembler-with-cpp ,使得 gcc 能够对 .s 文件做预处理。 gcc 编译一般分为四个阶段,分别是预处理、编译、汇编、链接。预处理的作用是宏展开和头文件替换,即将 .c 文件转成 .i 文件。编译的作用是把 c 代码转成汇编代码,即将 .i 文件转成 .s 文件。汇编的作用是将汇编代码转成对应的二进制形式的 cpu 指令,即将 .s 转成 .o 文件。链接的作用是把代码之间的引用关系关联起来,最终生成一个完整的程序 , 如 .elf 文件。编译过程在预处理过程之后,编译的产物 .s 文件自然不支持预处理。所以如果 .s 文件需要预处理的话,我们要显式指定上面的参数。把 .s 后缀改成大写的 .S 后缀可以让 gcc 自动使用 -x assembler-with-cpp 处理文件,这也是网上的很多文章会建议把 .s 后缀改成 .S 后缀的原因。这方面的知识可以参考 How to preprocess and compile an assembly file(.s) using gcc?

链接过程

经过汇编之后,所有的 .o 文件已经生成。回到生成 elf 文件的规则。

$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
	$(CC) $(OBJECTS) $(LDFLAGS) -o $@
	$(SZ) $@

变量 LDFLAGS 的选项包括

LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
          |      |                  |             |         |       |                                          |-- 删除没用到的段
          |      |                  |             |         |       |-- 生成 map 文件
          |      |                  |             |         |-- -lc ,链接标准 C 库 libc.a
          |      |                  |             |         |-- -lm ,链接标准数学库 libm.a
          |      |                  |             |         |-- -lnosys ,链接 libnosys.a
          |      |                  |             |-- 我们没有使用非标准库,保持为空
          |      |                  |-- 指定连接器脚本文件,文件名加 -T 前缀
          |      |-- 指定 nano.specs 配置,使用 newlib nano 库
          |-- 前文介绍过,这里不再展开说明

上面指定的库和 spec 文件都放在 lib/gcc/arm-none-eabi/12.3.1/thumb/v6-m/nofp 文件夹中,详细的链接参数参考 Link Options , spec 文件的语法参考 Spec Files 。 nano.specs 文件总的来说就是把所有用到的标准库都替换成 nano 版本,即把 newlib 改成 newlib-nano 。 所谓的 newlib-nano 是在 newlib 的基础上做了裁剪,比如取消对宽字符的支持, printf 不支持浮点数等。 nano.specs 里面还涉及 libgloss 库。

libgloss 是提供启动代码和底层 I/O 支持的。

查看 newlib 源码,我们可以知道 libgloss 提供跟操作系统相关的函数。比如 write.c 文件定义了 write 函数,当程序中引用了 printf 等标准输出函数时,最终 printf 会调用 write 函数进行输出,如果没有定义 write 函数,那么链接就失败了。 kill.c 源文件定义了 kill 系统函数, getpid.c 源文件定义了 getpid 系统函数,等等。

libgloss 目录下除了和处理器相关的子目录外,还有个很特别的子目录,那就是 libnosys 目录。这个目录下的源文件重新定义了 libgloss 的所有函数,但是所有函数都是空的,都是 stub 函数,完全是为了链接通过而定义的。如果程序并不实际使用系统函数,但是某些代码引用了系统函数,那么可以引入 libnosys ,以便通过编译。 libnosys 库是一个单一的库文件 libnosys.a, 编译时直接指定 -lnosys 即可。

libgloss 除了一个 librdimon.a 库文件外,还包含了若干启动代码目标文件( crt*.o ),编译时如果指定了 -lrdimon 还提示有符号未定义的话,需要把启动代码目标文件也链接进去。

后续工作

这部分比较简单。生成 elf 文件后用二进制工具 arm-none-eabi-size.exe 显示文件大小,用 arm-none-eabi-objcopy.exe 生成 hex 文件和 bin 文件。 clean 规则删掉整个 build 文件夹。最后的 -include $(wildcard $(BUILD_DIR)/*.d) 导入所有 .d 文件,用于加快编译速度。原理参考 makefile 中 include 的作用

优化 Makefile

构建 elf 文件的规则中指定了 -lnosys ,导致每次构建都会提示 _close 之类底层函数没实现的警告,属实有点烦。建议取消 -lnosys 参数,如果觉得标准 IO 函数不适用,自己另外实现类似的函数就行。

还有 GCC Arm12.2 之后的编译器会出现 elf has a LOAD segment with RWX permissions 的警告。建议链接时加上 -Wl,--no-warn-rwx-segments 屏蔽该警告。原理参考以下链接:

GCC Arm 12.2 编译提示 LOAD segment with RWX permissions 警告

The linker ’ s warnings about executable stacks and segments

GNU Linker: ELF has a LOAD segment with RWX permissions. Embedded ARM project

Windows 系统并不支持 rm 命令,直接执行 clean 规则会报错。我们首先判断系统类型,如果是 Windows 系统就利用 rmdir 命令删除文件夹。

ifeq ($(OS),Windows_NT)
    RM = -rmdir /s /q
else
    RM = -rm -rf
endif

clean:
	$(RM) $(BUILD_DIR)

在正式编译前输出 gcc 版本信息。

all: version $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin

version:
	$(CC) --version

参考文档:

GNU make 官方文档

GCC Option Summary

Which Embedded GCC Standard Library? newlib, newlib-nano, …

标签:文件,依赖,--,Makefile,STM32CubeMX,BUILD,解析,DIR
From: https://www.cnblogs.com/suanite/p/17736747.html

相关文章

  • OWASP Top 10漏洞解析(2)- A2:Cryptographic Failures 加密机制失效
    作者:gentle_zhou原文链接:<https://bbs.huaweicloud.com/blogs/405125>Web应用程序安全一直是一个重要的话题,它不但关系到网络用户的隐私,财产,而且关系着用户对程序的新人。随着Web应用程序功能持续增加,复杂性不断提高,这些程序也面临着越来越多的安全威胁和挑战。为了帮助这些应用程......
  • 音视频基础知识|ANS 噪声抑制原理解析
    在上一期课程《音视频开发进阶课程|第二讲:回声消除》中,我们接触了音频前处理的概念,还认识了音频前处理的三剑客之一AEC回声消除。今天,我们继续来认识三剑客中的第二位:噪声抑制 ANS (AmbientNoiseSuppression)。经常被卷入在线会议的你,想必也曾抱怨过:“太嘈杂了,什么都听不清”......
  • 音视频基础知识|ANS 噪声抑制原理解析
    ​在上一期课程《音视频开发进阶课程|第二讲:回声消除》中,我们接触了音频前处理的概念,还认识了音频前处理的三剑客之一AEC回声消除。今天,我们继续来认识三剑客中的第二位:噪声抑制 ANS (AmbientNoiseSuppression)。经常被卷入在线会议的你,想必也曾抱怨过:“太嘈杂了,什么都听......
  • npm 包从 ~/AppData 解析,而未从项目的 node_modules 中解析
    问题描述js文件import*asTHREEfrom"three";ctrl+点击three跳转到的文件为C:\Users\21632\AppData\Local\Microsoft\TypeScript\5.2\node_modules\@types\three\下的文件。而不是项目中根目录下的.\node_modules\three\build\three.js。参考为什么npm包从/AppD......
  • C# 手动解析灰度PNG图片为Bitmap
    问题:当直接使用文件路径加载8位灰度PNG图片为Bitmap时,Bitmap的格式将会是Format32bppArgb,而不是Format8bppIndexed,这对一些判断会有影响,所以需要手动解析PNG的数据来构造Bitmap步骤1.判断文件格式若对PNG文件格式不是很了解,阅读本文前可以参考PNG的文件格式PNG文件格式详解......
  • NAS是用来干嘛的?一点点解析
    随着互联网技术的发展,数据呈爆炸式增长,虽然网盘、移动硬盘方便,但功能单一,而NAS的出现,逐渐成为大多数人的选择,相比网盘、移动硬盘,将数据存储在NAS里,更安全、方便,且易于共享。在本篇文档中,我们将会介绍关于NAS设备的基本信息和NAS厂商群晖的优势,让读者对NAS有个大体的认识,知道NAS是......
  • Android Activity setContentView流程解析
    ActivitysetContentView流程解析参考图解:自主生码.jpg1.当MainActivity直接继承自Activity时此时会执行Activity类的setContentView方法:publicvoidsetContentView(@LayoutResintlayoutResID){getWindow().setContentView(layoutResID);initWindowDecorActi......
  • Kafka学习01:默认分区策略解析
     Kafka学习01:默认分区策略解析 Kafka版本:2.5.1 DefaultPartitioner类/***Thedefaultpartitioningstrategy:*<ul>*<li>Ifapartitionisspecifiedintherecord,useit;如果方法声明了分区,则直接使用*<li>Ifnopartitionisspecifiedbutakeyisp......
  • 202309272022-《idea编辑器,maven解析依赖慢,解决办法》
    法一:1.Preference2.Search"maven"keyword,,3.选中“运行程序(runner)”,4.在右侧“vm选项”一栏,输入:  -DarchetypeCatalog=internal 至于为什么,我也说不出一二。 法二:https://blog.csdn.net/weixin_43912822/article/details/114173413......
  • WEBRTC回声消除-AECM算法源码解析之参数解析
    一概述 webrtc针对回声问题一共开源了3种回声消除算法,分别为aec,aecm,以及aec3,其中aec是最早期的版本,在后续的更新中aec3的出现代替了aec在webrtc中的地位,而aecm主要是针对计算能力较弱的移动端或是嵌入式设备而开发的,但同时也带来了它自己的劣势;本文主要介绍AECM算法的计......