2. Makefile.md 13 KB


title: Makefile created: 2026-08-31 type: source tags:

  • makefile
  • build
  • linux
  • embedded ---

Makefile 详解

一、为什么需要 Makefile

1.1 手动编译的痛点

# 小项目手动编译
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

# 修改一个文件后,全部重新编译 → 浪费时间

1.2 Makefile 解决的问题

问题 Makefile 的解决方案
编译命令繁琐 一条 make 搞定
全量编译浪费 增量编译:只重建修改过的文件及其依赖
依赖关系混乱 显式声明文件间依赖关系
多目标管理 make cleanmake install 等伪目标
跨平台/跨工具链 变量机制,换工具链只需改一个变量

二、Makefile 基本规则

2.1 规则语法

target: prereq1 prereq2
	command1
	command2
  • target:目标文件(通常是 .o 或可执行文件)
  • prereq(prerequisites):依赖文件
  • command:构建命令(必须以 Tab 开头,不能用空格

2.2 make 的执行逻辑

1. 读取 Makefile
2. 找到第一个目标(默认目标)
3. 检查目标是否存在、依赖是否比目标新
4. 如果依赖更新或目标不存在 → 执行命令
5. 递归处理依赖(如果依赖也是目标)

核心机制:时间戳比较

如果 依赖文件的修改时间 > 目标文件的修改时间 → 重建目标
否则 → 跳过(认为目标已是最新)

2.3 最小 Makefile 示例

# 一个完整的、可编译的最小示例
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    # 清理

三、变量

3.1 四种赋值方式

运算符 名称 行为 示例
= 延迟展开 使用时才求值 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"

3.2 变量引用

CC = gcc
CFLAGS = -Wall -O2

# 引用变量
$(CC) $(CFLAGS) main.c -o main
# 等价于
gcc -Wall -O2 main.c -o main

四、自动变量

变量 含义 示例
$@ 当前目标 app: main.ogcc main.o -o $@
$< 第一个依赖 main.o: main.cgcc -c $< -o $@
$^ 所有依赖(去重) app: a.o b.ogcc $^ -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

五、通配符与模式规则

5.1 通配符 %*

符号 使用场景 含义
% 规则中 匹配任意长度的文件名前缀(词干)
* 文件名中 Shell 风格通配符,匹配任意字符
# * 通配符 — 在变量或依赖中匹配文件
SRCS = $(wildcard src/*.c)          # 获取 src/ 下所有 .c 文件
OBJS = $(SRCS:.c=.o)                # 将 .c 替换为 .o

# % 通配符 — 在规则中匹配
%.o: %.c
	gcc -c $< -o $@

5.2 模式规则

# 所有 .c → .o 的通用规则
%.o: %.c
	gcc -c $< -o $@

# 指定依赖
%.o: %.c include/*.h
	gcc -Wall -O2 -c $< -o $@

5.3 自动推导规则

Make 内置了隐含规则:

# 以下规则可以省略(make 自动推导)
main.o: main.c
	# make 自动执行 gcc -c main.c -o main.o

建议: 始终显式写出规则,避免依赖隐含行为。

六、多目录编译

6.1 常用函数

函数 作用 示例
$(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))

6.2 多目录项目完整示例

# 项目结构:
# 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 模板

# ============================================
# 嵌入式交叉编译 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 自动检查头文件变化

八、常用 Makefile 技巧

8.1 伪目标

.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 会因文件已存在而跳过。声明伪目标可避免此问题。

8.2 条件判断

# 根据平台选择不同选项
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            # 发布版本

8.3 函数与字符串处理

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))

8.4 静态模式规则

OBJS = main.o utils.o network.o

# 只对这些特定文件应用规则
$(OBJS): %.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

8.5 多目标规则

# 同一规则生成多个目标
all: libfoo.so libfoo.a

libfoo.so: foo.o
	$(CC) -shared -o $@ $^

libfoo.a: foo.o
	$(AR) rcs $@ $^

8.6 自动创建目录

# 在编译前确保目录存在
OBJDIR = build
$(shell mkdir -p $(OBJDIR))

$(OBJDIR)/%.o: %.c
	$(CC) -c $< -o $@

8.7 打印调试信息

# 打印变量值,排查 Makefile 问题
all:
	@echo "SRCS = $(SRCS)"
	@echo "OBJS = $(OBJS)"
	@echo "CC = $(CC)"

九、完整项目示例

9.1 嵌入式 LED 控制项目

# ============================================
# 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

十、注意事项

  1. Tab 缩进是强制的:命令行前必须用 Tab,不能用空格(最常见的错误)
  2. @ 前缀@echo 不打印命令本身,只打印输出
  3. - 前缀-rm 表示忽略错误继续执行
  4. $ 要转义:在 Shell 命令中用 $$ 表示 Shell 变量,如 $$HOME
  5. 并行编译make -j4 开启 4 个并行任务,大幅加速编译
  6. make 默认执行第一个目标:将 all 放在最前面
  7. 依赖自动生成:用 -MMD -MP 避免手动维护头文件依赖

十一、面试要点

Q1: Make 的核心机制是什么?

A: 基于时间戳的增量编译。make 比较目标文件和依赖文件的修改时间,如果依赖更新则重建目标。只重建需要更新的部分,避免全量编译。

Q2: =:= 的区别?

A: = 延迟展开(使用时求值),:= 立即展开(定义时求值)。OBJS = $(SRC:.c=.o)= 时,SRC 后续修改会影响 OBJS;用 := 则不会。

Q3: 什么是自动变量?$@$<$^ 分别是什么?

A: 自动变量在规则执行时自动赋值:$@ 是当前目标,$< 是第一个依赖,$^ 是所有依赖(去重)。用自动变量可以写出通用的模式规则。

Q4: 如何实现多目录编译?

A:$(wildcard src/**/*.c)$(shell find src -name '*.c') 递归查找源文件,用模式规则 %.o: %.c 配合 -o $@ 输出到对应目录,用 -MMD -MP 自动生成头文件依赖。

Q5: -MMD -MP 的作用?

A: -MMD 在编译时自动生成 .d 依赖文件(记录目标依赖哪些头文件),-MP 生成空规则防止头文件被删除后 make 报错。通过 -include $(OBJS:.o=.d) 引入,实现头文件变更时自动重新编译。

Q6: 为什么命令前必须用 Tab?

A: 这是 make 的语法约定。make 用 Tab 来区分规则定义和命令。如果用空格缩进,make 会将其误认为规则的一部分而报语法错误。

Q7: 交叉编译 Makefile 需要注意什么?

A: 1)设置正确的 CROSS_COMPILE 前缀;2)使用 -static 避免目标设备缺动态库;3)交叉编译的工具链路径需正确;4)可能需要指定 --sysroot 指向目标系统的根文件系统。

Q8: 如何调试 Makefile?

A: 1)make -n(dry-run)打印要执行的命令但不执行;2)make -p 打印完整规则数据库;3)在规则中加 @echo 打印变量值;4)make --debug=b 显示决策原因。