写点什么

5 分钟带你掌握 Makefile 分析

发布于: 2020 年 09 月 14 日

摘要:Makefile 是一个名为 GNU-Make 软件所需要的脚本文件,该脚本文件可以指导 Make 软件控制 arm-gcc 等工具链去编译工程文件最终得到可执行文件,几乎所有的 Linux 发行版都内置了 GNU-Make 软件,VScode 等多种 IED 也内置了 Make 程序。


什么是 Makefile


Makefile 是一个名为 GNU-Make 软件所需要的脚本文件,该脚本文件可以指导 Make 软件控制 arm-gcc 等工具链去编译工程文件最终得到可执行文件,几乎所有的 Linux 发行版都内置了 GNU-Make 软件,VScode 等多种 IED 也内置了 Make 程序。


你见到的 xxx.mk 文件或者 Makefile 都统称为 Makefile 脚本文件。


Makefile 脚本文件的语法学习可以参考:


https://www.gnu.org/software/make/manual/make.html (GNU make 官方文档)


https://seisman.github.io/how-to-write-makefile/overview.html (跟我一起写 Makefile 陈皓)


Makefile 的规则


Makefile 的规则如下,这里的[TAB]指键盘上的 TAB 按键,不是空格,如果在命令前输入了空格则会造成错误,并且在 Makefile 中 TAB 键不能随意使用:


目标 : 依赖


[TAB]命令


例如:


Hello :


@echo “Hello”


这时执行 make 命令就会输出一条语句”Hello”,Hello 是目标,依赖为空,为了生成目标,需要执行 echo “Hello”语句,从而导致输出 Hello。


例如:假设我们有一个 Hello.c C 语言源文件,需要将其编译不链接为 Hello.o 文件,最后在进行连接,Makefile 内容如下:


Hello.out : Hello.o


gcc -o Hello.out Hello.o


Hello.o : Hello.c


gcc -c -o Hello.o Hello.c


这时执行 make 命令,make 解释器发现目标为“Hello.out”,但是生成 Hello.out 需要 Hello.o,发现目录下找不到“Hello.o”,就向下查找是否有生成 Hello.o 的规则,找到了,发现”Hello.o”依赖于”Hello.c”,在目录下也找到了 Hello.c,就执行语句“gcc -c -o Hello.o Hello.c”生成”Hello.o”,只要编译过程不出错,即可得到”Hello.o”,这时可以执行“gcc -o Hello.out Hello.o“生成”Hello.out”


从哪里开始分析?


这里可以用分析一个 C 语言或 Java 语言程序来类比,一般都是根据程序是执行流来进行分析,也就是先找到 main 函数,因为 main 函数是程序的执行入口,Makefile 也有执行入口,在执行 make 命令时,make 解释器默认搜索当前目录下名为“Makefile”的文件,找到后,执行生成第一个目标的命令及生成其依赖所需的命令。


这里选择在 SDK/Targets 目录中 STM32L431_BearPi 工程中的 GCC 目录下的 Makefile 开始分析。


第 1 行到第 140 行都是设置一些变量和导入一些 makefile 文件(其中也没有任何规则,都是进行一些变量的设置),第 143 行是第一条规则



当我们执行 make 或 make all 时,就开始生成 all 目标,其依赖于 BUILD_DIR(GCC/build)目录中的 TARGET(Huawei_LiteOS).elf 文件,BUILD_DIR 和 TARGET 为两个变量,一开始就被赋值,如下图所示,实际使用时 $(变量)会被替换为变量的值,例如 $(TARGET).elf 最终会被替换为 Huawei_LiteOS.elf。




可是 Huawei_LiteOS.elf 还不存在,make 只好继续向下查找是否有生成 Huawei_LiteOS.elf 的规则,好在第 147 行的目标为 Huawei_LiteOS.elf,这就是生成 Huawei_LiteOS.elf 的规则,该规则依赖为 $(OBJ_DIRS) $(C_OBJ) $(S_OBJ)分别对应三个目录,这三个目录都不存在,所以 make 只好继续向下查找,它发现第 152 行正好为目标是该目录的规则,就创建了该目录,解决了 $(OBJ_DIRS)这个依赖,接着该处理 $(C_OBJ)这个依赖


Make 向下查找依赖发现位于第 156 行出现生成这个以来的规则,这里的 $(C_OBJ):$(BUILD_DIR)/%.o:%.c 对应 makefile 中的静态模式,我这里简单的说一下,大家如果想深入了解可以自行百度。


静态模型的格式如下:


目标列表: 与目标列表相匹配的模型: 与依赖相匹配的模型


[TAB]命令


来看一个例子,



目标列表中有 foo.o、bar.o 和 test.s 三个值,首先将匹配 %.o 的模型找出来,test.s 不匹配就被遗弃了,将匹配的 foo.o 和 bar.o 替换为 foo.c 和 bar.c,最终这一条规则等于执行了下列这两条规则,为什么要这样做呢?你可以试想以下,假设目标列表中有几千个文件,这样做的话是不是就可以少写很多规则:



回到 LiteOS_Lab 的 Makefile 上来,156 行将 C_OBJ 变量中的符合 build/xxx.o 格式的文件作为 xxx.c 格式的依赖,C_OBJ 变量的赋值如下图所示:



$(patsubst PATTERN, REPLACEMENT, TEXT)函数的作用是模式替换,将 TEXT 中以空格隔开的每个单词(文件名),符合 PATTERN 格式的替换为 REPLACEMENT 格式,例如第 124 行,将所有的 C_SOURCE 变量中的文件名,凡是只要在 SDK/ iot_link 目录下的都替换为.o 后缀,SDK/ iotlink 目录中有一个符合该模型的文件,link_main.c,在执行该规则对应的命令时,目标文件为 link_main.o,第 125 和 126 行同上。



这里以 link_main.c 为例向大家讲解指令,经过模式替换后规则如下:


link_main.o: link_main.c


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


这里的 CC 是指 arm-none-eabi-gcc,CFLAGS 是指各类编译参数,例如-MMD -MP -Wno-missing-braces,$(@:%.o=%.lst)函数的作用是将 $@目标符合 %.o 模型的值替换为 %. lst,这里就将 link_main.o 替换 link_main. lst,$<是指第一个依赖,$@是指目标,组合的命令后如下:


arm-none-eabi-gcc -c -MMD -MP -Wno-missing-braces -Wa,-a,-ad,-alms=link_main. lst link_main.c -o link_main.o。一句话来说就是将所有的.c 源文件编译不链接生成.o 文件和.lst 文件,等待后续进一步操作。


第 160 和 161 行的操作类似于上面的操作,将所有的汇编文件都编译不链接生成.o 文件等待后续进一步操作。


第 147 到 149 行规则所需的依赖全部都生成好了,可以开始执行该规则的命令,将所有依赖通过 LDFLAGS 变量中的值链接生成 Huawei_LiteOS.elf 文件并列出程序文件中各段的大小。


LDFLAGS 变量中通过 MCU 变量定义了内核相关参数,例如 ARM 架构的版本以及是否支持硬件浮点数运算等参数,如下图所示,如果你需要将工程移植到不是 STM32L431 系类的 MCU 上,就需要修改 MCU 变量的值。




LDFLAGS 变量中通过 LDSCRIPT 变量读取 os.ld 链接脚本来控制程序该如何链接,每个段应该存放在程序中的何处,在 os.ld 链接脚本中还指明了 MCU 的 RAM 和 FLASH 大小及起始位置,我们在进行移植时也需要修改。


Huawei_LiteOS.elf 文件是第 143 至 145 行规则的依赖,将该 elf 文件转换为 Huawei_LiteOS.hex 和 Huawei_LiteOS.bin 文件,即可烧录。


现在大家应该明白 make 控制下的整个程序编译过程了吧,以及 Makefile 文件起到的作用,我们再来看看前面的 include 导入的文件,如下图所示:



首先导入了.config 文件,这由 Kconfig 软件读取用户通过图形化配置的各项参数信息生成的,其中包含了对 SDK 中各组件参数的配置信息,如下图所示:




以 AT 组件为例,CONFIG_AT_ENABLE=y 代表使能 AT 组件;CONFIG_AT_DEVNAME="atdev"将 AT 组件用到的串口以"atdev"注册到 driver 层中;CONFIG_AT_OOBTABLEN=6 OOB 表的长度配置为 6 个,这时用于接收异步数据的结构体,意味着我们最多能配置 6 个特定字符串,当这时字符串出现时调用相应处理函数进行处理;CONFIG_AT_RECVMAXLEN=1024 将 AT 框架中的接收缓存区大小配置为 1024 字节,如果你的 MCU 资源受限可以减少这里的大小;CONFIG_AT_TASKPRIOR=10 将 AT 任务的优先级配置为 10,注意:这里只有第 35 行这条语句会影响 Make 的编译,其余语句是为了记录用户做了哪些配置和生成 iot_config.h 所用。


.config 文件中所有的组件配置都和上面分析的一致,如果组件没有被使能如下图所示:



相信大家看到这里又有新的疑问了,这些配置是如何影响到程序的编译呢?回到前面的第 70 行 include $(SDK_DIR)/iot_link/iot.mk,来看看这个 SDK/iot_link 目录下的 iot.mk 文件中有什么你就有答案了,如下图所示:



该 Makefile 将每个组件所属文件夹下的 Makefile 也导入进来了,我们还是以 AT 框架为例,第 31 行,导入 at 目录下的 at.mk 文件,该 Makefile 内容如下图所示:



看到了吧,第 7 行与前面的 CONFIG_AT_ENABLE=y 变量相对应,ifeq ($(CONFIG_AT_ENABLE),y)语句的意思是如果 CONFIG_AT_ENABLE 变量的值为 y,则将 ifeq 到 endif 之间的语句全部执行。


第 8 至 9 行将 at 目录下所有.c 文件添加到 C_SOURCES 变量中,注意这里用的是+=是追加上去。


第 11 至 12 行将 at 目录下所有.h 文件所在路径(注意是路径,通过-I 参数指定头文件所在的路径,这样编译器才能找到头文件,否则会因为找不到头文件导致编译失败)添加到 C_INCLUDES 变量中,注意这里用的是+=是追加上去。


第 14 至 14 行将-D CONFIG_AT_ENABLE=1 文本追加到 C_DEFS 变量中。


这三个变量大家都很眼熟吧,这就是工程目录/GCC 目录中 Makefile 中的那三个变量,如下图所示:




这样 AT 组件中的所有源文件和头文件就参与了编译。


回到第三个 include,include $(MAKEFILE_DIR)/project.mk,这是用于包含(引入)工程目录/GCC 目录下的 project.mk,该 Makefile 部分内容如下图所示:



主要用于包含 Hal 库中的文件以及用户自己添加进去的文件,这也是移植时需要进行修改的文件之一,大家可以仿照我前面分析的方法自己分析一下。最终所有被添加进入的.c 源文件会被追加到 C_SOURCES 变量中,所有.h 头文件所在的路径会被追加到 C_INCLUDES 变量中。


总结


以上就是 LiteOS_Lab 中 Makefile 运行的机制了,大家可以自己跟着文章全部分析分析一边以加深影响,SDK 中所有的 Makefile 文件都不需要也不能进行修改,只需要修改工程中的三个 Makefile,.config(这个不用手动修改,可以通过图形化配置进行修改),Makefile(根据目标 MCU 修改 MCU 相关的参数即可,也就是 MCU 这个变量的值),project.mk(根据目标 MCU 修改、添加或删除库文件以及用户文件以及最后的 C_DEFS 变量即可)。


发布于: 2020 年 09 月 14 日阅读数: 69
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
5分钟带你掌握Makefile分析