韦东山Linux - 通用Makefile解析
makefile
# Makefile文件
CROSS_COMPILE =
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include
LDFLAGS :=
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test
obj-y += main.o
obj-y += sub.o
obj-y += a/
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build
$(TARGET) : start_recursive_build
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
makefile
PHONY := __build
__build:
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
include Makefile
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
# c/built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
PHONY += $(subdir-y)
__build : $(subdir-y) built-in.o
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o : $(subdir-y) $(cur_objs)
$(LD) -r -o $@ $(cur_objs) $(subdir_objs)
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
这是一个经典的 Kbuild 风格(Linux 内核构建系统)的 Makefile 架构。这个系统将项目的配置信息 (Makefile) 与通用构建规则 (Makefile.build) 彻底分离,并实现了递归编译 和自动依赖追踪。
下面将对 Makefile 和 Makefile.build 进行详细的逐行解析。
📄 一、顶层 Makefile (配置与入口)
这个文件位于项目的顶层,负责定义编译工具、全局选项、目标列表,并作为启动递归构建的入口 1。
- 工具链变量定义
makefile
CROSS_COMPILE =
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
# ... (其他工具变量如 CC, CPP, AR, NM, STRIP, OBJCOPY, OBJDUMP)
- 定义工具链前缀
CROSS_COMPILE。如果为空,则使用本地工具(如gcc,as)。 - 基于
CROSS_COMPILE定义所有工具的完整名称(如CC即gcc)。 - 变量导出(Export)
makefile
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
-
export:将这些变量(如CC)导出到 Shell 环境 。这样,在Makefile规则中执行的任何命令(如gcc)都能访问这些变量的值,确保所有子目录和递归调用都使用相同的工具链。 -
编译与链接选项
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/includeLDFLAGS :=
export CFLAGS LDFLAGS
-
CFLAGS:定义全局 C 编译选项。:= -Wall -O2 -g:使用即时变量 定义-Wall(所有警告)、-O2(优化级别)、-g(生成调试信息)。+= -I $(shell pwd)/include:添加一个搜索头文件的路径,即项目根目录下的include文件夹。
-
LDFLAGS:定义链接选项(当前为空)。 -
export:将全局选项导出,确保在所有递归子目录中,编译和链接都使用这些统一的选项。 -
路径与目标定义
makefile
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test
obj-y += main.o
obj-y += sub.o
obj-y += a/
TOPDIR:定义项目的顶层目录 的绝对路径,并导出。这对于子目录调用Makefile.build时查找文件路径至关重要。TARGET:定义最终可执行文件名为test。obj-y:定义了本层目录的构建配置:main.o,sub.o:需要编译的文件。a/:需要递归进入的子目录。
- 核心规则与入口
makefile
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build
$(TARGET) : start_recursive_build
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
all:默认目标,依赖于start_recursive_build和$(TARGET)。start_recursive_build:递归构建的入口 。make -C ./ -f $(TOPDIR)/Makefile.build:启动一个新的make进程。它告诉make切换到当前目录 (./),并使用位于$(TOPDIR)的Makefile.build作为规则文件。这将触发Makefile.build开始执行本层和子目录的编译。
$(TARGET)(test) :最终的链接目标。- 依赖于
start_recursive_build(确保所有.o文件都已编译,并被打包到built-in.o中)。 - 命令:将
built-in.o(由Makefile.build聚合而成) 链接成最终的test可执行文件。
- 依赖于
- 清理目标
makefile
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
clean:删除所有.o文件和最终目标test。distclean:更彻底的清理,还删除了自动生成的依赖文件 (.d文件)。
📄 二、通用规则文件 Makefile.build
这个文件包含了所有通用的编译逻辑、递归规则和文件聚合机制,它不包含任何特定于项目的配置
- 变量初始化与包含
makefile
PHONY := __build
__build:
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
include Makefile
-
PHONY与__build:__build是Makefile.build中的核心目标 ,它代表"构建当前目录的所有内容",被声明为伪目标 (PHONY)。 -
变量清空 :
obj-y,subdir-y,EXTRA_CFLAGS被清空,以确保它们仅包含当前目录 (Makefile) 中定义的值。 -
include Makefile:关键步骤 。此时make停止,转而读取当前目录 下的Makefile(即顶层Makefile或子目录下的Makefile)。这个include会导入obj-y等配置变量。- 例如,在顶层运行时,
obj-y变为main.o sub.o a/。
- 例如,在顶层运行时,
-
目录和文件分离
__subdir-y := (patsubst %/,%,(filter %/, (obj-y))) subdir-y += (__subdir-y)
subdir_objs := (foreach f,(subdir-y),$(f)/built-in.o)
cur_objs := (filter-out %/, (obj-y))
-
__subdir-y:将obj-y中所有以/结尾的项(如a/)筛选出来 (filter %/, ...),并去除斜杠 (patsubst %/,%, ...),得到子目录名列表 (a)。 -
subdir-y:存储需要递归的子目录列表 (a)。 -
subdir_objs:生成子目录构建完成后的聚合目标文件列表(a/built-in.o)。 -
cur_objs:将obj-y中不 以/结尾的项筛选出来,得到本层需要编译的.o文件列表(main.o sub.o)。 -
自动依赖追踪 (Auto-Dependency)
makefile
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
-
生成依赖文件名的理论列表 (
.main.o.d,.sub.o.d)。 -
使用
wildcard查找实际存在的依赖文件。 -
如果找到任何
.d文件 (ifneq), 则将其include进来,实现了自动依赖追踪:一旦头文件有改动,make就能通过.d文件中的规则知道哪些.o文件需要重编译。递归与聚合规则
makefile
__build : $(subdir-y) built-in.o
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o : $(subdir-y) $(cur_objs)
$(LD) -r -o $@ $(cur_objs) $(subdir_objs)
__build:核心目标,它依赖于所有子目录的完成 ($(subdir-y)) 和本层聚合文件 (built-in.o)。$(subdir-y)(a): 递归规则 。对于每个子目录a:make -C $@ -f $(TOPDIR)/Makefile.build:递归调用make。切换到子目录 (-C $@,即a/),并要求子目录再次 使用Makefile.build进行构建。
built-in.o:聚合规则 。- 依赖于所有子目录的聚合文件 (
$(subdir-y)) 和本层.o文件 ($(cur_objs))。 - 命令:
$(LD) -r -o $@ ...:使用链接器 (LD) 的-r选项(可重定位输出),将本层所有编译出的.o文件 ($(cur_objs)) 以及所有子目录的聚合文件 ($(subdir_objs)) 打包 成一个大的聚合文件built-in.o。
- 依赖于所有子目录的聚合文件 (
- 编译模式规则
Makefile
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
dep_file:定义了当前目标.o对应的.d文件名 (.$@.d)。%.o : %.c:通用的编译规则。- 调用
gcc($(CC))。 - 使用全局 (
$(CFLAGS)), 额外的 ($(EXTRA_CFLAGS)), 局部 ($(CFLAGS_$@)) 选项。 -Wp,-MD,$(dep_file):这是 GCC 产生依赖的另一种写法。-MD标志生成依赖,并将其写入$(dep_file)中。-c -o $@ $<:正常的编译命令。
- 调用
总结工作流
-
顶层
Makefile启动make -C ./ -f Makefile.build -
Makefile.build读取Makefile中的配置 (obj-y = main.o sub.o a/) -
Makefile.build编译main.o,sub.o(通过%.o : %.c规则) -
Makefile.build发现a/,执行 递归调用make -C a/ -f Makefile.build -
子目录重复步骤 2-4,并最终生成
a/built-in.o. -
回到顶层,
Makefile.build将main.o,sub.o, 和a/built-in.o打包 成顶层的built-in.o -
顶层
Makefile将顶层的built-in.o链接 成最终的可执行文件test