作为一个嵌入式软件工程师,能够基于一个开发板构建自己的OS是一个基本的要求。一般来说OS的代码量和文件数都不算少,所以掌握构建工具将源码组织编译也是一个嵌入式软件工程师的基本要求。现在市面上的构建工具多种多样,针对嵌入式软件使用的构建工具有以下几种,老牌工具make,现在大多数项目使用的是cmake。如题目所示,本文主要介绍make的使用方式,即makefile的编写方式。下面我将举几个例子从实践和理论两个方面介绍make的用法。
实践方面
Stm32-freeRTOS
以我自己的一个项目举例,这是一个基于stm32的freertos项目,下面是整个项目makefile文件的内容
- build/lib-dependency.mk:配置stm32启动代码和hal代码的路径
- build/filepath.mk:搜寻所有的输入文件,配置输出文件
- build/toolchain.mk:编译工具链相关的配置
- build/common.mk:依赖关系的定义
- Makefile:顶层的Makefile文件,会引入上面的4个文件,搜索所有文件的位置都是以该Makfile作为参考。
在filepath.mk中所有的输入文件都要把目录作为前缀带上。打印一下SRC,如下图
输出文件同样也要将目录作为前缀带上,下面我们主要分析一下common.mk中的内容
# Check to make sure that the required variables are set
ifndef DEVICE
$(error Please set the required DEVICE variable in your makefile.)
endif
ifndef FLASH
$(error Please set the required FLASH variable in your makefile.)
endif
# 导入第三方的源文件的路径
include $(MAKEFILE_PATH)/lib-dependency.mk
# 将所有的输入文件导入到变量SRC中,指定输出文件
include $(MAKEFILE_PATH)/filepath.mk
# 根据顶层Makefile设置的device寻找对应操作芯片的源文件
# Determine the series folder name
include $(BASE_MAKE)/series-folder-name.mk
# 将芯片对应startup.S加入到SRC中
# Include the series-specific makefile
include $(BASE_MAKE)/$(SERIES_FOLDER)/common.mk
MAPPED_DEVICE ?= $(DEVICE)
# 指定工具链
include $(MAKEFILE_PATH)/toolchain.mk
# Make all
# 最终目标是生成一个bin文件
all:$(BIN_FILE_PATH)
# 这里描述了生成bin文件的规则和“菜谱”,需要输入elf文件,然后对elf文件做objcopy的操作
$(BIN_FILE_PATH): $(ELF_FILE_PATH)
$(OBJCOPY) -O binary $^ $@
# 这里描述了生成elf文件的规则和"菜谱",需要输入c语言源文件和由汇编文件编译的对象文件,然后调用toolchain去编译
$(ELF_FILE_PATH): $(SRC) $(OBJ_FILE_PATH) | $(BIN_FOLDER)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
# 这里描述了由汇编文件编译的对象文件的规则和“菜谱”,输入是芯片的启动文件,然后调用toolchain去编译
$(OBJ_FILE_PATH): $(DEVICE_STARTUP) | $(OBJ_FOLDER)
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
# 输出文件的目录
$(BIN_FOLDER):
mkdir $(BIN_FOLDER)
# 保存启动文件编译结果的目录
$(OBJ_FOLDER):
mkdir $(OBJ_FOLDER)
# Make clean
clean:
rm -f $(ELF_FILE_PATH)
rm -f $(BIN_FILE_PATH)
rm -f $(OBJ_FILE_PATH)
# Make flash
flash:
st-flash write $(BIN_FOLDER)/$(BIN_FILE_NAME) $(FLASH)
.PHONY: all clean flash
这里我先简单介绍一下makefile中一条规则的基本格式
targets : prerequisites
recipe
可以看到,上面的makefile中是严格按照这种规则来的,这种叫显式规则。有输入,有输出,以及如何从输入获得输出的指令。
上面的规则中所有的prerequisites都有对应的recipe生成,这种属于比较基础的makefile的使用方式。
ftrace
在上面的例子中,我们在搜索输入文件时使用了shell指令。目标文件在编译时没有保存中间的对象文件,直接由源文件生成目标文件。
下面一个例子,我们会将每个源文件对应的对象文件都编译出来,makefile如下
执行结果为
gcc -Wall -c -o src/ftrace.o src/ftrace.c
gcc -Wall -c -o src/utils_base.o src/utils_base.c
LD ftrace_test
gcc -o ftrace_test src/ftrace.o src/utils_base.o -I./inc
mkdir out
mkdir out
src src/ftrace.c src/utils_base.c
obj src/ftrace.o src/utils_base.o
target ftrace_test
在这个makefile中,我们并没有为OBJS指定recipe,所以我们会使用到模式匹配和隐式规则,OBJS的recipe就是用36行和38行的规则,这里就是隐式规则。在36行中,%.o会匹配到src/ftrace.o和src/utils_base.o(OBJS),这里就会寻找src/ftrace.c和src/utils_base.c。
上面的makefile等效于下面的makefile,等效的makefile仅使用了模式匹配的机制。区别是这里单独指定了OBJS的recipe,没有重写隐式的规则
下面我们再进一步,将36行到38行的规则全部注释掉,这样也是可以make成功的。这个makefile是使用了隐式的规则作为OBJS的recipe。
这里的原因就是make使用了build-in的隐式规则。这里的隐式规则具体有哪些会在理论方面简单的提几个
理论方面
make是如何执行的
简单的说,make执行需要两步:
- make会读取所有的makefile文件,并将所有的变量和相应的值,以及所有的隐式或者显式的规则内化,构建所有目标和相应的先决条件的依赖关系图
- make使用内部化的数据来确定哪些目标需要被更新,并且运行生成它们需要的recipes
如果扩展是在第一阶段执行的话,该扩展是立即的(immediate);如果扩展时在第二阶段执行的话,该扩展是延时的(deferred)。
变量的赋值
immediate = deferred
immediate ?= deferred
immediate := immediate
immediate ::= immediate
immediate += deferred or immediate
immediate != immediate
变量都是在第一阶段展开,右值是看不同的情况在第一阶段或者第二阶段展开
条件指令
条件指令是immediate的
规则定义
immediate : immediate : deferred
deferred
目标文件和先决条件是在第一阶段展开,用于生成目标文件的recipe是在第二阶段展开
Makefile是如何被解析的
第一阶段:
- 读入所有的逻辑行,包括转义字符
- 去掉所有的注释
- 如果读入的line是以recipe开头,会将当前line加入到当前的recipe中,然后读下一行
- 将第一阶段需要展开的表达式展开
- 扫描该行是否有分隔符,例如":“或者”=",以确定该行是宏赋值还是描述规则
- 内部化结果,然后读取下一行
第二阶段:
make可以将某些prerequisites的定义放在第二阶段展开,也只能将prerequisites放在第二阶段展开
方法是使用.SECONDEXPANSION
- 显式规则的第二阶段展开:
- 静态模式规则的第二阶段展开
- 隐式规则的第二阶段展开
举个例子
immediate = deferred
immediate ?= deferred
immediate := immediate
immediate ::= immediate
immediate += deferred or immediate
immediate != immediate
define immediate
deferred
endef
define immediate =
deferred
endef
define immediate ?=
deferred
endef
define immediate :=
immediate
endef
define immediate ::=
immediate
endef
define immediate +=
deferred or immediate
endef
define immediate !=
immediate
endef
makefile的书写规则
格式:
targets : prerequisites
recipe
...
targets是文件的名字,需要用空格隔开。这条规则其实是告诉make两件事情:
- target在什么时候是属于已经过时
- 当target过时的时候,应该如何更新target,就是执行recipe
prerequisites的类型
格式:
targets : normal-prerequisites | order-only-prerequisites
- 普通的prerequisites,一但prerequisites比target要新,那么立即执行recipe更新target
- order-only的prerequisite,prerequisite的更新不会触发target的更新
在指定目录中搜索prerequisite
-
使用VPATH
VPATH = src:../headers
该指令就指定了文件的搜索顺序,先从src目录中搜索,然后在…/header目录中搜索。
举个例子,比如foo.c在src路径下,那么下面两种写法是等效的
foo.o : foo.c foo.o : src/foo.c
-
使用vpath
vpath %.h ../headers vpath %.c foo vpath % blish vpath %.c bar
这个搜索过程是如何发生的
-
当prerequisite不在makefile中指定的路径中,路径搜索就会发生
-
如果在VPATH或者vpath语句中找到了相应的文件,我们将找到的文件作为prerequisite
-
对于target所需要的所有prerequisite都执行上面两步
-
当所有的prerequisite都做了处理之后,会出现两种情况。一个target文件需要更新,或者不需要更新
自动变量
$@
:规则的目标文件的名字,如果目标文件是archive member,则’$@'指代的是archive file的名字
$%
:如果目标文件是archive member,则’$%'指代的事member file的名字;否则指代的是null
$<
:第一个prerequisite的名字。如果target的生成使用的是隐式规则,那么这个将是隐式规则中添加的第一个prerequisite的名字
$?
:规则中所有比target新的prerequisite的名字,会包含空格
$^
:规则中所有的prerequisite的名字,会包含空格
$+
:和’$^'指代的对象类似,但是对于多次出现的prerequisite只会保留一个
$|
:规则中所有的order-only prerequisite
$*
:与隐式规则匹配的词干
模式匹配
静态模式规则
targets …: target-pattern: prereq-patterns …
recipe
…
# example
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
static pattern rules是指定了多目标并且基于目标的名字为每个目标构建需要的prerequisite生成一个名字的规则。
在上面的例子中,%.o必须匹配到foo.o和bar.o,不然就会报错
下面是静态模式规则的一种常用方法
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
使用filter和正则表达式去过滤变量中我们需要的内容,针对不同类型的文件进行不同的处理。
还有一个要注意的点是自动变量$*
的使用,$*
可以用用来匹配%对应的词干,举个例子
bigoutput littleoutput : %output : text.g
generate text.g -$* > $@
在这个例子中$*
匹配到的是big或者little
隐式规则可以应用到与pattern匹配的任何目标,但是需要目标没有recipe规则才行。如果声明了多个隐式规则,那么需要根据规则的顺序生效第一个隐式规则。
让make自己推断规则
先介绍一下makefile中的一些隐式规则,详情见官方文档
- 由.c文件编译生成的.o文件的操作:
$(CC) $(CPPFLAGS) $(CFLAGS) -c