Makefile从零写起:每条规则在干什么

先说背景。手上有个 imx6ull 的板子,之前一直用 Buildroot 一键出镜像,日子过得挺舒服。最近接了个小活,要在一个裸的交叉编译环境里单独编几个驱动模块和一个测试程序,人家给了个 SDK,里面 Makefile 一大堆,我打开一看,满屏的 $(CC)、$@、$<、:=、?=,头皮发麻。

说白了,我以前的水平就是"抄一个能跑的 Makefile,改改文件名,能编过就不管了"。这回跑不动了,只能硬着头皮从头捋。下面就是我这几天的折腾记录,写给和我一样"只会抄不会写"的朋友。

一、先搞清楚 Makefile 到底在干嘛

一句话:Makefile 就是告诉 make 这个工具"什么文件依赖什么文件,依赖变了就重新生成"。

类比一下,你做饭。菜谱上写"番茄炒蛋需要番茄、鸡蛋,番茄切好之后才能下锅"。哪天番茄换了新的,那就得重炒一遍;番茄没变,鸡蛋也没变,那这盘菜就不用动。make 干的就是这个"看谁变了就重做哪一步"的活。

涉及的知识点,先摆几个最基本的:

1、规则的基本样子

makefile 复制代码
目标: 依赖
	命令

注意命令前面必须是 Tab,不能是空格。这一条坑了无数人,包括我。

2、三个自动变量

  • $@:目标文件名
  • $<:第一个依赖文件名
  • $^:所有依赖文件

3、四种赋值

makefile 复制代码
A = 1        # 递归展开,用到时才展开
B := 1       # 立即展开,定义时就定死
C ?= 1       # 没定义过才赋值
D += 1       # 追加

4、伪目标 .PHONY,防止目录里正好有个同名文件导致规则不执行。

这些书上都有,我原来也"看过",但没真用过。下面上真实过程。

二、解决过程

1、先写个最小的,跑通再说

我在 ~/work/mk_demo 下建了三个文件:main.c、foo.c、foo.h。

先写一个最土的 Makefile:

makefile 复制代码
hello: main.o foo.o
	gcc -o hello main.o foo.o

main.o: main.c foo.h
	gcc -c main.c -o main.o

foo.o: foo.c foo.h
	gcc -c foo.c -o foo.o

敲 make,输出:

css 复制代码
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o hello main.o foo.o

跑起来了。然后我 touch foo.h 再 make,两个 .o 都重新编了,说明依赖写对了。这一步让我第一次真切感觉到"依赖关系"不是摆设。

2、换成变量和自动变量,把它写"像样"一点

makefile 复制代码
CC = gcc
CFLAGS = -Wall -O2
TARGET = hello
OBJS = main.o foo.o

$(TARGET): $(OBJS)
	$(CC) -o $@ $^

%.o: %.c foo.h
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(OBJS) $(TARGET)

这里 %.o: %.c 是模式规则,$@ 是目标,$< 是第一个依赖,$^ 是全部依赖。make clean 也能用了。

到这一步我还挺得意,觉得 Makefile 不过如此。

3、换成交叉编译,翻车开始

SDK 里要用 arm-linux-gnueabihf-gcc,我就把 CC = gcc 改成 CC = arm-linux-gnueabihf-gcc,然后 make。

报错:

go 复制代码
make: arm-linux-gnueabihf-gcc: 命令未找到

哦,交叉工具链没加到 PATH。export PATH=$PATH:/opt/gcc-arm-linux-gnueabihf/bin,再 make,编过了。file hello 一看是 ARM 的,没问题。

然后我想试试 make CC=gcc 覆盖一下,编个本机版本调试用。结果发现没生效,还是交叉编译器。

这里就是我第一个真正的弯路。原因是 CC = gcc 这种写法,在命令行上传 CC=gcc 其实是能覆盖的......我试了半天还是不行呀,折腾了好久才反应过来:我那个 Makefile 里写的是 CC := arm-linux-gnueabihf-gcc,而命令行变量本来就能覆盖任何赋值方式------问题不在这。

真正的问题是:我后来又在文件末尾加了一行 CC = gcc,把前面的覆盖了。make 里同名变量后面定义的会盖掉前面的。删掉那行,make CC=gcc 立刻生效。

这个错误特别蠢,但我确实卡了大半天。教训是:Makefile 里同一个变量别到处赋值,尤其别在末尾随手补一行。

4、头文件依赖,又栽一次

代码里 foo.c 也 include 了 foo.h。我改 foo.h 里的一个宏,make 之后发现 foo.o 没重编,只有 main.o 重编了。

原因很简单:我的模式规则写的是 %.o: %.c foo.h,这条规则只对 main.o 和 foo.o 都成立------等等,其实它对两个都成立啊。问题在哪?

问题在 foo.c 里 include 的头文件不止 foo.h,还有一个 config.h,而我的规则里根本没写 config.h。make 不知道 foo.o 依赖 config.h,自然不重编。

手动把所有头文件列进依赖,文件一多就崩溃。正确做法是用 -MMD -MP 让编译器自动生成依赖文件:

makefile 复制代码
CFLAGS += -MMD -MP
DEPS = $(OBJS:.o=.d)
-include $(DEPS)

-MMD 生成 .d 文件记录头文件依赖,-include 把 .d 文件包含进来。这样改任何头文件都能正确触发重编。这一招是 SDK 里学来的,之前完全不知道。

5、清理和目录

最后我把源码挪进 src/,输出放 build/,规则改成:

makefile 复制代码
SRCS = $(wildcard src/*.c)
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))

wildcard 取文件列表,patsubst 做路径替换。这里要注意 build/ 目录得先存在,否则 gcc -o build/main.o 会报"没有那个目录"。加一条:

makefile 复制代码
$(OBJS): | build
build:
	mkdir -p build

那个 | 是 order-only 依赖,表示"build 目录得先有,但它变了不用重编目标"。这个语法我原来见都没见过。

三、结果和遗留

现在这个 Makefile 能用了:改源码、改头文件都能正确重编,make clean 干净利落,交叉和本机编译靠命令行传 CC 切换。

遗留问题也有:还没做多目录递归,SDK 里那种 make -C subdir 的写法我还没完全吃透;另外 $(shell ...) 和条件判断 ifeq 我只看了没敢用,怕再翻车。

参考资料主要是 GNU make 官方手册,还有 SDK 里那份别人写的 Makefile,我一行一行对着猜的。感谢写那份 Makefile 的前辈,虽然不认识,但真是无私奉献。

我这水平也就刚入门,上面要是有说错的地方,欢迎留言指正,别客气。

相关推荐
pipixia200333 小时前
C++继承面试四连问:内存叠加、私有继承、sizeof、权限重映射
c++·面试·嵌入式·继承·面向对象·对象模型
shimly12345610 小时前
(undone) 野火嵌入式 (1) 打开例程工程
嵌入式
Zwawa1 天前
一只按键不只“按下”:短按、长按和连按如何共存
c语言·单片机·嵌入式
Zevalin爱灰灰1 天前
Linux入门第一部分(Ubuntu入门篇) 第三章 shell脚本入门(一)
linux·ubuntu·嵌入式
2035去旅行1 天前
U-Boot DM(Driver Model)驱动框架
嵌入式·arm·soc
硅基造物主1 天前
STM32 从 0 到 1 做智能家居(一):先想清楚要做什么
stm32·mqtt·嵌入式·智能家居·hal库·esp-12f
凉、介1 天前
深入理解 ARMv7-M | 异常/中断处理
笔记·学习·嵌入式·arm
凉、介1 天前
深入理解 ARMv7-M|处理器模式与寄存器
笔记·学习·嵌入式·arm
LCG米2 天前
STM32 DMA 多通道链式传输实测:请求仲裁顺序错位根因、半满中断乒乓与高速并发搬运落地
stm32·单片机·嵌入式·1024程序员节