详细介绍Makefile的常用操作和最佳实践,包括编译链接、变量函数、条件判断、模式规则等核心概念的使用方法。
编译
基本操作
如何将多个对象文件(.o文件,由${OBJS}变量表示)链接成一个可执行文件(os.elf),然后将该可执行文件转换成二进制格式的镜像文件(os.bin)
os.elf: ${OBJS}
${CC} ${CFLAGS} -T os.ld -o os.elf $^
${OBJCOPY} -O binary os.elf os.bin
-T os.ld:指定链接脚本os.ld。链接脚本用于控制链接过程,如指定各个段(如代码段、数据段)的位置。
-o os.elf:指定输出文件的名称为os.elf。
$^:这是Makefile的自动变量,表示所有的依赖项,这里指${OBJS}变量中列出的所有对象文件。
${OBJCOPY}:这是GNU Binutils工具集中objcopy程序的变量,用于进行文件格式转换。
-O binary:指定输出格式为二进制。objcopy可以将ELF格式(或其他格式)的文件转换为纯二进制格式,这对于裸机程序来说是必需的,因为硬件通常直接执行二进制代码。
os.elf:输入文件,即上一步生成的ELF格式的可执行文件。
os.bin:输出文件,转换成二进制格式后的文件名。
定义编译核心数
NPROC := $(shell nproc 2>/dev/null || echo 1)
这行Makefile代码定义了一个变量NPROC,它的目的是确定可以用于并行编译的处理器(CPU核心)数量。这个变量通常用于make命令的-j选项,以加速编译过程。让我们分解这行代码的各个部分:
$(shell …):shell函数执行一个Shell命令,并将输出作为结果返回。在这个上下文中,它尝试执行nproc命令。
nproc:这是一个在多种Unix-like系统上可用的命令,用于打印系统上可用的处理单元数量(通常是CPU核心数量)。它是确定可用于并行编译作业数的常用方法。
2>/dev/null:这部分将标准错误(stderr)重定向到/dev/null,意味着如果命令失败(例如,在某些环境中nproc命令不存在),错误信息不会被显示。
|| echo 1:这是一个逻辑或操作,如果nproc命令执行失败(返回非零退出状态),则执行echo 1。这意味着如果无法确定处理器数量,就假设只有1个可用的处理单元。
:=$(shell …):使用:=进行变量赋值表示立即求值,这意味着NPROC变量将在Makefile解析时被设置为nproc命令的输出,或者在nproc不可用的情况下被设置为1。
参数
这些是GCC编译器的选项,用于控制编译过程的各个方面。以下是每个选项的简要解释:
-MMD
这个选项告诉编译器为每个源文件生成一个.d
文件,其中包含了源文件中包含的头文件列表。这对于make工具自动解析文件依赖关系非常有用,帮助确保当头文件改变时,相关的源文件会被重新编译。
-Werror
将所有的警告转化为错误。这意味着,如果编译器发现任何警告,它将停止编译过程,并将这些警告视为错误处理。这有助于维持代码质量,确保开发者不会忽略编译器的警告。
-fno-asynchronous-unwind-tables
禁用生成异步异常展开表。这会减小生成的二进制文件大小,但可能会影响异常处理和程序调试。
-fno-builtin
告诉编译器不要认为任何函数是内建的,即使编译器有内建的优化版本。这确保了对函数的调用不会被替换为编译器的内建版本,有助于避免潜在的优化导致的问题。
-fno-stack-protector
禁用栈保护。默认情况下,GCC会为防止栈溢出攻击而插入特殊的安全检查代码。使用这个选项会禁用这些检查,可能会使得程序更容易受到栈溢出攻击。
-Wno-main
禁止关于main
函数的警告。正常情况下,如果编译器发现main
函数的声明不符合标准,会发出警告。这个选项会禁止这类警告。
-U_FORTIFY_SOURCE
取消定义宏_FORTIFY_SOURCE
。当定义了_FORTIFY_SOURCE
宏时,GCC会启用一些额外的检查来防止缓冲区溢出等问题。使用-U_FORTIFY_SOURCE
可以取消这些额外的检查。
-fvisibility=hidden
设置默认的符号可见性为隐藏。这意味着,除非显式地标记为可见,否则链接时不会导出符号。这有助于减小最终二进制文件的大小,并可能改善加载时间和防止符号冲突。
这些选项通常在构建需要精细控制编译过程的项目时使用,如操作系统内核、嵌入式系统或安全敏感的应用程序。它们有助于优化生成的代码,增强安全性和性能。
函数
在Makefile中,GNU make
提供了一系列内置函数,用于处理文件名、目标对象和依赖项。这些函数可以帮助你简化规则的书写、处理字符串和文件名、以及执行列表的操作等。以下是一些处理目标对象时常用的函数:
1. $(patsubst pattern,replacement,text)
用于模式字符串替换。它搜索text
中的单词,将符合pattern
的部分替换为replacement
。
例子:将所有的.c
文件扩展名替换为.o
。
1 | OBJS := $(patsubst %.c,%.o,$(SRCS)) |
2. $(wildcard pattern)
用于匹配符合pattern
的文件名。
例子:获取当前目录下所有的.c
文件。
1 | SRCS := $(wildcard *.c) |
3. $(dir names...)
提取文件名列表names
中每个文件的目录部分。
例子:获取所有源文件的目录。
1 | SRC_DIRS := $(dir $(SRCS)) |
4. $(notdir names...)
从文件名列表names
中去除所有的目录部分,只留下文件名。
例子:从完整路径列表中提取文件名。
1 | SRC_FILES := $(notdir $(SRCS)) |
5. $(addprefix prefix,names...)
给names
列表中的每个单词添加前缀prefix
。
例子:给所有目标文件添加路径前缀。
1 | OBJS := $(addprefix obj/,$(notdir $(SRCS:.c=.o))) |
6. $(addsuffix suffix,names...)
给names
列表中的每个单词添加后缀suffix
。
例子:给所有模块名添加.o
后缀。
1 | MOD_OBJS := $(addsuffix .o,$(MODULES)) |
7. $(filter pattern...,text)
从text
中选择符合pattern
的单词。
例子:从所有文件中筛选出.c
和.h
文件。
1 | C_AND_H_FILES := $(filter %.c %.h,$(FILES)) |
8. $(filter-out pattern...,text)
从text
中去除符合pattern
的单词。
例子:从所有文件中去除.o
文件。
1 | NON_O_FILES := $(filter-out %.o,$(FILES)) |
9. $(sort list)
将list
中的单词排序并去除重复的单词。
例子:排序并去重源文件列表。
1 | UNIQUE_SRCS := $(sort $(SRCS)) |
10. $(foreach var,list,text)
对list
中的每个单词执行text
中的表达式,其中var
作为当前单词的变量。
例子:为每个源文件打印一条编译信息。
1 | $(foreach src,$(SRCS),echo Compiling $(src);) |
在Makefile中,使用一些高效的表达式和技巧可以大大提高构建系统的灵活性和可维护性。以下是一些常用的表达式和技巧,它们可以帮助你更有效地编写Makefile。
技巧
1. 使用变量简化文件列表
定义变量来简化文件列表的管理,使得在多个地方引用时不需要重复书写。
1 | SRCS := main.c foo.c bar.c |
这里,.c
文件列表被赋值给SRCS
变量,然后使用模式替换将.c
后缀替换为.o
生成对象文件列表,并赋值给OBJS
变量。
2. 使用通配符自动获取文件列表
使用wildcard
函数自动获取目录下的文件列表,避免手动列出。
1 | SRCS := $(wildcard src/*.c) |
$(wildcard .config)是一个Makefile函数,用于查找当前目录下名为.config的文件。如果找到文件,则返回文件的完整路径;如果没有找到,则返回空。
3. 自动变量简化规则
使用自动变量简化规则的书写。例如$@
代表规则的目标,$<
代表第一个依赖,$^
代表所有依赖。
1 | app: $(OBJS) |
4. 使用模式规则
模式规则可以应用于匹配模式的目标,使得规则更加通用。
1 | %.o: %.c |
这条规则表示任何.o
文件都依赖于同名的.c
文件,并描述了如何从.c
文件构建.o
文件。
$*,表示不带前缀的目标名称)
$<,表示规则的第一个依赖
5. 条件判断
使用条件判断来根据不同的条件选择不同的操作。
1 | ifeq ($(DEBUG),yes) |
根据DEBUG
变量的值,调整编译标志。
6. 函数使用
Makefile提供了许多内置函数,如filter
、patsubst
等,用于处理文本和文件名。
1 | DEBUG_SRCS := $(filter %_debug.c,$(SRCS)) |
使用filter
和filter-out
函数来区分调试和发布的源文件。
8. 多目标规则
一个规则可以有多个目标,用于执行相同的命令序列构建多个目标。
1 | all: prog1 prog2 |
9. .PHONY目标
使用.PHONY
声明伪目标,以避免与同名文件冲突。
1 |
|
在Makefile中,处理字符串和变量的几个有用概念包括子集(subset
)、单词(word
)操作和变量类型的检查(flavor
)。虽然Makefile本身不直接提供名为subset
的函数,但是可以通过组合使用其他函数来实现子集选择等操作。下面是对word
函数和flavor
函数的解释,以及如何模拟实现子集选择的示例。
在Makefile中,$(subst from,to,text)函数用于文本替换,它在text字符串中将所有出现的from字符串替换为to字符串。在你提供的表达式:
1 | ARCH_SPLIT = $(subst -, ,$(ARCH)) |
这行代码的作用是将变量ARCH中的所有破折号(-)替换为空格。这种替换通常用于处理类似于x86_64-linux-gnu这样的复合体系结构名称或其他用破折号连接的字符串,目的是将它们拆分为由空格分隔的单词列表,以便于后续操作或查询。
word 函数
word
函数用于从以空格分隔的单词列表中选取第n
个单词。其语法如下:
1 | $(word n,text) |
n
是要选取的单词的位置(从1开始计数)。text
是单词列表。
例如,如果你有一个文件列表,想要选取第二个文件名,可以这样做:
1 | FILES := file1.c file2.c file3.c |
这里,SECOND_FILE
的值将是file2.c
。
flavor 函数
flavor
函数用于查询变量的类型。Makefile中的变量可以是简单展开的(simple
)、递归展开的(recursive
)、环境变量(environment
)、命令行定义的(command line
)等。
1 | $(flavor variable-name) |
例如:
1 | VAR := simple |
override
override ARGS ?= …行使用了条件赋值操作符?=, 它只在ARGS未定义时设置值。override关键字用于确保即使在命令行中定义了ARGS变量,这个赋值也会生效。
::
$(BINARY):: compile_git
这里定义了一个目标$(BINARY),它有一个额外的依赖compile_git。$(BINARY)很可能是一个变量,表示要构建的二进制文件的名字。使用::而不是单个:定义规则,表示这是一个终端规则(Terminal Rule),它允许同一个目标有多个独立的规则定义,这些规则都会被执行。
$(MAKE)
$(MAKE) -C $(NEMU_HOME):
$(MAKE)是一个特殊变量,代表make工具的名称,通常就是make。这允许在Makefile中递归地调用make。
-C $(NEMU_HOME)选项告诉make更改到目录$(NEMU_HOME)然后执行后续的make操作。$(NEMU_HOME)应该是一个变量,它指定了NEMU(一个可能的模拟器或工具链)的根目录。
if [-s.. ]
在 shell 脚本中,[ -s … ] 是一个条件测试,用于检查文件的大小是否非零(即文件是否存在且至少有一个字节)。-s 是这个测试中的特定选项。
除了 -s 之外,还有很多其他的文件测试操作符可以在 [ … ] 或 [[ … ]] 中使用(注意 [ 和 ] 之间应有空格)。以下是一些常见的文件测试操作符:
-e:文件存在
-f:文件是一个常规文件(不是目录、设备文件等)
-d:文件是一个目录
-r:文件存在且可读
-w:文件存在且可写
-x:文件存在且可执行
-L:文件是一个符号链接
-S:文件是一个套接字
-b:文件是一个块特殊文件
-c:文件是一个字符特殊文件
-p:文件是一个命名管道(FIFO)
-N:文件自上次读取后已被修改
-O:文件由当前用户拥有
-G:文件的组与当前用户相同
此外,还有一些数字比较和字符串比较操作符等。
例如,如果你想检查一个文件是否存在并且是一个目录,你可以使用:
bash
if [ -d “/path/to/directory” ]; then
echo “It’s a directory!”
fi
希望这能帮助你理解 [ … ] 中的各种文件测试操作符。