韦东山Linux - 通用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)
	
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) 彻底分离,并实现了递归编译自动依赖追踪

下面将对 MakefileMakefile.build 进行详细的逐行解析。


📄 一、顶层 Makefile (配置与入口)

这个文件位于项目的顶层,负责定义编译工具、全局选项、目标列表,并作为启动递归构建的入口 1。

  1. 工具链变量定义
CROSS_COMPILE = 
AS		= $(CROSS_COMPILE)as
LD		= $(CROSS_COMPILE)ld
# ... (其他工具变量如 CC, CPP, AR, NM, STRIP, OBJCOPY, OBJDUMP)
  • 定义工具链前缀 CROSS_COMPILE。如果为空,则使用本地工具(如 gcc, as)。
  • 基于 CROSS_COMPILE 定义所有工具的完整名称(如 CCgcc)。
  • 变量导出(Export)
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
  • export:将这些变量(如 CC)导出到 Shell 环境。这样,在 Makefile 规则中执行的任何命令(如 gcc)都能访问这些变量的值,确保所有子目录和递归调用都使用相同的工具链。
  • 编译与链接选项
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include

LDFLAGS := 

export CFLAGS LDFLAGS
  • CFLAGS:定义全局 C 编译选项。
    • := -Wall -O2 -g:使用即时变量定义 -Wall(所有警告)、-O2(优化级别)、-g(生成调试信息)。
    • += -I $(shell pwd)/include:添加一个搜索头文件的路径,即项目根目录下的 include 文件夹。
  • LDFLAGS:定义链接选项(当前为空)。
  • export:将全局选项导出,确保在所有递归子目录中,编译和链接都使用这些统一的选项。
  • 路径与目标定义
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/:需要递归进入的子目录。
  • 核心规则与入口
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 可执行文件。
  • 清理目标
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

这个文件包含了所有通用的编译逻辑、递归规则和文件聚合机制,它不包含任何特定于项目的配置

  1. 变量初始化与包含
PHONY := __build
__build:

obj-y :=
subdir-y :=
EXTRA_CFLAGS :=

include Makefile
  • PHONY__build__buildMakefile.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)
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 文件需要重编译。

    递归与聚合规则

__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 $@ $<:正常的编译命令。

总结工作流

  1. 顶层 Makefile 启动 make -C ./ -f Makefile.build

  2. Makefile.build 读取 Makefile 中的配置 (obj-y = main.o sub.o a/)

  3. Makefile.build 编译 main.o, sub.o (通过 %.o : %.c 规则)

  4. Makefile.build 发现 a/,执行 递归调用 make -C a/ -f Makefile.build

  5. 子目录重复步骤 2-4,并最终生成 a/built-in.o.

  6. 回到顶层,Makefile.buildmain.o, sub.o, 和 a/built-in.o 打包成顶层的 built-in.o

  7. 顶层 Makefile 将顶层的 built-in.o 链接成最终的可执行文件 test

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐