20251127 - 韦东山Linux - 通用Makefile解析

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

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


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

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

  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 定义所有工具的完整名称(如 CCgcc)。
  • 变量导出(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)/include

    LDFLAGS :=

    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

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

  1. 变量初始化与包含
makefile 复制代码
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)

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

总结工作流

  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

相关推荐
可爱又迷人的反派角色“yang”2 小时前
docker基本概念(一)
linux·运维·docker·容器
西瓜和拾月2 小时前
Ubuntu Server 24.04 LVM 分区扩容
linux·运维·ubuntu
RisunJan2 小时前
Linux命令-help命令(查看 Shell 内置命令帮助信息)
linux·运维·服务器
大猫和小黄2 小时前
Windows环境下在VMware中安装和配置CentOS 7
linux·windows·centos
梁正雄2 小时前
linux服务-Kibana8原理与安装
linux·运维·服务器
奋斗的好青年2 小时前
Ubuntu+Windows双系统修复引导+更改启动顺序
linux·windows·ubuntu
yiSty3 小时前
Windows 10/11下安装WSL Ubuntu
linux·windows·ubuntu
LYFlied3 小时前
规范驱动开发(SDD)主流工具与框架深度解析
驱动开发·ai编程·sdd
编程研究坊3 小时前
LabelStudio linux 系统下部署教程
linux·运维·服务器