title: Makefile created: 2026-08-31 type: source tags:
# 小项目手动编译
gcc main.c utils.c -o app
# 大项目手动编译(噩梦)
gcc src/main.c src/utils.c src/network.c src/crypto.c \
-I./include -L./lib -lssl -lcrypto -lpthread -o app
# 修改一个文件后,全部重新编译 → 浪费时间
| 问题 | Makefile 的解决方案 |
|---|---|
| 编译命令繁琐 | 一条 make 搞定 |
| 全量编译浪费 | 增量编译:只重建修改过的文件及其依赖 |
| 依赖关系混乱 | 显式声明文件间依赖关系 |
| 多目标管理 | make clean、make install 等伪目标 |
| 跨平台/跨工具链 | 变量机制,换工具链只需改一个变量 |
target: prereq1 prereq2
command1
command2
.o 或可执行文件)1. 读取 Makefile
2. 找到第一个目标(默认目标)
3. 检查目标是否存在、依赖是否比目标新
4. 如果依赖更新或目标不存在 → 执行命令
5. 递归处理依赖(如果依赖也是目标)
核心机制:时间戳比较
如果 依赖文件的修改时间 > 目标文件的修改时间 → 重建目标
否则 → 跳过(认为目标已是最新)
# 一个完整的、可编译的最小示例
app: main.o utils.o
gcc main.o utils.o -o app
main.o: main.c utils.h
gcc -c main.c -o main.o
utils.o: utils.c utils.h
gcc -c utils.c -o utils.o
clean:
rm -f *.o app
# 使用
make # 编译
make clean # 清理
| 运算符 | 名称 | 行为 | 示例 |
|---|---|---|---|
= |
延迟展开 | 使用时才求值 | OBJS = $(SRC:.c=.o) |
:= |
立即展开 | 赋值时立刻求值 | OBJS := main.o utils.o |
?= |
条件赋值 | 仅当变量未定义时赋值 | CC ?= gcc |
+= |
追加 | 在现有值后追加内容 | CFLAGS += -Wall |
# 延迟展开 vs 立即展开
CC = gcc # 延迟展开(用时求值)
OBJS := main.o utils.o # 立即展开(定义时求值)
# 条件赋值:允许外部覆盖
CC ?= gcc # 默认 gcc,但 make CC=clang 可覆盖
# 追加
CFLAGS = -Wall
CFLAGS += -O2 # 现在 CFLAGS = "-Wall -O2"
CC = gcc
CFLAGS = -Wall -O2
# 引用变量
$(CC) $(CFLAGS) main.c -o main
# 等价于
gcc -Wall -O2 main.c -o main
| 变量 | 含义 | 示例 |
|---|---|---|
$@ |
当前目标 | app: main.o → gcc main.o -o $@ |
$< |
第一个依赖 | main.o: main.c → gcc -c $< -o $@ |
$^ |
所有依赖(去重) | app: a.o b.o → gcc $^ -o $@ |
$? |
比目标新的依赖 | 只传递修改过的文件 |
$* |
模式匹配的词干 | %.o: %.c 中,$* 是不带后缀的文件名 |
# 使用自动变量改写
app: main.o utils.o
gcc $^ -o $@ # gcc main.o utils.o -o app
main.o: main.c utils.h
gcc -c $< -o $@ # gcc -c main.c -o main.o
% 和 *| 符号 | 使用场景 | 含义 |
|---|---|---|
% |
规则中 | 匹配任意长度的文件名前缀(词干) |
* |
文件名中 | Shell 风格通配符,匹配任意字符 |
# * 通配符 — 在变量或依赖中匹配文件
SRCS = $(wildcard src/*.c) # 获取 src/ 下所有 .c 文件
OBJS = $(SRCS:.c=.o) # 将 .c 替换为 .o
# % 通配符 — 在规则中匹配
%.o: %.c
gcc -c $< -o $@
# 所有 .c → .o 的通用规则
%.o: %.c
gcc -c $< -o $@
# 指定依赖
%.o: %.c include/*.h
gcc -Wall -O2 -c $< -o $@
Make 内置了隐含规则:
# 以下规则可以省略(make 自动推导)
main.o: main.c
# make 自动执行 gcc -c main.c -o main.o
建议: 始终显式写出规则,避免依赖隐含行为。
| 函数 | 作用 | 示例 |
|---|---|---|
$(wildcard pat) |
匹配文件 | $(wildcard src/*.c) |
$(patsubst p,r,t) |
模式替换 | $(patsubst %.c,%.o,src/*.c) |
$(notdir names) |
取文件名(去掉路径) | $(notdir src/main.c) → main.c |
$(dir names) |
取目录 | $(dir src/main.c) → src/ |
$(foreach v,l,t) |
循环 | $(foreach d,$(DIRS),$(wildcard $(d)/*.c)) |
# 项目结构:
# project/
# ├── include/ # 头文件
# │ ├── utils.h
# │ └── network.h
# ├── src/ # 源文件
# │ ├── main.c
# │ ├── utils.c
# │ └── network/
# │ ├── tcp.c
# │ └── udp.c
# ├── lib/ # 外部库
# └── Makefile
CC = gcc
CFLAGS = -Wall -O2 -I./include
# 递归查找所有 .c 文件
SRCS = $(shell find src -name '*.c')
OBJS = $(SRCS:.c=.o)
# 自动创建输出目录
OBJDIR = build
$(shell mkdir -p $(OBJDIR))
TARGET = app
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $^ -o $@
# 模式规则:所有 .c 编译为 .o,放入 build 目录
$(OBJDIR)/%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
.PHONY: all clean
# ============================================
# 嵌入式交叉编译 Makefile 模板
# ============================================
# 交叉编译工具链
CROSS_COMPILE ?= arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar
STRIP = $(CROSS_COMPILE)strip
# 编译选项
CFLAGS = -Wall -O2 -g
CFLAGS += -I./include
CFLAGS += -DDEBUG -DVERSION=\"1.0\"
CFLAGS += -std=c11
# 链接选项
LDFLAGS = -static
LIBS = -lpthread
# 目标
TARGET = app
# 源文件与目标文件
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
# 默认目标
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
$(STRIP) $@ # 去掉调试符号,减小体积
@echo "Build complete: $(TARGET)"
# 通用编译规则
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# 头文件依赖自动生成(-MMD -MP)
-include $(OBJS:.o=.d)
%.o: %.c
$(CC) $(CFLAGS) -MMD -MP -c -o $@ $<
# 清理
clean:
rm -f $(OBJS) $(OBJS:.o=.d) $(TARGET)
# 安装(通常用于根文件系统部署)
DESTDIR ?= /usr/local
install: $(TARGET)
install -d $(DESTDIR)/bin
install -m 755 $(TARGET) $(DESTDIR)/bin/
.PHONY: all clean install
# -MMD 生成 .d 文件(依赖关系),如 main.d:
# main.o: main.c utils.h config.h
# -MP 生成空规则防止头文件删除时报错
-include $(OBJS:.o=.d)
# 每次编译时自动生成依赖,下次 make 自动检查头文件变化
.PHONY: all clean install test
all: $(TARGET)
clean:
rm -f $(OBJS) $(TARGET)
install:
cp $(TARGET) /usr/local/bin/
test:
./run_tests.sh
为什么要声明
.PHONY? 如果当前目录下有名为clean的文件,make clean会因文件已存在而跳过。声明伪目标可避免此问题。
# 根据平台选择不同选项
ifeq ($(OS),Windows_NT)
TARGET = app.exe
RM = del /Q
else
TARGET = app
RM = rm -f
endif
# 根据编译类型选择
ifdef DEBUG
CFLAGS += -g -O0
else
CFLAGS += -O2
endif
# 使用
make DEBUG=1 # 调试版本
make # 发布版本
SRCS = src/main.c src/utils.c src/net/tcp.c
# 取所有文件名(去掉路径)
$(notdir $(SRCS)) # → main.c utils.c tcp.c
# 模式替换
$(patsubst %.c,%.o,$(SRCS)) # → src/main.o src/utils.o src/net/tcp.o
# 简写
$(SRCS:.c=.o) # → 同上
# 循环
DIRS = src include lib
$(foreach d,$(DIRS),$(wildcard $(d)/*.c))
OBJS = main.o utils.o network.o
# 只对这些特定文件应用规则
$(OBJS): %.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# 同一规则生成多个目标
all: libfoo.so libfoo.a
libfoo.so: foo.o
$(CC) -shared -o $@ $^
libfoo.a: foo.o
$(AR) rcs $@ $^
# 在编译前确保目录存在
OBJDIR = build
$(shell mkdir -p $(OBJDIR))
$(OBJDIR)/%.o: %.c
$(CC) -c $< -o $@
# 打印变量值,排查 Makefile 问题
all:
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
@echo "CC = $(CC)"
以下是一个真实的嵌入式裸机项目 Makefile,涵盖了多目录编译、交叉编译、链接脚本、反汇编等生产级特性。
project/
├── imx6ul/ # 芯片级头文件(寄存器定义等)
│ ├── imx6ul.h
│ └── fsl_common.h
├── bsp/ # 板级支持包
│ ├── clk/ # 时钟驱动
│ │ ├── bsp_clk.h
│ │ └── bsp_clk.c
│ ├── led/ # LED 驱动
│ │ ├── bsp_led.h
│ │ └── bsp_led.c
│ └── delay/ # 延时驱动
│ ├── bsp_delay.h
│ └── bsp_delay.c
├── project/ # 应用入口
│ └── main.c
├── imx6ul.lds # 链接脚本(定义内存布局)
├── obj/ # 编译输出目录(.o 文件)
└── Makefile
CROSS_COMPILE ?= arm-linux-gnueabihf-
TARGET ?= bsp
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
INCDIRS := imx6ul \
bsp/clk \
bsp/led \
bsp/delay
SRCDIRS := project \
bsp/clk \
bsp/led \
bsp/delay
INCLUDE := $(patsubst %, -I %, $(INCDIRS))
SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
SFILENDIR := $(notdir $(SFILES))
CFILENDIR := $(notdir $(CFILES))
SOBJS := $(patsubst %, obj/%, $(SFILENDIR:.S=.o))
COBJS := $(patsubst %, obj/%, $(CFILENDIR:.c=.o))
OBJS := $(SOBJS) $(COBJS)
VPATH := $(SRCDIRS)
.PHONY: clean
$(TARGET).bin : $(OBJS)
$(LD) -Timx6ul.lds -o $(TARGET).elf $^
$(OBJCOPY) -O binary -S $(TARGET).elf $@
$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis
$(SOBJS) : obj/%.o : %.S
$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
$(COBJS) : obj/%.o : %.c
$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
clean:
rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)
CROSS_COMPILE ?= arm-linux-gnueabihf-
TARGET ?= bsp
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
| 变量 | 赋值方式 | 作用 |
|---|---|---|
CROSS_COMPILE |
?= |
条件赋值,允许外部覆盖。make CROSS_COMPILE=aarch64-linux-gnu- 可切换工具链 |
TARGET |
?= |
同上,默认目标名为 bsp,可外部指定 |
CC |
:= |
立即展开,派生出 arm-linux-gnueabihf-gcc |
LD |
:= |
链接器,用于将 .o 链接为 .elf |
OBJCOPY |
:= |
格式转换工具,将 .elf 转为裸二进制 .bin |
OBJDUMP |
:= |
反汇编工具,生成 .dis 文件供调试分析 |
为什么用
?=而不是:=??=允许用户在命令行覆盖,例如make CROSS_COMPILE=arm-linux-gnueabihf-,方便在同一台机器上切换不同工具链。
INCDIRS := imx6ul \
bsp/clk \
bsp/led \
bsp/delay
SRCDIRS := project \
bsp/clk \
bsp/led \
bsp/delay
INCDIRS 和 SRCDIRS 分别列出所有头文件目录和源文件目录。裸机项目通常按功能模块拆分目录,每个模块包含自己的 .c 和 .h 文件。
-I 编译选项INCLUDE := $(patsubst %, -I %, $(INCDIRS))
patsubst 执行过程:
输入: imx6ul bsp/clk bsp/led bsp/delay
模式: % → -I %
输出: -I imx6ul -I bsp/clk -I bsp/led -I bsp/delay
最终 INCLUDE 变量展开为编译器的头文件搜索路径参数。
SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
展开过程(以 CFILES 为例):
SRCDIRS = project bsp/clk bsp/led bsp/delay
$(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
→ $(wildcard project/*.c) → project/main.c
$(wildcard bsp/clk/*.c) → bsp/clk/bsp_clk.c
$(wildcard bsp/led/*.c) → bsp/led/bsp_led.c
$(wildcard bsp/delay/*.c) → bsp/delay/bsp_delay.c
最终: CFILES = project/main.c bsp/clk/bsp_clk.c bsp/led/bsp_led.c bsp/delay/bsp_delay.c
foreach + wildcard 组合是多目录项目自动收集源文件的标准模式。
SFILENDIR := $(notdir $(SFILES))
CFILENDIR := $(notdir $(CFILES))
SOBJS := $(patsubst %, obj/%, $(SFILENDIR:.S=.o))
COBJS := $(patsubst %, obj/%, $(CFILENDIR:.c=.o))
OBJS := $(SOBJS) $(COBJS)
逐步展开:
CFILES = project/main.c bsp/clk/bsp_clk.c bsp/led/bsp_led.c bsp/delay/bsp_delay.c
① notdir: 去掉路径前缀
CFILENDIR = main.c bsp_clk.c bsp_led.c bsp_delay.c
② 后缀替换: .c → .o
CFILENDIR:.c=.o → main.o bsp_clk.o bsp_led.o bsp_delay.o
③ patsubst: 加上 obj/ 前缀
COBJS = obj/main.o obj/bsp_clk.o obj/bsp_led.o obj/bsp_delay.o
所有 .o 文件统一输出到 obj/ 目录,保持源码目录干净。
VPATH := $(SRCDIRS)
这是本 Makefile 最关键的设计之一。
VPATH 告诉 make:当在当前目录找不到某个依赖文件时,依次去 project、bsp/clk、bsp/led、bsp/delay 目录下搜索。
为什么需要 VPATH?
目标文件统一放在 obj/ 目录(如 obj/main.o),但源文件分散在多个子目录。模式规则写的是:
$(COBJS) : obj/%.o : %.c
make 看到 obj/main.o 需要 main.c,但 main.c 不在当前目录。VPATH 让 make 去 project/ 目录下找到它,然后执行:
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -I imx6ul -I bsp/clk ... -o obj/main.o project/main.c
没有 VPATH 会怎样? make 会报错 No rule to make target 'main.c', needed by 'obj/main.o'。
$(SOBJS) : obj/%.o : %.S
$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
$(COBJS) : obj/%.o : %.c
$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
静态模式规则语法:
$(targets): pattern: prereqs
command
$(SOBJS) 限定此规则只对 SOBJS 列表中的目标生效obj/%.o : %.S 是模式部分,% 匹配文件名词干(如 start)$@ = obj/start.o(当前目标)$< = start.S(第一个依赖,由 VPATH 解析)-nostdlib 的含义: 不链接标准库(libc、libgcc 等),因为裸机环境没有操作系统提供的 C 运行时库。
$(TARGET).bin : $(OBJS)
$(LD) -Timx6ul.lds -o $(TARGET).elf $^
$(OBJCOPY) -O binary -S $(TARGET).elf $@
$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis
| 步骤 | 命令 | 作用 |
|---|---|---|
| 1 | ld -Timx6ul.lds -o bsp.elf $^ |
用链接脚本将所有 .o 链接为 ELF 可执行文件 |
| 2 | objcopy -O binary -S bsp.elf bsp.bin |
从 ELF 提取裸二进制,用于烧录到 Flash |
| 3 | objdump -D -m arm bsp.elf > bsp.dis |
反汇编全部内容,生成 .dis 文件供调试分析 |
链接脚本 imx6ul.lds 的作用: 定义内存布局——代码段放哪里、数据段放哪里、栈的起始地址等。裸机项目没有操作系统的内存管理,必须手动指定。
为什么需要 .bin? ELF 是 Linux 可执行文件格式,包含元数据。烧录到裸机 Flash 时需要纯二进制 .bin(不含 ELF 头、段表等)。
clean:
rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)
删除所有生成物,恢复到干净状态。
┌─────────────────────────────────┐
│ make 执行流程 │
└─────────────────────────────────┘
│
┌───────────────▼───────────────┐
│ 1. 默认目标: bsp.bin │
│ 依赖: $(OBJS) │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 2. 检查 OBJS 中每个 .o 文件 │
│ obj/main.o → main.c (VPATH)│
│ obj/bsp_clk.o → bsp/clk/ │
│ obj/bsp_led.o → bsp/led/ │
│ ... │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 3. 模式规则编译 │
│ .c → obj/*.o (gcc -c) │
│ .S → obj/*.o (gcc -c) │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 4. 链接 │
│ ld → bsp.elf │
└───────────────┬───────────────┘
│
┌───────────────▼───────────────┐
│ 5. 格式转换 │
│ objcopy → bsp.bin (烧录用) │
│ objdump → bsp.dis (调试用) │
└───────────────────────────────┘
| 技巧 | 本例用法 | 解决的问题 |
|---|---|---|
?= 条件赋值 |
CROSS_COMPILE ?= |
允许命令行覆盖工具链 |
foreach + wildcard |
自动收集多目录源文件 | 无需手动列出每个文件 |
notdir + patsubst |
生成 obj/ 前缀的目标列表 |
统一输出目录,保持源码整洁 |
VPATH |
指定源文件搜索路径 | 解决目标与源文件路径分离的问题 |
| 静态模式规则 | 分别处理 .S 和 .c |
汇编和 C 用不同编译选项 |
链接脚本 -T |
imx6ul.lds |
裸机项目的内存布局定义 |
objcopy |
ELF → bin | 生成可烧录的裸二进制 |
objdump -D |
生成反汇编 | 调试时对照源码分析 |
# ============================================
# Makefile — LED 控制应用(嵌入式交叉编译)
# ============================================
CROSS_COMPILE ?= arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
STRIP = $(CROSS_COMPILE)strip
CFLAGS = -Wall -O2 -g -I./include
CFLAGS += -DUSE_GPIO -DDEVICE=\"/dev/gpio0\"
LDFLAGS = -static
TARGET = led_app
SRCS = src/main.c src/led.c src/gpio.c
OBJS = $(SRCS:.c=.o)
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^
$(STRIP) $@
@echo "=== Build complete: $(TARGET) ==="
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(OBJS) $(TARGET)
# 烧录到设备(示例)
deploy: $(TARGET)
scp $(TARGET) root@192.168.1.100:/usr/bin/
ssh root@192.168.1.100 "chmod +x /usr/bin/$(TARGET)"
.PHONY: all clean deploy
@ 前缀:@echo 不打印命令本身,只打印输出- 前缀:-rm 表示忽略错误继续执行$ 要转义:在 Shell 命令中用 $$ 表示 Shell 变量,如 $$HOMEmake -j4 开启 4 个并行任务,大幅加速编译make 默认执行第一个目标:将 all 放在最前面-MMD -MP 避免手动维护头文件依赖A: 基于时间戳的增量编译。make 比较目标文件和依赖文件的修改时间,如果依赖更新则重建目标。只重建需要更新的部分,避免全量编译。
= 和 := 的区别?A: = 延迟展开(使用时求值),:= 立即展开(定义时求值)。OBJS = $(SRC:.c=.o) 用 = 时,SRC 后续修改会影响 OBJS;用 := 则不会。
$@、$<、$^ 分别是什么?A: 自动变量在规则执行时自动赋值:$@ 是当前目标,$< 是第一个依赖,$^ 是所有依赖(去重)。用自动变量可以写出通用的模式规则。
A: 用 $(wildcard src/**/*.c) 或 $(shell find src -name '*.c') 递归查找源文件,用模式规则 %.o: %.c 配合 -o $@ 输出到对应目录,用 -MMD -MP 自动生成头文件依赖。
-MMD -MP 的作用?A: -MMD 在编译时自动生成 .d 依赖文件(记录目标依赖哪些头文件),-MP 生成空规则防止头文件被删除后 make 报错。通过 -include $(OBJS:.o=.d) 引入,实现头文件变更时自动重新编译。
A: 这是 make 的语法约定。make 用 Tab 来区分规则定义和命令。如果用空格缩进,make 会将其误认为规则的一部分而报语法错误。
A: 1)设置正确的 CROSS_COMPILE 前缀;2)使用 -static 避免目标设备缺动态库;3)交叉编译的工具链路径需正确;4)可能需要指定 --sysroot 指向目标系统的根文件系统。
A: 1)make -n(dry-run)打印要执行的命令但不执行;2)make -p 打印完整规则数据库;3)在规则中加 @echo 打印变量值;4)make --debug=b 显示决策原因。