3. Makefile 入门:从“手动敲命令”到“一键编译”
说实话,我刚入行那会儿,最烦的就是编译。每次改一行代码,都要敲一长串 gcc 命令,还得记着链接哪个库、输出什么文件。后来老工程师甩给我一个 Makefile,说“你以后就敲个 make 就行”。我一看,好家伙,这玩意儿比女朋友还难懂。
但用顺手之后,我再也回不去了。Makefile 说白了就是一套“自动化编译脚本”,你告诉它规则,它帮你干活。今天我就带你把它啃下来。
3.1 什么是 Makefile?
Makefile 是 make 工具的配置文件。make 会读取这个文件,判断哪些文件需要重新编译,然后执行对应的命令。
它的核心逻辑就三个词:目标、依赖、命令。
核心公式:
目标文件 : 依赖文件列表
→ 执行命令(Tab 开头)
举个例子。你有个 led.c 和 main.c,想编译成 led.o 和 main.o,最后链接成 firmware.elf。手动敲命令很累,但写成 Makefile 就清爽了。
3.2 基本语法:目标、依赖、命令
看个最简单的例子:
firmware.elf: main.o led.o
arm-none-eabi-gcc -o firmware.elf main.o led.o
main.o: main.c
arm-none-eabi-gcc -c main.c -o main.o
led.o: led.c
arm-none-eabi-gcc -c led.c -o led.o
这里 firmware.elf 是最终目标,它依赖 main.o 和 led.o。如果这两个 .o 文件比 .elf 新,make 就会执行下面的链接命令。
嗯,这里有个坑:命令前面必须是 Tab 键,不能用空格。我见过有人因为这个错误卡了一下午,死活编译不过。
避坑指南:Makefile 里的命令缩进必须是 Tab,不是 4 个空格。如果你用编辑器自动把 Tab 转成空格了,make 会报“missing separator”错误。我曾经在 CI 环境里被这个坑过,排查了半小时才发现是编辑器设置的问题。
3.3 伪目标:别让文件名坑了你
你想想看,如果目录下恰好有一个叫 clean 的文件,那 make clean 会怎样?make 会认为这个目标已经是最新的了,什么都不执行。
解决办法就是声明它为伪目标:
.PHONY: clean
clean:
rm -f *.o firmware.elf
伪目标不检查文件是否存在,每次都执行命令。我个人习惯把所有非文件目标都加上 .PHONY,比如 all、clean、flash、debug。
3.4 变量:别写死,用变量
写死的 Makefile 很难维护。换个编译器、改个源文件路径,你得改好几处。用变量就灵活多了:
CC = arm-none-eabi-gcc
CFLAGS = -Wall -O2 -mcpu=cortex-m4
TARGET = firmware.elf
SRCS = main.c led.c uart.c
OBJS = $(SRCS:.c=.o)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)
这里我用了几个自动变量:
| 变量 | 含义 | 例子 |
|---|---|---|
$@ |
目标文件名 | firmware.elf |
$^ |
所有依赖文件 | main.o led.o uart.o |
$< |
第一个依赖文件 | main.c |
我的习惯:变量名用大写,赋值用 = 或 :=。= 是递归展开,:= 是简单展开。我一般用 :=,避免变量引用顺序出问题。
3.5 常用函数:让 Makefile 更聪明
Makefile 里也有函数,虽然语法有点怪,但很实用。
# 获取 src 目录下所有 .c 文件
SRCS := $(wildcard src/*.c)
# 把 .c 替换成 .o,并放到 obj 目录
OBJS := $(patsubst src/%.c, obj/%.o, $(SRCS))
# 添加头文件搜索路径
CFLAGS += -I$(shell pwd)/inc
# 条件判断:如果是调试版,加 -g
ifeq ($(DEBUG), 1)
CFLAGS += -g -O0
endif
常用的就这几个:wildcard 找文件,patsubst 替换路径,shell 执行 shell 命令。够用了。
3.6 完整嵌入式 Makefile 示例
下面是我给 STM32F4 项目写的一个模板,你可以直接拿来改:
# 工具链
CC := arm-none-eabi-gcc
LD := arm-none-eabi-gcc
OBJCOPY := arm-none-eabi-objcopy
SIZE := arm-none-eabi-size
# 目标
TARGET := firmware
MCU := cortex-m4
# 路径
SRC_DIR := src
INC_DIR := inc
BUILD_DIR := build
# 源文件
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
# 编译参数
CFLAGS := -mcpu=$(MCU) -mthumb -Wall -O2 -ffunction-sections -fdata-sections
CFLAGS += -I$(INC_DIR)
LDFLAGS := -mcpu=$(MCU) -mthumb -Wl,--gc-sections -Wl,-Map=$(BUILD_DIR)/$(TARGET).map
# 默认目标
all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).bin
# 编译
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
# 链接
$(BUILD_DIR)/$(TARGET).elf: $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
$(SIZE) $@
# 生成 bin 文件
$(BUILD_DIR)/$(TARGET).bin: $(BUILD_DIR)/$(TARGET).elf
$(OBJCOPY) -O binary $< $@
# 创建 build 目录
$(BUILD_DIR):
mkdir -p $@
# 烧录(以 ST-Link 为例)
flash: $(BUILD_DIR)/$(TARGET).bin
st-flash write $< 0x08000000
# 清理
.PHONY: all clean flash
clean:
rm -rf $(BUILD_DIR)
这个 Makefile 做了几件事:自动扫描源文件、编译到 build 目录、生成 .elf 和 .bin、支持一键烧录。你只需要改 MCU 和路径就行。
关键点:
| $(BUILD_DIR)是“仅当目录不存在时才创建”,属于 order-only 依赖。-ffunction-sections -fdata-sections配合--gc-sections可以去掉未使用的函数和数据,减小固件体积。$(SIZE) $@会在链接后打印各段大小,方便你监控固件膨胀。
3.7 Makefile 知识体系
下面这张图帮你理清 Makefile 的核心脉络:
3.8 避坑总结
最后,我把这些年踩过的坑列出来,你遇到了直接对照:
| 问题 | 现象 | 解决办法 |
|---|---|---|
| Tab 用了空格 | missing separator | 编辑器设置 Tab 为实际 Tab,不要转空格 |
| 伪目标没加 .PHONY | 同名文件存在时 make 不执行 | 所有非文件目标都加 .PHONY |
| 变量赋值用了 = | 变量引用顺序导致意外值 | 用 := 简单赋值 |
| 路径写死 | 换目录就得改 Makefile | 用 wildcard 和 patsubst 动态生成 |
| 没加 -j 参数 | 单核编译,速度慢 | make -j$(nproc) 并行编译 |
Makefile 这东西,说白了就是个“偷懒工具”。你花半小时写好它,以后每次编译就省下 5 分钟。时间长了,这笔账怎么算都划算。