3. Makefile基础:Makefile的作用、基本规则、变量与自动变量
大家好,我是你们的嵌入式工具链讲师。今天我们来聊聊Makefile。
说实话,我见过太多工程师在项目里手动编译、手动链接,改一个文件就要敲一长串命令。嗯,我刚开始做嵌入式那会儿也这样,直到有一次项目文件多了,漏编译了一个模块,排查了整整一个下午……从那以后,我再也不敢不用Makefile了。
3.1 Makefile到底有什么用?
简单说,Makefile就是一份「自动化编译说明书」。你告诉它:源文件有哪些、依赖什么头文件、用什么编译器、最终生成什么目标。然后一个make命令,全部搞定。
它的核心价值有三点:
- 自动化:一条命令完成编译、链接、清理等所有操作
- 增量编译:只重新编译修改过的文件,节省大量时间
- 可维护:编译规则写在文件里,换环境、换人接手都能快速上手
重要概念:Makefile的核心思想是「如果依赖比目标新,就重新生成目标」。说白了,它只做必要的事,不做重复的事。
3.2 Makefile的基本规则
一个Makefile规则长什么样?其实就三部分:
目标: 依赖
命令
举个例子:
main.o: main.c
gcc -c main.c -o main.o
意思是:要生成main.o,需要main.c。如果main.c比main.o新,就执行下面的编译命令。
注意,命令前面必须是一个Tab键,不是空格。我见过不少新手在这里栽跟头——明明规则写对了,就是报错,一看,空格代替了Tab。嗯,这个坑我当年也踩过。
一个完整的Makefile通常包含多个规则:
final.bin: main.o uart.o i2c.o
gcc main.o uart.o i2c.o -o final.bin
main.o: main.c main.h
gcc -c main.c -o main.o
uart.o: uart.c uart.h
gcc -c uart.c -o uart.o
i2c.o: i2c.c i2c.h
gcc -c i2c.c -o i2c.o
clean:
rm -f *.o final.bin
你想想看,如果每次改一个文件都要手动敲这些命令,是不是很崩溃?有了Makefile,一个make就全搞定了。
3.3 变量与自动变量
上面的例子有个问题:重复写了太多gcc、-c、-o。如果我想换编译器,比如从gcc换成arm-none-eabi-gcc,难道要一个个改?
这时候变量就派上用场了。
3.3.1 自定义变量
CC = gcc
CFLAGS = -Wall -O2
TARGET = final.bin
$(TARGET): main.o uart.o i2c.o
$(CC) main.o uart.o i2c.o -o $(TARGET)
main.o: main.c main.h
$(CC) $(CFLAGS) -c main.c -o main.o
uart.o: uart.c uart.h
$(CC) $(CFLAGS) -c uart.c -o uart.o
i2c.o: i2c.c i2c.h
$(CC) $(CFLAGS) -c i2c.c -o i2c.o
clean:
rm -f *.o $(TARGET)
变量用$(变量名)引用。这样换编译器时,只需要改第一行的CC就行。我个人习惯把所有可能变动的参数都提取成变量,比如编译器、编译选项、链接选项、输出目录等。这样项目移植到新平台时,改起来特别快。
3.3.2 自动变量
自动变量是Makefile内置的「快捷方式」。它们能自动获取当前规则中的目标、依赖等信息。常用的有这几个:
| 自动变量 | 含义 | 示例值(上面规则中) |
|---|---|---|
$@ |
当前规则的目标 | main.o |
$< |
第一个依赖文件 | main.c |
$^ |
所有依赖文件(去重) | main.c main.h |
$? |
比目标新的依赖文件 | (视情况而定) |
用自动变量改写上面的例子:
CC = gcc
CFLAGS = -Wall -O2
TARGET = final.bin
$(TARGET): main.o uart.o i2c.o
$(CC) $^ -o $@
main.o: main.c main.h
$(CC) $(CFLAGS) -c $< -o $@
uart.o: uart.c uart.h
$(CC) $(CFLAGS) -c $< -o $@
i2c.o: i2c.c i2c.h
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o $(TARGET)
你看,$@自动变成目标文件名,$<自动变成第一个依赖。代码简洁多了,而且不容易写错。
我的小技巧:在调试Makefile时,可以用$(info 变量名=$@)打印自动变量的值。我曾经用这招排查过一个诡异的编译顺序问题,省了不少时间。
3.4 模式规则——让Makefile更优雅
上面还是有点啰嗦,三个.o文件的规则几乎一模一样。能不能合并?当然可以,用模式规则:
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
%是通配符,匹配任意文件名。这条规则的意思是:任何.o文件,如果它对应的.c和.h有更新,就重新编译。
完整的Makefile变成:
CC = gcc
CFLAGS = -Wall -O2
TARGET = final.bin
OBJS = main.o uart.o i2c.o
$(TARGET): $(OBJS)
$(CC) $^ -o $@
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f *.o $(TARGET)
是不是清爽多了?以后加一个新模块,只需要在OBJS里加一个.o文件,编译规则自动生效。
注意:模式规则中的%.h依赖可能会让Makefile认为每个.c文件都必须有对应的.h文件。如果某些.c没有头文件,可以单独写规则覆盖,或者去掉%.h依赖,只在具体规则中声明头文件依赖。
3.5 伪目标——别和文件名搞混了
你有没有想过,如果当前目录下恰好有一个叫clean的文件,那make clean会怎样?它会认为clean已经是最新的,什么都不做。
为了避免这种尴尬,我们用.PHONY声明伪目标:
.PHONY: clean
clean:
rm -f *.o $(TARGET)
伪目标不检查文件时间戳,每次都执行。我建议把所有不生成文件的目标都声明为.PHONY,比如clean、all、install等。
3.6 一个完整的嵌入式Makefile示例
最后,给你看一个我在STM32项目里用过的Makefile框架:
# 工具链
CC = arm-none-eabi-gcc
LD = arm-none-eabi-ld
OBJCOPY = arm-none-eabi-objcopy
# 编译选项
CFLAGS = -mcpu=cortex-m4 -mthumb -Wall -O2
LDFLAGS = -T stm32f4.ld
# 文件
TARGET = firmware.elf
BIN = firmware.bin
SRCS = main.c uart.c i2c.c spi.c
OBJS = $(SRCS:.c=.o)
# 默认目标
all: $(BIN)
# 链接
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
# 生成bin文件
$(BIN): $(TARGET)
$(OBJCOPY) -O binary $< $@
# 编译
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET) $(BIN)
这个框架我用了好几年,从Cortex-M0到M7都适用。你只需要改芯片型号、源文件列表和链接脚本就行。
好了,这一章的内容就到这里。Makefile的变量和自动变量是基础中的基础,但用好它们,你的编译效率能提升一个档次。下一章我们聊聊Makefile的条件判断和函数,那才是真正让Makefile「活起来」的东西。