makefile
一、Makefile 核心概念
1.1 基本结构
- 目标: 要生成的文件或任务名称
- 依赖: 构建目标所需的文件
- 命令: 如何从依赖生成目标(必须用 Tab 开头)
1.2 执行规则
- 检查目标是否需要重建
- 先递归构建所有依赖
- 如果依赖比目标新,或目标不存在,则执行命令
- 否则跳过(已经是最新)
1.3 依赖关系
- 依赖关系是树形结构
- Make 从叶子节点开始向上构建
- 示例:
program → main.o utils.o → main.c utils.c
二、变量和赋值操作符
2.1 赋值操作符表格
操作符 |
名称 |
行为 |
示例 |
使用场景 |
= |
递归展开赋值 |
延迟求值,使用时展开 |
A = $(B) |
需要动态值的变量 |
:= |
简单展开赋值 |
立即求值,定义时展开 |
A := value |
推荐默认使用,性能好 |
+= |
追加赋值 |
向变量追加内容 |
CFLAGS += -g |
增量添加选项 |
?= |
条件赋值 |
如果变量未定义则赋值 |
PREFIX ?= /usr |
提供默认值 |
!= |
Shell赋值 |
执行shell命令并赋值 |
DATE != date |
获取shell命令结果 |
2.2 变量使用示例
CC := gcc
CFLAGS = -Wall
SRCS := main.c utils.c
OBJS := $(SRCS:.c=.o) # 模式替换
TARGET := program
三、特殊符号和自动变量
3.1 自动变量表格
符号 |
含义 |
示例 |
说明 |
$@ |
当前目标名 |
$(CC) -o $@ |
代表规则中的目标文件名 |
$< |
第一个依赖文件 |
$(CC) -c $< |
代表规则中第一个依赖文件 |
$^ |
所有依赖文件 |
$(CC) $^ -o $@ |
代表规则中所有依赖文件 |
$? |
比目标新的依赖 |
$(CC) $? -o $@ |
只重新编译更新的文件 |
$* |
匹配通配符的部分 |
%.o: %.c 中的 $* |
代表 % 匹配的部分 |
$(@D) |
目标目录部分 |
$(@D)/file |
build/main.o → build |
$(@F) |
目标文件名部分 |
$(@F) |
build/main.o → main.o |
3.2 命令前缀表格
前缀 |
作用 |
示例 |
@ |
不显示执行的命令 |
@echo "Hello" |
- |
忽略命令的错误 |
-rm file |
+ |
总是执行命令 |
+make sub |
四、函数功能
4.1 常用函数表格
函数 |
语法 |
作用 |
示例 |
wildcard |
$(wildcard pattern) |
文件通配 |
$(wildcard *.c) |
patsubst |
$(patsubst p,r,text) |
模式替换 |
$(patsubst %.c,%.o,$(SRCS)) |
subst |
$(subst from,to,text) |
文本替换 |
$(subst .c,.o,$(FILES)) |
filter |
$(filter pattern,text) |
过滤匹配 |
$(filter %.c,$(FILES)) |
filter-out |
$(filter-out p,text) |
过滤排除 |
$(filter-out test%,$(FILES)) |
addprefix |
$(addprefix p,names) |
添加前缀 |
$(addprefix build/,$(OBJS)) |
addsuffix |
$(addsuffix s,names) |
添加后缀 |
$(addsuffix .o,$(FILES)) |
shell |
$(shell command) |
执行shell命令 |
$(shell date) |
foreach |
$(foreach v,list,text) |
循环处理 |
$(foreach f,$(FILES),build/$(f)) |
if |
$(if cond,then,else) |
条件判断 |
$(if $(DEBUG),-g,-O2) |
4.2 函数使用示例
# 文件操作
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c,build/%.o,$(SRCS))
INCLUDES := $(addprefix -I,include lib/include)
# 条件编译
OPTIMIZATION := $(if $(DEBUG),-g -O0,-O2 -DNDEBUG)
五、伪目标 (Phony Target)
5.1 伪目标定义
.PHONY: clean install all
5.2 伪目标 vs 真实目标
类型 |
示例 |
特点 |
是否需要文件 |
真实目标 |
main.o: main.c |
对应实际文件 |
是 |
伪目标 |
clean: |
不对应实际文件 |
否 |
5.3 为什么需要伪目标?
- 防止与同名文件冲突
- 声明不生成文件的任务
- 常见伪目标:
all clean install distclean help test
六、高级特性
6.1 模式规则
# 通用模式规则
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 静态模式规则
OBJECTS = main.o utils.o
$(OBJECTS): %.o: %.c
$(CC) -c $< -o $@
6.2 包含其他文件
include config.mk
-include $(DEP_FILES) # 安静包含(文件不存在不报错)
6.3 条件判断
ifdef DEBUG
CFLAGS += -g -DDEBUG
else
CFLAGS += -O2
endif
6.4 多目标规则
obj1.o obj2.o: common.h
@echo "这两个目标都依赖 common.h"
七、特殊目标表格
目标 |
作用 |
示例 |
.DEFAULT_GOAL |
设置默认目标 |
.DEFAULT_GOAL := all |
.PHONY |
声明伪目标 |
.PHONY: clean |
.SUFFIXES |
定义后缀规则 |
.SUFFIXES: .c .o |
.DELETE_ON_ERROR |
出错时删除目标 |
.DELETE_ON_ERROR: |
八、完整示例代码
# Common part for the Makefile.
# This file will be included by the Makefile of each project.
# Custom Macro Definition (Common part)
include ../defines.mk
DEFS +=
CROSS_COMPILE = riscv64-unknown-elf-
CFLAGS += -nostdlib -fno-builtin -g -Wall
CFLAGS += -march=rv32g -mabi=ilp32
QEMU = qemu-system-riscv32
QFLAGS = -nographic -smp 1 -machine virt -bios none
GDB = gdb-multiarch
CC = ${CROSS_COMPILE}gcc
OBJCOPY = ${CROSS_COMPILE}objcopy
OBJDUMP = ${CROSS_COMPILE}objdump
MKDIR = mkdir -p
RM = rm -rf
OUTPUT_PATH = out
# SRCS_ASM & SRCS_C are defined in the Makefile of each project.
OBJS_ASM := $(addprefix ${OUTPUT_PATH}/, $(patsubst %.S, %.o, ${SRCS_ASM}))
OBJS_C := $(addprefix $(OUTPUT_PATH)/, $(patsubst %.c, %.o, ${SRCS_C}))
OBJS = ${OBJS_ASM} ${OBJS_C}
ELF = ${OUTPUT_PATH}/os.elf
BIN = ${OUTPUT_PATH}/os.bin
USE_LINKER_SCRIPT ?= true
ifeq (${USE_LINKER_SCRIPT}, true)
LDFLAGS = -T ${OUTPUT_PATH}/os.ld.generated
else
LDFLAGS = -Ttext=0x80000000
endif
.DEFAULT_GOAL := all
all: ${OUTPUT_PATH} ${ELF}
${OUTPUT_PATH}:
@${MKDIR} $@
# start.o must be the first in dependency!
#
# For USE_LINKER_SCRIPT == true, before do link, run preprocessor manually for
# linker script.
# -E specifies GCC to only run preprocessor
# -P prevents preprocessor from generating linemarkers (#line directives)
# -x c tells GCC to treat your linker script as C source file
${ELF}: ${OBJS}
ifeq (${USE_LINKER_SCRIPT}, true)
${CC} -E -P -x c ${DEFS} ${CFLAGS} os.ld > ${OUTPUT_PATH}/os.ld.generated
endif
${CC} ${CFLAGS} ${LDFLAGS} -o ${ELF} $^
${OBJCOPY} -O binary ${ELF} ${BIN}
${OUTPUT_PATH}/%.o : %.c
${CC} ${DEFS} ${CFLAGS} -c -o $@ $<
${OUTPUT_PATH}/%.o : %.S
${CC} ${DEFS} ${CFLAGS} -c -o $@ $<
run: all
@${QEMU} -M ? | grep virt >/dev/null || exit
@echo "Press Ctrl-A and then X to exit QEMU"
@echo "------------------------------------"
@${QEMU} ${QFLAGS} -kernel ${ELF}
.PHONY : debug
debug: all
@echo "Press Ctrl-C and then input 'quit' to exit GDB and QEMU"
@echo "-------------------------------------------------------"
@${QEMU} ${QFLAGS} -kernel ${ELF} -s -S &
@${GDB} ${ELF} -q -x ../gdbinit
.PHONY : code
code: all
@${OBJDUMP} -S ${ELF} | less
.PHONY : clean
clean:
@${RM} ${OUTPUT_PATH}
九、最佳实践总结
- 优先使用
:=
赋值,性能更好
- 合理使用伪目标,避免文件名冲突
- 利用自动变量 减少重复代码
- 使用模式规则 处理同类文件
- 包含依赖文件 实现精确的重建
- 设置默认目标 明确入口点
- 使用函数 简化复杂操作