1、 什么是MakefileMakefile是将一系列指令整合在一起的脚本文件将C/C项目中的源文件逐步生成最终的可执行文件。在目录下输入指令make会自动寻找文件名为Makefile的文件并按照指令生成可执行文件在大型项目中优势显著。回忆一下C语言编译的过程C语言将源文件.c转换为可执行文件的过程分为预处理、编译、汇编、链接四个核心阶段每一个阶段都会生成相应的输出文件。一般而言在Makefile中直接简化成两个步骤第一步是前3阶段将源文件.c转换成目标文件.o第二步是将所有的目标文件.o链接生成最终的可执行文件。2、一个简单的示例当前目录下包含的文件如下所示。hello.h文件内容如下#ifndefHELLO_H#defineHELLO_Hvoidprint_hello();#endifhello.c文件内容如下#includehello.h#includestdio.hvoidprint_hello(){printf(hello!\n);}main.c文件内容如下#includestdio.h#includehello.hintmain(){printf(This is main function\n);print_hello();return0;}Makefile文件内容如下test:main.o hello.o cc-otestmain.o hello.o main.o:main.c cc-c-omain.o main.c hello.o:hello.c cc-c-ohello.o hello.c .PHONY: clean clean:-rm-rftestmain.o hello.o整体的运行效果输入make指令后相关的源文件被编译链接生成了3个文件目标文件hello.o main.o和可执行文件test。执行文件test效果与预期相符。输入make clean指令相关的生成文件被清除。下面对Makefile文件进行一下分析和补充(1)#用于添加注释\用作行继续符。(2) Makefile的规则如下target:prerequisitescommandtarget是一个目标文件可以是.o文件、.s文件、.i文件和可执行文件也可以是一个伪目标。prerequisites是生成target文件所需的依赖文件。例如可执行文件test的生成依赖于hello.o和main.o这两个文件。target : prerequisites反映了文件的依赖关系。根据依赖关系和文件更新的时间戳就可以断定命令command是否执行。当prerequisites文件的时间戳晚于target文件的时间戳时依赖关系形成此时输入make指令后command命令才会运行。时间戳的好处是若prerequisites文件没有被重新修改其时间戳一直早于target文件依赖关系不会形成避免了重新执行command命令的时间浪费。command定义了具体的命令命令之前必须以[TAB]键开头。①cc是编译器或链接器的别名。②-o filename是指定生成文件的名称。③-c参数是让编译器只进行编译而不链接没有这个参数设置的话编译器会直接生成可执行文件而不是生成.o的目标文件。④其他的文件是命令所需的输入文件。值得注意的是Makefile中编写的命令顺序并不是编译的执行顺序。在Makefile中需要将最终的目标即可执行文件放在最前面。在终端输入make后在当前目录下寻找Makefilemakefile文件并将文件中第一个目标test作为最终的目标。紧接着寻找该目标的依赖文件main.o和hello.o并根据时间戳来判定是否执行command。若command执行需要将main.o和hello.o作为目标再次寻找他们的依赖文件直到找到最初始的源文件。从终端可以看出先执行.c文件到.o文件的生成再执行.o文件到可执行文件的生成。(3) 伪目标.PHONY: clean clean:-rm-rftestmain.o hello.o.PHONY是Makfile的特殊内置目标用于声明其后跟随的目标如clean是伪目标即该目标不对应任何实际的文件和目录。声明.PHONY: clean后在终端输入make clean后直接运行clean后面命令而不去进行文件检查。若不申明.PHONY: clean如果当前目录下恰好有一个名为clean的文件执行make clean时Make会认为目标文件clean已存在且无依赖就不去执行clean后的命令导致清理失败。常见的伪目标有clean、all、run、debug等。用.PHONY声明后这些伪目标都是执行相关动作而非生成文件还可以避免同名文件的干扰。clean:下面的命令前也要以[TAB]键开头。开头的-是Make的错误忽略符即使rm命令执行失败Make不会终止会继续执行。rm -rf就是常用的linux的删除指令-r是递归删除-f是强制删除。看到这里已经可以自己写Makefile文件了下面介绍一个复杂的案例并引入一些新的用法。3、一个复杂的示例如果像简单示例的写法一样需要搞清楚每个源文件的依赖关系这往往是不现实的。以我最近看的一个项目代码为例引入一些新的用法这样就能对任意的项目编写输入自己的Makefile文件了。3.1 引入变量引入变量可以对Makefile文件的编写进行极致的简化。变量的赋值方式如下其中CC是变量名是赋值符gcc是变量值通常为字符串。CCgcc变量在使用时需要添加$()$(CC)等价于gcc。main.o:main.c$(CC)-c-omain.o main.c介绍一下4种Makefile的赋值方式赋值符(1)被赋值的变量在使用时会递归展开即只要所依赖的变量发生变化该变量就发生变化aaa b$(a)aaaa show: echo变量a $(a)echo变量b $(b)变量b的值会因为后续变量a的改变而发生改变show在这里也是伪目标结果如下。(2):被赋值的变量立即展开不受后续依赖变量的影响。a :aa b :$(a)aaaa show: echo变量a $(a)echo变量b $(b)变量b的值只与前面的依赖变量相关结果如下。(3)?当变量未定义时进行赋值否则赋值无效。通常用来设置默认值。aaa a ?aaa b ?bbb show: echo变量a $(a)echo变量b $(b)变量a之前已被赋值a ? aaa无效。变量b未定义b ? bbb有效。(4)用于变量值的追加新追加的变量值与原先的变量值默认用一个空格分隔。a : aa a bb show: echo 变量a $(a)结果如下3.2 案例下面直接结合案例来一步步构建Makefile文件。先介绍一下项目的整体结构目录架构如下上图共有3级目录结构其中黑色字体是目录文件绿色字体是c文件玫红色字体是h文件红色字体是Makefile文件。项目的主函数在main_hqc.c文件中第2级目录下的hqc-1、hqc-3和hqc-5中放的是不同安全等级下的配置文件。值得注意的是项目的c文件和其对应的h文件不一定在同一目录下。1找出所有源文件要构建最终的可执行文件需要将所有的.o文件进行链接起来而每个.o文件又是通过.c文件编译获得。因此需要先找出项目所有的.c文件的目录地址。先介绍一个Makefile内置函数wildcard该函数按照指定模式返回匹配的文件列表匹配文件的字符串以空格分隔如果没有匹配的文件就返回空字符串。# 文件发现a$(wildcard common/*.c)b$(wildcard common/*.c common/*.h)show: echo第1行:$(a)# 打印变量aecho第2行:$(b)# 打印变量b上面的脚本功能是匹配common目录下所有的.c文件*是常用的通配符*.c表示所有.c结尾的文件并返回文件列表字符串空格分隔。结果如下项目中的所有.c文件分布在4个目录中分别是Makefile所在目录commonref和fips202。因此再引入循环函数foreach用于在4个目录中依次查找源文件。# 目录配置SRC_DIR :.common ref fips202# 文件发现SRCS$(foreach dir,$(SRC_DIR),$(wildcard$(dir)/*.c))show: echo第1行:$(SRCS)# 打印变量SRCSforeach是Makefile中用于遍历列表元素的循环函数其核心功能是对列表中的每个元素执行指定的操作最终将所有操作结果拼接成一个字符串用空格分隔。$(foreach dir,$(SRC_DIR),$(wildcard $(dir)/*.c))表示从$(SRC_DIR)依次取出元素共计4个元素. common ref fips202将元素值赋给变量dir并执行$(wildcard $(dir)/*.c)获取操作的结果。最终将所有的操作结果拼接空格分隔。通过这个循环操作将4个目录的.c文件全部提取并赋值给变量SRCS变量SRCS中是每个.c文件的路径名称结果如下。值得注意的是若变量的赋值中没有其他的依赖变量建议使用:进行赋值变量的赋值中有其他的依赖变量则需要特别考虑赋值符的选取。2.o文件和可执行文件放哪儿在第2章节的简单示例中.o文件和可执行文件直接放在了源文件目录下这样并不利于文件管理。因此需要对.o文件和可执行文件的文件名和存放路径进行规划。将所有的.o文件存放在bulid目录下将可执行文件存放在bin目录下。此外项目的HQC算法有种安全等级3种不同的参数因此bulid目录下需要3个目录hqc-1、hqc-3和hqc-5分别存放不同安全等级的.o文件同时可执行文件名也需要反应安全等级信息。所有安全等级编译完后应如下图所示。下面规定.o文件和可执行文件目录# 安全等级配置SEC_LEVEL ?hqc-1 SEC_LEVELS :hqc-1 hqc-3 hqc-5# 验证安全等级参数ifeq($(filter$(SEC_LEVEL),$(SEC_LEVELS)),)$(error Invalid security level:$(SEC_LEVEL). Valid options:$(SEC_LEVELS))endif# 目录配置SRC_DIR :.common ref fips202 BUILD_DIRbuild/$(SEC_LEVEL)BIN_DIR :bin HEAD_DIR-Icommon-Iref-Ifips202-Iref/$(SEC_LEVEL)-Icommon/$(SEC_LEVEL)# 文件发现SRCS$(foreach dir,$(SRC_DIR),$(wildcard$(dir)/*.c))OBJS$(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))DEPS$(patsubst %.o,%.d,$(OBJS))# DEPS $(OBJS:.o.d)# 依赖文件与.o文件在同一目录# 最终目标,可执行文件TARGET$(BIN_DIR)/app_$(SEC_LEVEL)show: echo第1行$(SRCS)echo第2行$(TARGET)echo第3行$(OBJS)echo第4行$(DEPS)变量SEC_LEVEL是当前待编译的安全等级变量SEC_LEVELS所有可能的安全等级。为了脚本的鲁棒性增加判断语句保证SEC_LEVEL值确实在SEC_LEVELS中。下面对几个函数的用法进行说明。$(filter)是Makefile中内置文本处理函数功能是从一个列表中筛选出匹配指定模式的元素。$(filter $(SEC_LEVEL),$(SEC_LEVELS))是从变量$(SEC_LEVELS)中将$(SEC_LEVEL)筛选出来返回筛选后的元素若没有匹配的元素返回空字符。ifeq是Makefile中常见的条件判断指令用于比较两个字符串是否完全相等相等就执行函数体中语句。脚本中将$(filter)的筛选结果和空字符比较两者相等说明筛选结果为空即变量SEC_LEVEL的值不在SEC_LEVELS中。$(error)是Makefile中的内置错误处理函数功能是输出给出的错误信息到终端并终止Make的执行流程。对上文函数进行验证修改SEC_LEVEL ? hqc-2终端结果显示如下BUILD_DIR是所有.o文件的存放目录并根据不同的安全等级划分。BIN_DIR是可执行文件存放目录。HEAD_DIR是所有.h文件所在目录-I\dir是gcc的编译选项用来指定编译时头文件的搜索目录不同安全等级不同参数下搜索的头文件路径是不同的。OBJS是每个.o文件的具体路径名称根据对应的.c文件的路径名称推导得来。这里用到了函数$(patsubst)功能是根据指定的模式对一个字符串列表中的每个元素进行匹配和替换返回替换后的新列表。OBJS $(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))是将变量SRCS中任意.c结尾的字符串在前面加上$(BUILD_DIR)/并将.c改为.o%是常用的通配符匹配任意长度的任意字符例如common/code.c被替换为build/hqc-1/common/code.o后放入变量OBJS。DEPS $(patsubst %.o,%.d,$(OBJS))是将变量OBJS中任意.o结尾的字符串替换为.d。例如build/hqc-1/common/code.o被替换为build/hqc-1/common/code.d。用法DEPS $(OBJS:.o.d)的效果也是一样的。这里的.d文件和对应.o文件路径相同后续来补充.d文件的作用。TARGET是最终的可执行文件可能值为bin/app_hqc-1bin/app_hqc-3和bin/app_hqc-5。上面脚本的整体效果如下3.d文件的作用在正式构建规则之前介绍一下为什么这里引入.d文件。在大部分情况下从.c文件编译成.o文件时一般直接写成main.o : main.c的依赖关系但这种写法是不完善的因为我们忽略了所依赖的头文件信息。这种写法带来的问题是如果在项目中只修改了头文件重新make后不会执行编译过程生成相应的.o文件前一次make生成.o文件未被删除的前提下。.d文件本质上是一个依赖关系文件dependency file它保存的是某个目标文件.o依赖了哪些源文件和头文件。当前目录下有个main_hqc.c的源文件我们来生成对应的.d文件并查看.d文件的内容。gcc -MMD -MP -Icommon -Iref -Ifips202 -Iref/hqc-1 -Icommon/hqc-1 -c main_hqc.c -o main_hqc.o-MMD生成一个与目标文件同名的依赖文件main_hqc.d文件中只记录用户头文件不记录系统头文件如stdio.h。-MD是记录所有头文件。-MP是为每个头文件生成伪目标空规则这样即便头文件被删除使用make时也不会报错崩溃。-I指定所有可能的头文件存在目录用于指令查找头文件。-c只编译不链接。-o指定输出文件名。在终端输入指令后效果如下图所示生成了两个文件main_hqc.o和main_hqc.d.d文件的内容打印结果如下。.d文件的第1行\表示行继续符表明了main_hqc.o文件的真正的依赖文件包括了main_hqc.c文件和相应的头文件。当这些文件被修改时make会触发.c文件重新编译生成.o文件。其余行是由-MP选项生成的伪目标空规则有了这些规则当删除某一个头文件时make也不会报错。在Makefile文件中通过-include main_hqc.d来使用.d文件。这样make在判断是否需要重新编译时会检查这些文件的时间戳。4完整Makefile文件# 编译器配置CC :gcc LDFLAGS :-lmCFLAGS-Wall-Wextra-MMD-MP$(HEAD_DIR)# 调试模式配置DEBUG ?0ifeq($(DEBUG),1)CFLAGS-g-O0elseCFLAGS-O2endif# 安全等级配置SEC_LEVEL ?hqc-1 SEC_LEVELS :hqc-1 hqc-3 hqc-5# 验证安全等级参数ifeq($(filter$(SEC_LEVEL),$(SEC_LEVELS)),)$(error Invalid security level:$(SEC_LEVEL). Valid options:$(SEC_LEVELS))endif# 目录配置SRC_DIR :.common ref fips202 BUILD_DIRbuild/$(SEC_LEVEL)BIN_DIR :bin HEAD_DIR-Icommon-Iref-Ifips202-Iref/$(SEC_LEVEL)-Icommon/$(SEC_LEVEL)# CFLAGS $(HEAD_DIR)# 条件编译宏# CFLAGS -DVERBOSE# 文件发现SRCS$(foreach dir,$(SRC_DIR),$(wildcard$(dir)/*.c))OBJS$(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))DEPS$(patsubst %.o,%.d,$(OBJS))# DEPS $(OBJS:.o.d) # 依赖文件与.o文件在同一目录# 最终目标,可执行文件TARGET$(BIN_DIR)/app_$(SEC_LEVEL)# 主构建规则$(TARGET):$(OBJS)mkdir-p$(dir$)$(CC)$^-o$$(LDFLAGS)# 编译单个源文件$(BUILD_DIR)/%.o:%.c mkdir-p$(D)$(CC)-c$-o$$(CFLAGS)# 包含所有依赖文件-表示忽略不存在的文件-include$(DEPS)# 伪目标.PHONY: all clean run debug all:$(TARGET)clean:-rm-rfbuild$(BIN_DIR)run: all echoRunning$(TARGET)...$(TARGET)debug: $(MAKE)DEBUG1hqc-1 hqc-3 hqc-5:$(MAKE)SEC_LEVEL$变量CC指定编译器为gcc。变量LDFLAGS是链接形成可执行文件时添加的参数-lm表示链接时添加数学库。变量CFLAGS时编译时添加的参数-Wall和-Wextra用于开启警告-MMD和-MP用于自动生成.d依赖文件$(HEAD_DIR)是对变量HEAD_DIR的使用变量中是头文件搜索路径。变量DEBUG默认为0变量CFLAGS默认追加-O2表示编译时基本优化。如果执行Makefile文件最后的伪目标debug即在终端中输入make debug相当于直接输入make DEBUG1表示将变量DEBUG设置为1后再执行编译链接生成可执行文件。变量CFLAGS追加-g表示添加调试信息-O0表示不优化。主构建规则$(TARGET) : $(OBJS)表示可执行文件依赖于所有的.o文件。表示静默执行即在终端中不显示这行指令。mkdir -p表示递归创建目录如果目录已存在也不报错。$表示当前规则的目标文件这里即$bin/app_hqc-1。$(dir ...)是Make函数表示取文件路径的目录部分$(dir bin/app_hqc-1)的结果为bin/。$^表示当前规则的所有依赖文件这里即所有的.o文件。整个主构建规则可以翻译如下bin/app_hqc-1:build/hqc-1/xxx.o build/hqc-1/ref/xxx.o... mkdir-pbin/ gcc build/hqc-1/xxx.o build/hqc-1/ref/xxx.o...-obin/app_hqc-1-lm$(BUILD_DIR)/%.o : %.c表示目标文件.o依赖于对应的.c源文件。%是通配符用来匹配任意文件名例如build/hqc-1/common/code.o : common/code.c。$表示当前规则的目标文件而$(D)表示目标文件的目录部分同用法$(dir $)。$表示当前规则的第一个依赖文件规则中只有一个依赖文件即相应的%.c文件。整个规则可以翻译如下build/hqc-1/common/code.o:common/code.c mkdir-pbuild/hqc-1/common/ gcc-ccommon/code.c-obuild/hqc-1/common/code.o-Wall-Wextra-MMD-MP-Icommon-Iref-Ifips202-Iref/hqc-1 -Icommon/hqc-1-O2build/hqc-1/ref/gf2x.o:ref/gf2x.c mkdir-pbuild/hqc-1/ref/ gcc-cref/gf2x.c-obuild/hqc-1/ref/gf2x.o-Wall-Wextra-MMD-MP-Icommon-Iref-Ifips202-Iref/hqc-1 -Icommon/hqc-1-O2......伪目标all: $(TARGET)表示默认构建目标。终端输入make all时make会去构建可执行文件$(TARGET)即构建bin/app_hqc-1。由于make总是默认执行Makefile中的第一个目标输入make和make all的效果一致。伪目标clean用于清理编译产生的文件。输入make clean时执行-rm -rf build $(BIN_DIR)删除整个build和bin目录。-rm的-表示即使删除失败例如目录不存在make 也不会报错并停止。伪目标run: all用于运行可执行文件。run依赖all输入make run会先编译链接形成可执行文件然后再运行可执行文件。echo用于打印信息$(TARGET)执行最终生成的可执行文件。伪目标debug用于调式模式。输入make debug时修改了变量DEBUG为1编译参数从-O2修改成-g -O0便于用gdb调试程序。伪目标hqc-1 hqc-3 hqc-5用于切换不同的安全等级。输入make hqc-5会修改变量SEC_LEVELhqc-5从而对应的路径BUILD_DIR build/$(SEC_LEVEL)和-Iref/$(SEC_LEVEL)等也相应发生改变。5结果测试输入make生成可执行文件并手动执行。输入make run生成可执行文件并自动执行。输入make hqc-1 hqc-3 hqc-5一次性生成3个可执行文件。