在 “Linux基础知识(11)- GCC 简单使用(一)| GCC 安装配置和 Makefile 的基本用法” 里我们演示了 GCC 安装配置和 Makefile 的基本用法,本文将继续演示 Makefile 的高级用法。
1. 伪目标
前文我们在 Makefile 的基本用法的演示中,Makefile 编写的目标,在 make 看来其实都是目标文件,例如 make 在执行的时候由于在目录找不到 target_a 文件,所以每次 make target_a 的时候,它都会去执行target_a 的命令,期待执行后能得到名为 target_a 的同名文件。
如果目录下真的有 target_a、target_b、target_c 的文件,即假如目标文件和依赖文件都存在且是最新的,那么 make target_a 就不会被正常执行了,这会引起误会。
为了避免这种情况,Makefile 使用 “.PHONY” 前缀来区分目标代号和目标文件,并且这种目标代号被称为 “伪目标”,phony 单词翻译过来本身就是假的意思。
也就是说,只要我们不期待生成目标文件,就应该把它定义成伪目标,前文的演示代码修改如下。
示例1:
# 使用 .PHONY 表示 target_a 是个伪目标 .PHONY: target_a # target_a 是第一个目标,是最终目标,即 make 的默认目标 # 执行 ls 命令列出当前目录下的内容,target_a 依赖于 target_b 和 target_c target_a: target_b target_c ls .PHONY: target_b # 执行 touch 命令创建 test.txt 文件,target_b 无依赖 target_b: touch test.txt .PHONY: target_c # 执行 pwd 命令显示当前路径,target_c 无依赖 target_c: pwd .PHONY: target_d # 执行 rm 命令删除 test.txt 文件,target_d 无依赖 target_d: rm -f test.txt
示例2:
# 默认目标,hello_main 依赖于 hello_main.c 和 hello_func.c hello_main: hello_main.c hello_func.c gcc -o hello_main hello_main.c hello_func.c -I . # clean目标,删除编译生成的中间文件 .PHONY:clean clean: rm -f *.o hello_main
GNU 组织发布的软件工程代码的 Makefile,除了 clean 伪目标,常见的还有 all、install、print、tar 等,虽然并没有固定的要求伪目标必须用这些名字,但可以参考这些习惯来编写自己的 Makefile。
如果以上代码中不写 “.PHONY:clean” 语句,并且在目录下创建一个名为 clean 的文件,那么当执行 “make clean” 时,clean 的命令并不会被执行。
2. 默认规则
上文示例2 的 Makefile 里的 hello_main 依赖于 hello_main.c 和 hello_func.c,hello_main 目标文件本质上并不是依赖 hello_main.c 和 hello_func.c 文件,而是依赖于 hello_main.o 和 hello_func.o,把这两个文件链接起来就能得到我们最终想要的 hello_main 目标文件。
另外,由于 make 有一条默认规则,当找不到 *.o 文件时,会查找目录下的同名 xxx.c 文件进行编译。根据这样的规则,我们可把示例2 的 Makefile 做如下修改:
# 默认目标,hello_main 依赖于 hello_main.o 和 hello_func.o hello_main: hello_main.o hello_func.o gcc -o hello_main hello_main.o hello_func.o # 根据 make 的默认规则,下面两行可以不写 #hello_main.o: hello_main.c # gcc -c hello_main.c # 根据 make 的默认规则,下面两行可以不写 #hello_func.o: hello_func.c # gcc -c hello_func.c # clean目标,删除编译生成的中间文件 .PHONY:clean clean: rm -f *.o hello_main
使用修改后的 Makefile 编译结果如下:
$ make
cc -c -o hello_main.o hello_main.c cc -c -o hello_func.o hello_func.c gcc -o hello_main hello_main.o hello_func.o
从 make 的输出可看到,它先执行了两条额外的 “cc” 编译命令,这是由 make 默认规则执行的,它们把 C 代码编译生成了同名的 .o 文件,然后 make 根据 Makefile 的命令链接这两个文件得到最终目标文件 hello_main。
3. 使用变量
使用 C 自动编译成 *.o 的默认规则有个缺陷,由于没有显式地表示 *.o 依赖于 .h 头文件,假如我们修改了头文件的内容,那么 *.o 并不会更新,这是不可接受的。并且默认规则使用固定的 “cc” 进行编译,假如我们想使用ARM-GCC 进行交叉编译,那么系统默认的 “cc” 会导致编译错误。
要解决这些问题并且让 Makefile 变得更加通用,需要引入变量和分支进行处理。
1) 基本语法
在 Makefile 中的变量,有点像 C 语言的宏定义,在引用变量的地方使用变量值进行替换。变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:
定义方式 | 描述 |
= | 延时赋值,该变量只有在调用的时候,才会被赋值 |
:= | 直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。 |
?= | 若变量的值为空,则进行赋值,通常用于设置默认值。 |
+= | 追加赋值,可以往变量后面增加新的内容。 |
变量使用的语法如下:
$(变量名)
示例:
VAR_A = FILE_A VAR_B = $(VAR_A) VAR_C := $(VAR_A) VAR_A += FILE_B VAR_D ?= FILE_D .PHONY:check check: @echo "VAR_A: "$(VAR_A) @echo "VAR_B: "$(VAR_B) @echo "VAR_C: "$(VAR_C) @echo "VAR_D: "$(VAR_D)
运行:
$ make
VAR_A: FILE_A FILE_B VAR_B: FILE_A FILE_B VAR_C: FILE_A VAR_D: FILE_D
注:执行完 make 命令后,只有 VAR_C 是FILE_A。这是因为 VAR_B 采用的延时赋值,只有当调用时,才会进行赋值。当调用 VAR_B 时,VAR_A 的值已经被修改为 FILE_A FILE_B,因此 VAR_B 的变量值也就等于 FILE_A FILE_B。
要注意,变量的值后面的不能带有空格。
2) 改造默认规则
接下来使用变量对前面 hello_main 的 Makefile 进行大改造,代码修改如下:
# 定义变量 CC = gcc CFLAGS = -I. DEPS = hello_func.h # 目标文件 hello_main: hello_main.o hello_func.o $(CC) -o hello_main hello_main.o hello_func.o # *.o 文件的生成规则 %.o: %.c $(DEPS) $(CC) -c -o $@ $< $(CFLAGS) # 伪目标 .PHONY: clean clean: rm -f *.o hello_main
代码说明:
第 1 ~ 4 行:分别定义了 CC、CFLAGS、DEPS 变量,变量的值就是等号右侧的内容,定义好的变量可通过 “$(变量名)” 的形式引用,如后面的 “$(CC)”、“$( CFLAGS)”、“$(DEPS)” 等价于定义时赋予的变量值 “gcc”、“-I.” 和 “hello_func.h”;
第 8 行:使用 $(CC) 替代了 gcc,这样编写的 Makefile 非常容易更换不同的编译器,如要进行交叉编译,只要把开头的编译器名字修改掉即可;
第 11 行:“%” 是一个通配符,功能类似 “*”,如 “%.o” 表示所有以 ”.o” 结尾的文件。所以 “%.o: %.c”在本例子中等价于 “hello_main.o: hello_main.c”、“hello_func.o: hello_func.c”,即等价于 *.o 文件依赖于 *.c 文件的默认规则。不过这行代码后面的 “$(DEPS)” 表示它除了依赖 *.c 文件,还依赖于变量 “$(DEPS)” 表示的头文件,所以当头文件修改的话,*.o 文件也会被重新编译;
第 12 行:这行代码出现了特殊的变量 “$@”、“$<”,可理解为 Makefile 文件保留的关键字,是系统保留的自动化变量,“$@” 代表了目标文件,“$<” 代表了第一个依赖文件。即 “$@” 表示 “%.o”、“$<” 表示 “%.c”,例如:
# 当 "%" 匹配的字符为 "hello_func" 的话 $(CC) -c -o $@ $< $(CFLAGS) # 等价于 gcc -c -o hello_func.o func_func.c -I .
以上示例代码说明 Makefile 可以利用变量及自动化变量,来重写 *.o 文件的默认生成规则,以及增加头文件的依赖。
3) 改造链接规则
与 *.o 文件的默认规则类似,我们也可以使用变量来修改生成最终目标文件的链接规则,代码修改如下:
# 定义变量 CC = gcc CFLAGS = -I. DEPS = hello_func.h TARGET = hello_main OBJS = hello_main.o hello_func.o # 目标文件 $(TARGET): $(OBJS) $(CC) -o $@ $^ $(CFLAGS) # *.o 文件的生成规则 %.o: %.c $(DEPS) $(CC) -c -o $@ $< $(CFLAGS) # 伪目标 .PHONY: clean clean: rm -f *.o hello_main
代码说明:
第 5 行:定义了 TARGET 变量,它的值为目标文件名 hello_main;
第 6 行:定义了 OBJS 变量,它的值为依赖的各个 *.o 文件,如 hello_main.o、hello_func.o 文件;
第 9 行:使用 TARGET 和 OBJS 变量替换原来固定的内容;
第 10 行:使用自动化变量 “$@” 表示目标文件 “$(TARGET)”,使用自动化变量 “$^” 表示所有的依赖文件即 “$(OBJS)”;
以上示例代码中的 Makefile 把编译及链接的过程都通过变量表示出来了,非常通用。使用这样的 Makefile 可以针对不同的工程直接修改变量的内容就可以使用;
4) 其它自动化变量
Makefile 中还有其它自动化变量,见下表。
符号 | 描述 |
$@ | 匹配目标文件 |
$% | 与 $@ 类似,但 $% 仅匹配库类型的目标文件 |
$< | 依赖中的第一个目标文件 |
$^ | 所有的依赖目标,如果依赖中有重复的,只保留一份 |
$+ | 所有的依赖目标,即使依赖中有重复的也原样保留 |
$? | 所有比目标要新的依赖目标 |
4. 使用分支
为方便直接切换 GCC 编译器,我们还可以使用条件分支增加切换编译器的功能。在 Makefile 中的条件分支语法如下:
ifeq (arg1, arg2) 分支1 else 分支2 endif
分支会比较括号内的参数 arg1 和 arg2 的值是否相 同,如果相同,则为真,执行 “分支1” 的内容,否则执行 “分支2” 的内容,参数 arg1 和 arg2 可以是变量或者是常量。
常用的条件语句,见下表。
语句 | 描述 |
ifeq | 判断一个变量是否等于某个值 |
ifneq | 判断一个变量是否不等于某个值 |
ifdef | 判断一个变量是否有值 |
ifndef | 判断一个变量是否空值 |
else | 表示条件表达式为假的情况 |
endif | 表示一个条件语句的结束,任何一个条件表达式都应该以 endif 结束 |
使用分支切换 GCC 编译器的 Makefile,代码修改如下:
# 定义变量 CC = gcc CFLAGS = -I. DEPS = hello_func.h TARGET = hello_main OBJS = hello_main.o hello_func.o # ARCH 默认为 x86,使用 gcc 编译器,否则使用 arm 编译器 ARCH ?= x86 # 根据 ARCH 变量来选择编译器 ifeq ($(ARCH),x86) CC = gcc else CC = arm-linux-gnueabihf-gcc endif # 目标文件 $(TARGET): $(OBJS) $(CC) -o $@ $^ $(CFLAGS) # *.o 文件的生成规则 %.o: %.c $(DEPS) $(CC) -c -o $@ $< $(CFLAGS) # 伪目标 .PHONY: clean clean: rm -f *.o hello_main
Makefile 主要是增加了 ARCH 变量用于选择目标平台,第 9 行代码中使用 “?=” 给 ARCH 赋予默认值 x86,然后在代码 12 ~ 16 行增加了根据 ARCH 变量值的内容对 CC 变量赋予不同的编译器名。
在执行 make 命令的时候,通过给 ARCH 赋予不同的变量值切换不同的编译器平台:
# 清除编译输出,确保不受之前的编译输出影响 $ make clean # 使用 ARM 平台 $ make ARCH=arm # 清除编译输出 $ make clean # 默认是 x86 平台 $ make
5. 使用函数
在更复杂的工程中,头文件、源文件可能会放在二级目录,编译生成的 *.o 或可执行文件也放到专门的编译输出目录方便整理。上文的示例做一些调整,把 *.h 头文件放在 includes 目录下,*.c 文件放在 sources 目录下,不同平台的编译输出分别存放在 build_x86 和 build_arm 中,目录结构如下:
| |- includes | |- hello_func.h | |- sources | |- hello_func.c | |- hello_main.c | |- Makefile
实现这些复杂的操作通常需要使用 Makefile 的函数,修改 Makefile 代码如下:
# 定义变量 # ARCH 默认为 x86,使用 gcc 编译器,否则使用 arm 编译器 ARCH ?= x86 TARGET = hello_main # 存放中间文件的路径 BUILD_DIR = build_$(ARCH) # 存放源文件的文件夹 SRC_DIR = sources # 存放头文件的文件夹 INC_DIR = includes . # 源文件 SRCS = $(wildcard $(SRC_DIR)/*.c) # 目标文件(*.o) OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS))) # 头文件 DEPS = $(wildcard $(INC_DIR)/*.h) # 指定头文件的路径 CFLAGS = $(patsubst %, -I%, $(INC_DIR)) # 根据输入的 ARCH 变量来选择编译器 ifeq ($(ARCH), x86) CC = gcc else CC = arm-linux-gnueabihf-gcc endif # 目标文件 $(BUILD_DIR)/$(TARGET): $(OBJS) $(CC) -o $@ $^ $(CFLAGS) # *.o 文件的生成规则 $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(DEPS) # 创建一个编译目录,用于存放过程文件 # 命令前带 “@”,表示不在终端上输出 @mkdir -p $(BUILD_DIR) $(CC) -c -o $@ $< $(CFLAGS) # 伪目标 .PHONY: clean cleanall # 按架构删除 clean: rm -rf $(BUILD_DIR) # 全部删除 cleanall: rm -rf build_x86 build_arm
代码说明:
第 7 ~ 11 行:定义了变量 BULID_DIR、SRC_DIR、INC_DIR 分别赋值为工程的编译输出路径 build_$(ARCH)、源文件路径 sources 以及头文件路径 includes 和当前目录 “.”。其中编译输出路径包含了架构 $(ARCH) 的内容,ARCH=x86 时编译输出路径为 build _x86,ARCH=arm 时编译输出路径为 build_arm,方便区分不同的编译输出;
第 14 行:定义了变量 SRCS 用于存储所有需要编译的源文件,它的值为 wildcard 函数的输出,该函数的输出为 “sources/hello_func.c sources/hello_main.c sources/test.c”;
第 16 行:定义了 OBJS 变量用于存储所有要生成的的 *.o 文件,它的值为 patsubst 函数的输出,该函数是把所有 *.c 文件名替换为同名的 *.o 文件,并添加 build 目录,即函数的输出为 “build/hello_func.o build/hello_main.o build/test.o”;
第 18 行:与 SRCS 变量类似,定义一个 DEPS 变量存储所有依赖的头文件,它的值为 wildcard 函数的输出,该函数的输出为 “includes/hello_func.h”;
第 21 行:定义了 CFLAGS 变量,用于存储包含的头文件路径,它的值为 patsubst 函数的输出,该函数是把 includes 目录添加到 “-I” 后面,函数的输出为 “-Iincludes”;
第 31 行:相对于之前的 Makefile,我们在 $(TARGET) 前增加了 $(BUILD_DIR) 路径,使得最终的可执行程序放在 build 目录下;
第 35 行:与上面类似,给 *.o 目标文件添加 $(BUILD_DIR) 路径;
第 38 行:在执行编译前先创建 build 目录,以存放后面的 *.o 文件,命令前的 “@” 表示执行该命令时不在终端上输出;
第 45 行:rm 删除命令也被修改成直接删除编译目录 $(BUILD_DIR);
第 48 ~ 49 行:增加了删除所有架构编译目录的伪目标 cleanall;
本示例中用到的函数,见下表:
函数名称 | 描述 |
notdir | 判断文件路径不是目录,格式: $(notdir 文件路径) |
wildcard | 用于获取文件列表,并使用空格分隔开。格式:$(wildcard 匹配规则) |
patsubst | 模式字符串替换,格式:$(patsubst 匹配规则, 替换规则, 输入的字符串) |
调用函数的方法跟变量的使用类似,以 “$()” 或 “${}” 符号包含函数名和参数,语法如下:
$(函数名 参数)
或
${函数名 参数}
运行:
# 默认是 x86 平台 $ make # 使用 ARM 平台 $ make ARCH=arm
查看目录结构:
|- build_arm | |- hello_func.o | |- hello_main.o | |- hello_main | |- build_x86 | |- hello_func.o | |- hello_main.o | |- hello_main | |- includes | |- hello_func.h | |- sources | |- hello_func.c | |- hello_main.c | |- Makefile
注: 可以使用 tree 命令查看目录结构,若提示找不到命令,运行 sudo apt install tree 命令 (CentOS: sudo yum install tree)安装 tree。
如果没有安装 arm-linux-gnueabihf-gcc 交叉编译工具链,运行 make ARCH=arm 后会出现如下错误:
arm-linux-gnueabihf-gcc -c -o build_arm/hello_func.o sources/hello_func.c -Iincludes -I. make: arm-linux-gnueabihf-gcc: Command not found make: *** [build_arm/hello_func.o] Error 127
安装 arm-linux-gnueabihf-gcc 交叉编译工具链,本文在 Ubuntu 20.04 下安装:
$ sudo apt install gcc-arm-linux-gnueabihf
# 查看版本
$ arm-linux-gnueabihf-gcc -v
Using built-in specs. COLLECT_GCC=arm-linux-gnueabihf-gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc-cross/arm-linux-gnueabihf/9/lto-wrapper Target: arm-linux-gnueabihf Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.1' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libitm --disable-libquadmath --disable-libquadmath-support --enable-plugin --enable-default-pie --with-system-zlib --without-target-system-zlib --enable-libpth-m2 --enable-multiarch --enable-multilib --disable-sjlj-exceptions --with-arch=armv7-a --with-fpu=vfpv3-d16 --with-float=hard --with-mode=thumb --disable-werror --enable-multilib --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=arm-linux-gnueabihf --program-prefix=arm-linux-gnueabihf- --includedir=/usr/arm-linux-gnueabihf/include Thread model: posix gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
# 使用 ARM 平台
$ make ARCH=arm
arm-linux-gnueabihf-gcc -c -o build_arm/hello_func.o sources/hello_func.c -Iincludes -I. arm-linux-gnueabihf-gcc -c -o build_arm/hello_main.o sources/hello_main.c -Iincludes -I. arm-linux-gnueabihf-gcc -o build_arm/hello_main build_arm/hello_func.o build_arm/hello_main.o -Iincludes -I.