跳转至

makefile

一、Makefile 核心概念

1.1 基本结构

目标: 依赖文件
    构建命令
  • 目标: 要生成的文件或任务名称
  • 依赖: 构建目标所需的文件
  • 命令: 如何从依赖生成目标(必须用 Tab 开头)

1.2 执行规则

  1. 检查目标是否需要重建
  2. 先递归构建所有依赖
  3. 如果依赖比目标新,或目标不存在,则执行命令
  4. 否则跳过(已经是最新)

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.obuild
$(@F) 目标文件名部分 $(@F) build/main.omain.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}

九、最佳实践总结

  1. 优先使用 := 赋值,性能更好
  2. 合理使用伪目标,避免文件名冲突
  3. 利用自动变量 减少重复代码
  4. 使用模式规则 处理同类文件
  5. 包含依赖文件 实现精确的重建
  6. 设置默认目标 明确入口点
  7. 使用函数 简化复杂操作