STM32 的 Makefile 文件解析 ==================================== Makefile 的前置知识 ------------------- 一个 makefile 是由一系列的规则(rule)组成的。一条完整的规则包括目标(target)、依赖(prerequistites)、方法(recipe): .. code:: none target ... : prerequistites ... recipe ... ... 依赖和方法不一定需要同时存在,只要保证至少有一个就行。当要生成目标时, make 会递归地寻找依赖关系,逐步生成目标。如果找到最底层依然无法满足生成条件就会报错。默认情况下,make 会且只会执行第一条规则。如果要执行指定的规则需要显式说明,如 make clean 调用 clean 规则清除文件。注意方法前的空白是一个制表符 TAB ,有些编辑器会自作主张把制表符替换成空格,从而导致 make 执行失败。 解析 Makefile 文件 ------------------ 本文解析的是由 STM32CubeMX 生成的 STM32F030C8 的 Makefile 文件,只使能 SWCLK 和 SWDIO 引脚,其他配置保持原始状态。 我们首先搜索冒号,找到第一条规则 .. code:: none all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin 第一个依赖是 $(BUILD_DIR)/$(TARGET).elf,把变量替换成实际值,即 build/makefile.elf。一开始这个 elf 文件是不存在的,所以我们找到生成 elf 的规则。 .. code:: none $(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile $(CC) $(OBJECTS) $(LDFLAGS) -o $@ $(SZ) $@ 发现其第一个依赖是 $(OBJECTS) 变量。找到变量 $(OBJECTS) 的赋值如下 .. code:: none # 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 的值为: .. code:: none 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 文件都在下面的目录中搜索(如果某文件在当前目录没有找到的话)。 .. code:: none Core/Src/ Drivers/STM32F0xx_HAL_Driver/Src/ 汇编过程 -------- 回到主线,变量 $(OBJECTS) 代表的一系列 .o 文件并不存在,所以跳到下面的规则意图创建 .o 文件。 .. code:: none $(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 不存在。所以跳转到规则 .. code:: none $(BUILD_DIR): mkdir $@ 这条规则里的方法中自动化变量 $@ 代指规则的目标名,所以这条规则的作用是创建 build 文件夹。 回到创建 .o 文件的规则,这时所有依赖全部满足,开始执行方法。利用 gcc 编译 .o 文件,我们逐个解析 gcc 的参数。 .. code:: none $(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 文件。 .. code:: none $(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 文件的规则。 .. code:: none $(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile $(CC) $(OBJECTS) $(LDFLAGS) -o $@ $(SZ) $@ 变量 LDFLAGS 的选项包括 .. code:: none 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 ------------- .. raw:: html GCC Arm12.2 之后的编译器会出现 ``elf has a LOAD segment with RWX permissions`` 的警告。建议链接时加上 ``-Wl,--no-warn-rwx-segments`` 屏蔽该警告。原理参考以下链接:(更新版本的GCC已经修复这个问题) `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 命令删除文件夹。当然还有第二种方法,我们可以安装 windows-build-tools,这样就可以直接调用 ``rm`` 之类的命令。第二种方法这里不展开介绍了。 .. code:: none ifeq ($(OS),Windows_NT) RM = -rmdir /s /q else RM = -rm -rf endif clean: $(RM) $(BUILD_DIR) 参考文档: `GNU make `_ 官方文档 GCC `Option Summary `_ `Which Embedded GCC Standard Library? newlib, newlib-nano, … `_