Makefile文件编写的学习记录(以IMX6ULL开发板的Makefile文件和Makefile.build文件来进行学习)

发布于:2024-12-20 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

前言

本文通过嵌入式应用实例→电子产品量产工具→显示系统的Makefile文件来学习Makefile的相关知识。

在我们基于IMX6ULL开发板的嵌入式开发中,我们书写的Makefile文件主要是为了驱动交叉编译工具arm-buildroot-linux-gnueabihf-gcc来编译生成我们需要的程序。
关于交叉编译工具arm-buildroot-linux-gnueabihf-gcc的安装及解释见下面两篇博文:
https://blog.csdn.net/wenhao_ir/article/details/144326545
https://blog.csdn.net/wenhao_ir/article/details/144329543

在阅读以下内容前强烈推荐先仔细读一遍我写的另一篇博文:
读懂和编写Makefile需掌握的关键基础知识
读懂和编写Makefile需掌握的关键基础知识
读懂和编写Makefile需掌握的关键基础知识

源码下载

这个例子的压缩包下载地址:
https://pan.baidu.com/s/1akuFrooBUPibgtPgPS2Xzw?pwd=jcat

顶层Makefile文件:我命名为Makefile_A

嵌入式应用实例→电子产品量产工具→显示系统一文中对应的顶层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 += display/
obj-y += unittest/

all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build

$(TARGET) : built-in.o
	$(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)
	

内容并不长,我们一行一行的来阅读。

CROSS_COMPILE ?=

这里关键是要理解?= 是什么?

  • ?=条件赋值运算符,表示 “如果变量未定义或为空时,才赋予它一个默认值”
  • 如果 CROSS_COMPILE 在外部环境中已经被定义(例如通过命令行传入,或在外部环境变量中存在),那么它不会被这一行重新赋值。
  • 如果 CROSS_COMPILE 未被定义,这里将其设为空字符串。

在博文https://blog.csdn.net/wenhao_ir/article/details/144326545中我们已经定义了CROSS_COMPILE 的值,相关语句如下:

export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-

所以如果我们已经在Ubuntu配置了CROSS_COMPILE,那么其实这里我们不需要设置,当然为了保险起见,我们可以把这句改为:

CROSS_COMPILE ?= arm-buildroot-linux-gnueabihf-

AS、LD、CC、CPP、AR、NM进行定义的语句

AS		= $(CROSS_COMPILE)as
LD		= $(CROSS_COMPILE)ld
CC		= $(CROSS_COMPILE)gcc
CPP		= $(CC) -E
AR		= $(CROSS_COMPILE)ar
NM		= $(CROSS_COMPILE)nm

根据前文叙述,CROSS_COMPILE的值为arm-buildroot-linux-gnueabihf-,那么上面的代码等效为:

AS		= arm-buildroot-linux-gnueabihf-as
LD		= arm-buildroot-linux-gnueabihf-ld
CC		= arm-buildroot-linux-gnueabihf-gcc
CPP		= $(CC) -E
AR		= arm-buildroot-linux-gnueabihf-ar
NM		= arm-buildroot-linux-gnueabihf-nm

CPP的情况我们等会儿再说,我们先看下能展开的五个命令各自的作用。

我把CROSS_COMPILE的值arm-buildroot-linux-gnueabihf-代入AS、LD、CC、AR、NM中,得到了下面的命令:

AS		= arm-buildroot-linux-gnueabihf-as
LD		= arm-buildroot-linux-gnueabihf-ld
CC		= arm-buildroot-linux-gnueabihf-gcc
AR		= arm-buildroot-linux-gnueabihf-ar
NM		= arm-buildroot-linux-gnueabihf-nm

请分别说下这些命令各自的作用。

下面是你提到的工具的功能介绍,基于arm-buildroot-linux-gnueabihf-工具链,这些命令主要用于交叉编译环境下,生成适用于ARM架构的二进制文件:

1. AS - 汇编器

  • 命令arm-buildroot-linux-gnueabihf-as
  • 作用:汇编器将汇编语言源代码(通常是.s.S文件)转换为目标代码(.o文件)。
  • 主要用途
    在嵌入式开发中,汇编语言常被用于优化性能或实现底层硬件控制,as工具将这些汇编代码进行汇编生成机器代码。

示例

arm-buildroot-linux-gnueabihf-as -o startup.o startup.s

2. LD - 链接器

  • 命令arm-buildroot-linux-gnueabihf-ld
  • 作用:链接器将多个目标文件(.o)和库文件组合在一起,生成最终的可执行文件或链接库。
  • 主要用途
    ld 负责解决符号依赖和内存地址映射,确保程序能正确运行。

示例

arm-buildroot-linux-gnueabihf-ld -o final.elf startup.o main.o

3. CC - 编译器

  • 命令arm-buildroot-linux-gnueabihf-gcc
  • 作用:C/C++ 编译器将 C/C++ 源代码(.c.cpp)编译成目标代码(.o文件),也可以直接生成可执行文件。
  • 主要用途
    gcc 是工具链的核心组件,通常还调用as(汇编器)和ld(链接器)来完成整个编译和链接过程。

示例

arm-buildroot-linux-gnueabihf-gcc -c main.c -o main.o
arm-buildroot-linux-gnueabihf-gcc -o test main.o

4. AR - 静态库归档工具

  • 命令arm-buildroot-linux-gnueabihf-ar
  • 作用:将多个目标文件(.o)打包成静态库(.a文件)。
  • 主要用途
    提供静态链接库支持,便于代码复用和模块化。

示例

arm-buildroot-linux-gnueabihf-ar rcs libmy.a file1.o file2.o

5. NM - 符号表查看工具

  • 命令arm-buildroot-linux-gnueabihf-nm
  • 作用:显示目标文件(.o.a 或可执行文件)的符号表,查看符号的地址、类型以及名称。
  • 主要用途
    用于调试和分析目标文件中的符号(如函数、变量等),帮助开发者理解程序的链接状态。

示例

arm-buildroot-linux-gnueabihf-nm main.o

小结

这些工具在嵌入式开发中协同工作,as 负责汇编,gcc 负责编译,ld 负责链接,ar 负责归档静态库,nm 负责查看符号表。在交叉编译工具链中,它们会生成适用于 ARM 架构的二进制文件,而不是本地主机(如 x86 架构)可以直接运行的文件。

`CPP = $(CC) -E

能不能解释下Makefile_A中的语句:

CPP		= $(CC) -E

这行语句在 Makefile_A 中的作用是定义了 CPP 变量,并将其设置为 $(CC) -E。具体来说:

CPP = $(CC) -E

  • $(CC) 是之前定义的变量,它代表交叉编译工具链中的 C 编译器,具体为 arm-buildroot-linux-gnueabihf-gcc。所以 $(CC) 就是 arm-buildroot-linux-gnueabihf-gcc

  • -Egcc 编译器的一个选项,表示 只运行预处理(Preprocessing),并不会编译成目标代码或执行链接步骤。

作用
这行代码将 CPP 定义为 arm-buildroot-linux-gnueabihf-gcc -E,它的作用是执行 C 预处理。预处理是 C 编译过程的第一步,主要作用包括:

  1. 宏展开:处理所有的宏定义(如 #define)和预处理指令。
  2. 文件包含:处理 #include 指令,将头文件的内容插入到源代码中。
  3. 条件编译:处理条件编译指令(如 #ifdef#ifndef 等)。

使用场景
通常,预处理过程的输出会显示为一个经过宏展开和文件包含后的纯文本文件,通常扩展名为 .i。这种输出并不包含编译后的机器代码,只是纯粹的源代码文本,适合查看代码的预处理结果。

例如,执行以下命令:

arm-buildroot-linux-gnueabihf-gcc -E main.c

会输出 main.c 文件经过预处理后的结果。这个过程通常在调试宏定义或处理头文件时非常有用。

小结

  • CPP = $(CC) -E 的作用是定义 CPP 作为预处理器(预处理阶段的工具),通常用于处理 C 语言代码中的宏定义、头文件包含和条件编译等,输出的是经过预处理的代码。

STRIP、OBJCOPY、OBJDUMP进行定义的语句

STRIP		= $(CROSS_COMPILE)strip
OBJCOPY		= $(CROSS_COMPILE)objcopy
OBJDUMP		= $(CROSS_COMPILE)objdump

我把CROSS_COMPILE的值arm-buildroot-linux-gnueabihf-代入STRIP、OBJCOPY、OBJDUMP中,得到了下面的命令:

STRIP		= arm-buildroot-linux-gnueabihf-strip
OBJCOPY		= arm-buildroot-linux-gnueabihf-objcopy
OBJDUMP		= arm-buildroot-linux-gnueabihf-objdump

请分别说下这些命令各自的作用。
根据你提供的命令,下面是对 STRIPOBJCOPYOBJDUMP 命令的详细解释:

1. STRIP - 去除符号信息工具

  • 命令arm-buildroot-linux-gnueabihf-strip
  • 作用strip 工具用于去除目标文件或可执行文件中的符号信息和调试信息,从而减少文件的大小。
  • 主要用途
    • 在生成最终的二进制文件(如嵌入式设备上的可执行文件)时,通常会去除不必要的符号和调试信息,优化存储空间。
    • 对于生产环境中的可执行文件,strip 可以有效减小文件大小,提升加载速度。

示例

arm-buildroot-linux-gnueabihf-strip myprogram

上述命令会去除 myprogram 可执行文件中的符号信息和调试信息,减少文件的体积。

能不能举例说明下arm-buildroot-linux-gnueabihf-strip 这条命令解释中提到的符号信息和调试信息,比如哪样的东西属于符号信息和调试信息?关于这个问题,详细解释见 目标文件或可执行文件中的符号信息和调试信息是怎么回事儿?


2. OBJCOPY - 二进制文件转换工具

  • 命令arm-buildroot-linux-gnueabihf-objcopy
  • 作用objcopy 用于转换、复制和修改目标文件的格式。它可以将目标文件(如 .elf 格式的文件)转换成其他格式(如 .bin.hex 等),或者提取文件中的某些部分。
  • 主要用途
    • 将 ELF 文件转换为纯二进制格式(.bin),该格式通常用于嵌入式系统的固件烧录。
    • 提取和操作目标文件中的特定节(section)。

示例

arm-buildroot-linux-gnueabihf-objcopy -O binary myprogram.elf myprogram.bin

上述命令将 myprogram.elf 转换成 myprogram.bin,即从 ELF 格式转换为纯二进制格式。


3. OBJDUMP - 反汇编工具

  • 命令arm-buildroot-linux-gnueabihf-objdump
  • 作用objdump 是一个非常有用的工具,用于反汇编目标文件。它可以显示目标文件中的机器代码及其对应的汇编指令,还可以展示目标文件的符号表和节(section)信息。
  • 主要用途
    • 用于分析和调试目标文件或可执行文件。
    • 反汇编程序以查看其指令和符号,帮助开发人员理解程序的执行过程。
    • 打印目标文件中的各种信息,如节的内容、符号表、调试信息等。

示例

arm-buildroot-linux-gnueabihf-objdump -d myprogram.elf

上述命令将 myprogram.elf 文件反汇编为可读的汇编指令。


小结

  • STRIP:用来去除目标文件或可执行文件中的符号和调试信息,减小文件大小。
  • OBJCOPY:用于转换目标文件格式,或者从目标文件中提取内容,常用于将 ELF 格式转换为二进制格式。
  • OBJDUMP:用于反汇编目标文件,查看文件中的汇编代码、符号表和其他信息,帮助调试和分析二进制文件。

这三个工具在嵌入式开发中非常常见,尤其是在生产环境中,它们帮助优化文件大小并提供调试信息。

export AS LD CC CPP AR NM

能不能解释下Makefile_A中的语句:

export AS LD CC CPP AR NM

的作用?

Makefile_A 中,语句:

export AS LD CC CPP AR NM

的作用是将这些变量 ASLDCCCPPARNM 导出为环境变量,使得它们在 Makefile 的其他部分以及被调用的子 Makefile 或外部命令中可以被访问。

详细解释:

  1. export 命令
    export 是一个 shell 命令,它的作用是将指定的变量标记为环境变量,使得这些变量在当前 shell 进程以及该进程启动的任何子进程中都能被访问到。对于 Makefile,这意味着这些变量不仅在当前 Makefile 中有效,还能被 make 进程的子进程(比如调用的编译命令)访问。

  2. 导出的变量
    在这个命令中,ASLDCCCPPARNM 都是编译工具链的工具命令,它们分别代表汇编器、链接器、C 编译器、预处理器、静态库归档工具和符号表查看工具。通过 export,这些命令变量将成为环境变量。

    • AS:汇编器的命令,例如 arm-buildroot-linux-gnueabihf-as
    • LD:链接器的命令,例如 arm-buildroot-linux-gnueabihf-ld
    • CC:C 编译器的命令,例如 arm-buildroot-linux-gnueabihf-gcc
    • CPP:C 预处理器的命令,例如 arm-buildroot-linux-gnueabihf-gcc -E
    • AR:静态库归档工具的命令,例如 arm-buildroot-linux-gnueabihf-ar
    • NM:符号表查看工具的命令,例如 arm-buildroot-linux-gnueabihf-nm
  3. 影响范围
    当这些变量被 export 后,它们将成为环境变量,因此后续的任何命令、子 Makefile 或外部程序(比如编译器或链接器)可以直接使用这些环境变量来调用相应的工具。换句话说,这使得在其他地方(比如子 Makefile 或子命令)调用这些工具时,它们会自动使用定义的交叉编译工具链(比如 arm-buildroot-linux-gnueabihf-gcc 等)。

举例说明:

假设你在当前 Makefile 中设置了以下:

CC = arm-buildroot-linux-gnueabihf-gcc
export CC

如果在某个子 Makefile 或命令中调用了 $(CC),例如:

$(CC) -o output source.c

因为 CC 被导出为环境变量,make 会使用 arm-buildroot-linux-gnueabihf-gcc 来编译 source.c,而不是本地的 gcc

总结:

export AS LD CC CPP AR NM 这条命令的作用是将 ASLDCCCPPARNM 这几个变量导出为环境变量,使得它们可以在当前 Makefile 和任何被调用的子 Makefile 或命令中使用。这是为了确保交叉编译工具链中的命令能够在整个构建过程中被一致地访问和使用。

export STRIP OBJCOPY OBJDUMP

这条语句的解释见对上一条语句export AS LD CC CPP AR NM 的解释。

CFLAGS := -Wall -O2 -g

能不能解释下Makefile_A中的语句:

CFLAGS := -Wall -O2 -g

答:
Makefile_A 中,语句:

CFLAGS := -Wall -O2 -g

的作用是定义并设置一个名为 CFLAGS 的变量,用于指定 C 编译器 (gcc) 在编译源代码时所使用的选项和标志。

详细解释:

  1. CFLAGS 变量
    CFLAGS 是一个常用的 Makefile 变量,通常用来指定 C 编译器(gcc)的编译选项。它是传递给 gcc 编译器的参数,控制编译过程中的行为。

  2. := 运算符
    在 Makefile 中,:=立即赋值运算符。它会立即计算右边的值并将其赋给左边的变量,且该值在赋值后不再变化。因此,CFLAGS := -Wall -O2 -g-Wall -O2 -g 立即赋值给 CFLAGS,并且 CFLAGS 在后续的 Makefile 中保持这个值。

  3. 编译选项

    • -Wall
      这个选项告诉编译器启用所有常见的警告信息(warning)。这对于发现潜在的代码问题或编译器提醒非常有用。-Wall 不是指所有警告,而是“所有常见的警告”,但实际上它启用了绝大多数有用的警告信息。

    • -O2
      这是一个优化选项,表示 中等级别的优化-O2 会在不增加编译时间过长的前提下进行多种优化,优化程序的执行效率。这通常是一个平衡的选项,既能提高代码性能,又不会引入过多的编译时间。

    • -g
      这个选项告诉编译器 生成调试信息,使得编译出来的程序包含源代码与编译后的机器代码之间的映射信息。调试信息可以帮助调试器(如 gdb)在调试时定位源代码行、查看变量的值等。生成调试信息会增大目标文件的大小,但对于调试过程是必需的。当然,在部署到生产环境时,可以利用命令arm-buildroot-linux-gnueabihf-strip myprogram去掉其中的调试信息哦。

整体作用:

CFLAGS := -Wall -O2 -g 这一行的意思是,设置 CFLAGS 为三个编译选项:启用常见警告(-Wall)、进行中等级别的优化(-O2)以及生成调试信息(-g)。这些选项会被传递给 C 编译器(例如 gcc)来编译源代码。

举例:

假设你在 Makefile 中有以下规则:

$(CC) $(CFLAGS) -o myprogram myprogram.c

执行 make 时,$(CC) 被替换为 arm-buildroot-linux-gnueabihf-gcc(根据之前的设置),而 $(CFLAGS) 被替换为 -Wall -O2 -g。因此,最终执行的命令将类似于:

arm-buildroot-linux-gnueabihf-gcc -Wall -O2 -g -o myprogram myprogram.c

这条命令会使用 arm-buildroot-linux-gnueabihf-gcc 编译器编译 myprogram.c,启用警告、进行优化并生成调试信息,最终生成名为 myprogram 的可执行文件。

总结:

CFLAGS := -Wall -O2 -g 这条语句设置了 C 编译器的选项,启用常见警告、进行优化并生成调试信息。这些选项使得编译过程更加严格、优化和易于调试。

CFLAGS += -I $(shell pwd)/include

能不能解释下Makefile_A中的语句:

CFLAGS += -I $(shell pwd)/include

答:
Makefile_A 中,语句:

CFLAGS += -I $(shell pwd)/include

的作用是将指定的包含目录路径 $(shell pwd)/include 添加到 CFLAGS 变量中,供 C 编译器在编译时使用。

详细解释:

  1. CFLAGS +=

    • +=追加赋值运算符。它的作用是将右边的内容追加到 CFLAGS 变量的原有值上。
    • 如果之前 CFLAGS 已经有值(如在之前的行中设置了 CFLAGS := -Wall -O2 -g),这条语句会将新的选项添加到原有的 CFLAGS 选项之后。CFLAGS 就变成了之前的内容加上新的选项。

    例如,如果 CFLAGS 最初是 -Wall -O2 -g,执行此命令后,CFLAGS 将变为 -Wall -O2 -g -I $(shell pwd)/include

  2. -I 选项

    • -I 是 GCC 编译器的一个选项,用来指定 头文件的搜索目录。在编译源代码时,GCC 会查找指定路径下的头文件。
    • 这意味着,如果源代码中 #include 语句引用了某个头文件,GCC 会在 -I 指定的路径中查找这个头文件。
  3. $(shell pwd)

    • $(shell ...) 是 Makefile 中的一个内建函数,它会执行括号中的 shell 命令,并将命令的输出作为值返回。
    • 在此语句中,$(shell pwd) 执行的是 pwd 命令,该命令会返回当前工作目录的绝对路径。

    例如,如果当前工作目录是 /home/user/project,那么 $(shell pwd) 会返回 /home/user/project

  4. /include

    • 这个部分是将 $(shell pwd) 返回的当前工作目录路径与 /include 连接在一起,构成一个新的路径。
    • 如果当前目录是 /home/user/project,那么 $(shell pwd)/include 的值就是 /home/user/project/include

组合在一起:

CFLAGS += -I $(shell pwd)/include 的作用是将当前工作目录下的 include 目录添加到 CFLAGS 变量中,作为 C 编译器的头文件搜索路径。这样,编译器在编译时,如果遇到 #include 指令查找头文件时,会去当前目录下的 include 目录查找头文件。

举例说明:

假设在当前目录下有一个 include 文件夹,里面有一个名为 myheader.h 的头文件。如果在源代码中写了如下的 #include "myheader.h"

#include "myheader.h"

通过 CFLAGS += -I $(shell pwd)/include,编译器会在 /home/user/project/include 目录下查找 myheader.h 文件。

总结:

CFLAGS += -I $(shell pwd)/include 语句的作用是将当前工作目录下的 include 文件夹添加到 C 编译器的头文件搜索路径中,这样编译器在处理 #include 指令时,会去 $(shell pwd)/include 目录查找头文件。

LDFLAGS :=

能不能解释下Makefile_A中的语句:

LDFLAGS := 

答:
Makefile_A 中,语句:

LDFLAGS := 

的作用是定义并初始化一个名为 LDFLAGS 的变量,通常用于存储链接器的选项和标志。在这个例子中,LDFLAGS 被初始化为空字符串(即没有任何值)。

详细解释:

  1. LDFLAGS 变量

    • LDFLAGS 是一个常用的 Makefile 变量,通常用于存储链接器(ld)的选项。链接器负责将编译后的目标文件(.o 文件)和库文件链接成最终的可执行文件。
    • 这个变量通常用于指定链接器的选项,比如库的搜索路径、库文件、优化选项等。
  2. := 运算符

    • :=立即赋值运算符,它会立即计算右边的值,并将该值赋给左边的变量。
    • 在这个例子中,LDFLAGS := 表示将 LDFLAGS 设置为空字符串。这意味着目前 LDFLAGS 没有任何链接器选项。
  3. 空字符串赋值

    • 通过 LDFLAGS := 赋值为空字符串,实际上是将 LDFLAGS 变量初始化为空。这意味着在这个位置,LDFLAGS 不包含任何额外的链接器选项。
    • 这种做法有时用于清楚地定义一个空的变量,目的是在后续的 Makefile 中可能会根据需要对 LDFLAGS 添加选项(通过 +=)。

使用示例:

尽管此行初始化了 LDFLAGS 为空,通常你会在后续的 Makefile 中为 LDFLAGS 添加一些链接器选项。例如,可能会在后面看到类似这样的行:

LDFLAGS += -L /path/to/library -lm

这行命令会将库路径 -L /path/to/library 和链接数学库 -lm 添加到 LDFLAGS 中。

然后在链接阶段,可能会有类似这样的命令:

$(CC) $(LDFLAGS) -o $(TARGET) built-in.o

此时,$(LDFLAGS) 会被替换为指定的链接选项,最终传递给链接器(如 ld)。

总结:

LDFLAGS := 语句在 Makefile 中定义了一个空的 LDFLAGS 变量,通常用于存储链接器选项。初始化为空字符串表明目前没有任何链接选项,后续可以通过 += 向其中添加具体的链接器选项。

export CFLAGS LDFLAGS

这条语句的解释略,参看前面对export AS LD CC CPP AR NM的解释。

TOPDIR := $(shell pwd)

这条语句的解释略,参看前面对CFLAGS += -I $(shell pwd)/include的解释。
这条语句的作用在后面的下面这句代码中会用到哦:

all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build

$(TARGET) : built-in.o
	$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

具体的理解请看后面对语句all : start_recursive_build $(TARGET)....的解释吧。

TARGET := test

这条语句的解释略,参看前面对CFLAGS := -Wall -O2 -g的解释。

obj-y += display/

能不能解释下Makefile_A中的语句:

obj-y += display/

答:
Makefile_A 中,语句:

obj-y += display/

是一个用于 指定目标对象文件目录 的语句,通常用于 构建模块或子目录 的相关目标。这里的 obj-y 是一个特殊变量,通常用于表示参与构建目标文件的目录或文件,在这里是表示子目录 display/ 中的目标文件(目标文件即.o文件)需要参与构建。

详细解释:

  1. obj-y 变量

    • obj-y 是一个特定的 Makefile 变量,常用于 Linux 内核或类似的模块化构建系统中。它的作用是列出那些需要参与构建的文件路径或子目录或具体的文件,在这里是表示子目录 display/ 需要参与构建。
  2. += 运算符

    • += 是一个 追加赋值运算符,它将右边的值(在这个例子中是 display/)添加到左边的 obj-y 变量中。
    • 如果 obj-y 已经有其他的值,那么 += 会将 display/ 追加到现有的值之后。

作用:

  • obj-y += display/ 语句的作用是告诉 Makefile 在构建过程中包括 display/ 子目录。如果想了解实现细节,要去认真阅读文件Makefile.build,而进入文件Makefile.build的关键语句是后面的:make -C ./ -f $(TOPDIR)/Makefile.build

例如:

假设 display/ 目录下有一个 Makefile,并且该目录中有一个源代码文件 display.c。在 display/Makefile 中,可能有类似这样的内容:

obj-y += display.o

这意味着,display.c 将被编译成 display.o 目标文件,并且这个文件会被添加到最终的目标中。

总结:

  • obj-y += display/ 的意思是将 display/ 子目录中的目标文件(通常是 .o 文件)加入到构建过程中的目标中。而至于子目录有哪些目标文件,由于子目录的Makefile 文件来确定。
  • 这是一种模块化构建的方式,允许你将构建过程拆分为多个子目录,每个子目录负责自己的部分,最终通过 obj-y 将它们汇聚在一起。

关于下面两条命令的详细理解,请见我的另一篇博文:https://blog.csdn.net/wenhao_ir/article/details/144544647

obj-y += display/ # 这是顶层Makefile文件的obj-y语句
obj-y += unittest/ # 这是子目录Makefile文件的obj-y语句

obj-y += unittest/

这条语句的解释略,参看前面对obj-y += display/的解释。

all : start_recursive_build $(TARGET)...

能不能解释下Makefile_A中的语句:

all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build

$(TARGET) : built-in.o
	$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

答:
Makefile_A 中,以下这段代码:

all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build

$(TARGET) : built-in.o
	$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

定义了 allstart_recursive_build$(TARGET) 这三个目标及它们之间的依赖关系。我们逐一解释每个目标和其关联的规则。

1. all : start_recursive_build $(TARGET)

这个规则定义了 all 目标,它依赖于 start_recursive_build$(TARGET) 两个目标。

  • all 目标

    • all 是默认的目标,如果你在命令行运行 make 而不指定目标,make 会默认执行 all 目标。
    • make 执行 all 目标时,它会依次执行 start_recursive_build$(TARGET) 这两个目标。
  • start_recursive_build$(TARGET)

    • 这两个目标的依赖会先被构建,只有在它们都完成后,all 目标的命令(@echo $(TARGET) has been built!)才会执行。

2. @echo $(TARGET) has been built!

  • 这是 all 目标完成后的命令,它会输出一条消息,表示构建的目标已经完成。
  • $(TARGET) 是一个变量,代表最终生成的可执行文件(在此示例中是 test),所以这条消息会输出:
    test has been built!
    
  • @ 前缀会禁止 make 打印命令本身,只有命令的输出会显示在终端。

3. start_recursive_build 目标

start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build
  • start_recursive_build 目标定义了一个递归构建过程。
  • make -C ./ -f $(TOPDIR)/Makefile.build
    • -C ./ 表示 make 在当前目录(./)中执行。
    • -f $(TOPDIR)/Makefile.build 指定了要使用的 Makefile 文件,$(TOPDIR) 是一个变量,通常指向项目的根目录,因此 $(TOPDIR)/Makefile.build 是一个特定的 Makefile 文件路径。
    • 这条命令启动了一个子 Make 进程,使用 Makefile.build 文件继续执行构建过程。

4. $(TARGET) : built-in.o

$(TARGET) : built-in.o
	$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
  • 这个规则定义了 $(TARGET)(最终的可执行文件)是如何生成的。
  • $(TARGET) 是最终的目标文件名(通常是 test,因为 TARGET := test)。
  • $(TARGET) 依赖于 built-in.o 目标文件。意味着在构建 $(TARGET) 之前,built-in.o 必须先被编译出来。
  • $(CC) -o $(TARGET) built-in.o $(LDFLAGS)
    • $(CC) 是编译器命令(如 gcc)。
    • -o $(TARGET) 指定了输出的目标文件名,即 $(TARGET)(在此为 test)。
    • built-in.o 是一个目标文件,它作为输入传递给链接器。
    • $(LDFLAGS) 是链接器选项(比如库路径、库文件等),虽然此处为空(在 Makefile_ALDFLAGS := 为初始化为空),但可以通过 += 为它添加链接器选项。

规则总结:

  1. all : start_recursive_build $(TARGET)all 目标依赖于 start_recursive_build$(TARGET)make 首先会执行 start_recursive_build,然后执行 $(TARGET) 的构建过程。
  2. start_recursive_build:该目标会调用 make -C ./ -f $(TOPDIR)/Makefile.build,启动另一个子 Makefile 进行构建。
  3. $(TARGET) : built-in.o:最终目标 $(TARGET) 依赖于 built-in.o,并使用指定的编译器将其链接成最终的可执行文件(test)。

运行流程:

  1. 当你运行 make 时,它会首先构建 start_recursive_build 目标,触发对 Makefile.build 的递归构建。
  2. 然后它会继续构建 $(TARGET),即生成最终的可执行文件 test
  3. 完成后,输出 test has been built!,表示构建完成。

通过这种方式,Makefile_A 支持模块化构建,允许在不同的 Makefile 之间进行递归调用,以便管理更复杂的构建过程。

clean语句

clean:
	rm -f $(shell find -name "*.o")
	rm -f $(TARGET)

这个没有什么好说的,当要重新编译的时候运行make clean就可以执行这些语句了。

distclean语句以及.d文件是什么文件

	rm -f $(shell find -name "*.o")
	rm -f $(shell find -name "*.d")
	rm -f $(TARGET)

这个也没有什么好说的,相比于make clean,这个是多删除了.d文件。这里要特别介绍下什么叫.d文件?

在构建系统中,*.d 文件 通常是 依赖文件,它们由编译器在编译源文件时生成,用于描述源文件及其依赖关系(例如头文件)的信息。这种文件在增量构建中非常有用。


*.d 文件的作用

  1. 依赖关系描述

    • *.d 文件记录了源文件(例如 main.c)和它所依赖的头文件(例如 myheader.h)之间的关系。
    • 这样在下一次构建时,make 可以根据这些依赖关系判断哪些文件需要重新编译,哪些文件不需要重新编译,从而提高编译效率。
  2. 自动生成

    • gcc 等编译器中,可以使用 -M-MD-MMD 选项生成依赖文件。例如:
      gcc -MMD -c main.c -o main.o
      
      这会生成:
      • main.o:目标文件。
      • main.d:依赖文件,记录 main.c 和相关头文件之间的依赖关系。
  3. 示例内容
    假设有一个源文件 main.c,它包含以下内容:

    #include "myheader.h"
    int main() { return 0; }
    

    通过命令 gcc -MMD -c main.c -o main.o,生成的 main.d 文件内容可能如下:

    main.o: main.c myheader.h
    

    这表示 main.o 依赖于 main.cmyheader.h。如果 main.cmyheader.h 发生变化,make 会重新编译 main.c


find -name "*.d"

  • 这条命令会在当前目录及其子目录中查找所有后缀名为 .d 的文件。
  • 在项目目录中,*.d 文件往往是由编译器自动生成的依赖文件。

为什么删除 *.d 文件?

  • 清理依赖文件*.d 文件是自动生成的,删除它们不会影响项目代码,可以在下一次构建时重新生成。
  • 确保干净的环境:有时候,依赖文件可能由于编译器或其他原因变得不正确,删除它们可以强制重新生成,确保依赖关系的准确性。

小结

*.d 文件是 依赖文件,用于记录源文件与头文件之间的依赖关系。在构建系统中,这些文件有助于增量编译,提高构建效率。在 make clean 目标中删除它们,可以确保项目目录保持干净,并在下次构建时重新生成这些依赖文件。

子目录display中的Makefile文件

子目录display中的Makefile文件的内容

EXTRA_CFLAGS  := 
CFLAGS_file.o := 

obj-y += disp_manager.o
obj-y += framebuffer.o

语句EXTRA_CFLAGS :=

子目录 display 中的 Makefile 里有下面的语句:

EXTRA_CFLAGS  := 

这条语句在 Makefile 中的作用是定义一个名为 EXTRA_CFLAGS 的变量,并将其初始化为空值。下面我会详细解释这条语句的含义、使用场景及作用。


1. CFLAGSEXTRA_CFLAGS 的背景

  • CFLAGSmake 构建系统中常见的变量,用于指定传递给 编译器 gcc 的编译选项,例如:

    CFLAGS := -Wall -O2 -g
    
    • -Wall:启用所有常见的警告。
    • -O2:优化级别 2,平衡编译时间和性能。
    • -g:生成调试信息。
  • EXTRA_CFLAGS 是在很多项目的 Makefile 构建系统中引入的额外编译标志变量,用来对 CFLAGS 进行补充。这通常出现在模块化构建系统中。


2. EXTRA_CFLAGS 的作用

在项目的构建规则中,EXTRA_CFLAGS 通常是作为附加编译选项来提供的。项目在编译源文件时,EXTRA_CFLAGS 会被加到默认的 CFLAGS 之后,传递给编译器。

例如,Makefile.build 文件中有以下规则:

%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

在这条规则中:

  • $(CFLAGS):默认的编译选项(如 -Wall -O2 -g)。
  • $(EXTRA_CFLAGS):用户在子目录的 Makefile 里提供的额外编译选项。
  • $(CFLAGS_$@):针对特定目标文件的编译选项。

3. EXTRA_CFLAGS 为什么初始化为空?

EXTRA_CFLAGS 初始化为空值 := 是一种占位默认赋值的方式,具体作用包括:

  1. 确保变量存在

    • 初始化为空,表示在当前目录下 EXTRA_CFLAGS 没有附加的编译选项,但其存在确保在构建系统中不会报错。
    • 如果其他构建系统或上层 Makefile 尝试引用 EXTRA_CFLAGS,也不会出错。
  2. 灵活性

    • 后续可以根据需要在同一目录或其他地方追加编译选项。
    • 例如,可以通过 += 追加选项:
      EXTRA_CFLAGS += -DDEBUG
      
      这里 -DDEBUG 表示定义 DEBUG 宏,常用于调试模式下。
  3. 统一模板

    • 在模块化的构建系统中,所有子目录的 Makefile 可能会遵循一个统一的模板。
    • 即使当前目录没有额外的编译选项,为了格式一致性,保留 EXTRA_CFLAGS 的声明。

语句CFLAGS_file.o :=

子目录 display 中的 Makefile 里有下面的语句:

CFLAGS_file.o :=

1. 这条语句的含义

CFLAGS_file.o := 的作用是定义一个名为 CFLAGS_file.o 的变量,并将其初始化为空值

Makefile 构建系统中,变量 CFLAGS_<目标文件> 是一种约定的写法,用于为特定的目标文件指定额外的编译选项。
这里的 file.o 是一个目标文件名的占位符,意味着这个变量可以根据实际目标文件来定义,例如:

CFLAGS_disp_manager.o := -O0
CFLAGS_framebuffer.o := -DDEBUG

2. CFLAGS_file.o := 为什么初始化为空?

CFLAGS_file.o 初始化为空的主要目的是:

  1. 确保变量存在

    • 明确表示可以为特定目标文件提供自定义的编译选项。
    • 即使当前没有任何选项需要传递,也保留了变量的声明,确保构建系统中的规则不会出错。
  2. 模板化设计

    • 这种写法通常是构建系统模板的一部分,子目录的 Makefile 可能会预留用于后续开发者扩展的空间。
    • 如果需要为特定文件添加额外编译选项,只需追加:
      CFLAGS_disp_manager.o := -O0
      
  3. 灵活扩展

    • 可以很容易地为特定文件设置单独的编译选项,而不影响其他目标文件的编译。

3. 示例

假设项目目录如下:

project/
├── Makefile        # 顶层 Makefile
├── Makefile.build  # 递归构建规则
├── display/
│   ├── Makefile    # display 子目录的 Makefile
│   ├── disp_manager.c
│   └── framebuffer.c
子目录 Makefile 内容
EXTRA_CFLAGS := -DDEBUG
CFLAGS_disp_manager.o := -O0
CFLAGS_framebuffer.o  := -O2

obj-y += disp_manager.o
obj-y += framebuffer.o
Makefile.build 中的规则
%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -c -o $@ $<
编译流程
  1. 当编译 disp_manager.c 生成 disp_manager.o 时:

    • $(CFLAGS):例如 -Wall -g
    • $(EXTRA_CFLAGS)-DDEBUG
    • $(CFLAGS_disp_manager.o)-O0(针对 disp_manager.o 的特别选项)。

    最终编译命令:

    arm-buildroot-linux-gnueabihf-gcc -Wall -g -DDEBUG -O0 -c -o disp_manager.o disp_manager.c
    
  2. 当编译 framebuffer.c 生成 framebuffer.o 时:

    • $(CFLAGS):例如 -Wall -g
    • $(EXTRA_CFLAGS)-DDEBUG
    • $(CFLAGS_framebuffer.o)-O2(针对 framebuffer.o 的特别选项)。

    最终编译命令:

    arm-buildroot-linux-gnueabihf-gcc -Wall -g -DDEBUG -O2 -c -o framebuffer.o framebuffer.c
    

  1. 总结
  • CFLAGS_file.o := 是一个占位声明,用于为特定目标文件file.o)指定额外的编译选项。
  • 这种写法是模块化和模板化设计的一部分,便于后续开发者根据需求为特定目标文件添加自定义编译标志。
  • 在当前 Makefile 中,将其初始化为空表示当前没有特别的编译选项,但保留了扩展的空间。

语句:obj-y += disp_manager.o

结合之前对语句obj-y += display/的理解和学习,我们知道语句obj-y += disp_manager.o就是表明子目录display中将会有目标文件 disp_manager.o 参与最终文件的构建。

关于下面两条命令的详细理解,请见我的另一篇博文:https://blog.csdn.net/wenhao_ir/article/details/144544647

obj-y += display/ # 这是顶层Makefile文件的obj-y语句
obj-y += unittest/ # 这是子目录Makefile文件的obj-y语句

文件:Makefile.build(顶层Makefile文件调用的Makefile文件-我命名为Makefile_B)

Makefile.build文件是怎么回事儿?

详细介绍见 有些Makefile文件以“.build”的后缀结尾是怎么回事儿?【Makefile.build文件是怎么回事儿?】

Makefile.build的内容:

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)

# cur_objs : 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 : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^

dep_file = .$@.d

%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
	
.PHONY : $(PHONY)

PHONY := __build和__build:

能不能解释下Makefile_B中的语句:

PHONY := __build
__build:

关于这两条语句的详细解释请见博文 https://blog.csdn.net/wenhao_ir/article/details/144556756
读了上篇这篇博文后你肯定知道了PHONY := __build的作用。

注意:这里的__build:你并不要以为它是一个空的目标你就认为它可以去除,事实上这里是不可以去除的,原因你读了我的下面这篇博文就清楚了:
Makefile中使用include语句时要特别注意避免陷入无限循环的死循环中

定义几个变量:obj-y、subdir-y、EXTRA_CFLAGS ,重点理解变量obj-y的值来源

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

如果从头到尾看这篇博文到这里,就知道这几条语句的作用了,这些变量后面可能用到,也可能没用到,如果用到,可以用符号+=追加。
问:
在顶层Makefile文件中,已经有变量obj-y了,那么在Makefile_B中(即文件Makefile.build中),还用语句“obj-y :=”定义obj-y干什么?还有后面的语句cur_objs := $(filter-out %/, $(obj-y))明明在从变量obj-y中提取内容,那说明obj-y中有内容,那么这个内容是从哪里来的?
这个问题的答案是:哪个Makefile文件调用件Makefile.build文件,其变量obj-y就会传递给Makefile.build的变量obj-y

__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))

__subdir-y	:= $(patsubst %/,%,$(filter %/, $(obj-y)))

这条语句在 Makefile_B 中的作用是从变量 obj-y 中提取所有以 / 结尾的目录名称,并去掉末尾的斜杠 /,将结果存储到变量 __subdir-y 中。


语句拆解

__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
1. $(filter %/, $(obj-y))
  • 作用:从 obj-y 中筛选出所有以 / 结尾的项。
  • 机制filter 函数根据指定的模式 "%/"(匹配以 / 结尾的字符串)对 $(obj-y) 中的内容进行过滤。
  • 例子
    obj-y := a.o b.o c/ d/
    $(filter %/, $(obj-y)) -> c/ d/
    
2. $(patsubst %/,%,$(...))
  • 作用:将筛选出的目录名称(如 c/d/)去掉末尾的 /
  • 机制patsubst 函数将模式 "%/" 替换为 "%
  • 例子
    $(patsubst %/,%,$(filter %/, $(obj-y))) -> c d
    
3. 赋值给 __subdir-y
  • 最终结果存储在变量 __subdir-y 中,用于表示当前 obj-y 中的所有子目录的名称。

整体流程示例

输入:
obj-y := a.o b.o c/ d/
执行:
  1. $(filter %/, $(obj-y)) 筛选出所有目录:
    结果c/ d/
  2. $(patsubst %/,%,$(...)) 去掉目录末尾的斜杠:
    结果c d
  3. 将结果赋值给 __subdir-y
    最终结果__subdir-y := c d

用途

这个语句的主要用途是:

  1. 确定需要递归处理的子目录。
  2. 在 Makefile 中,通常会对这些子目录执行额外的构建规则(如调用 make 处理每个子目录的内容)。

小结

__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
  • obj-y 中提取所有以 / 结尾的子目录名称,并去掉末尾的 /
  • 结果存储在 __subdir-y 中,便于后续处理子目录的构建逻辑。

subdir-y += $(__subdir-y)

subdir-y	+= $(__subdir-y)

这条语句的作用是将变量 __subdir-y 中的值追加到变量 subdir-y 中,所以subdir-y表示当前需要递归处理的所有子目录。

subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

这条语句的作用是根据 subdir-y 中列出的子目录名称,生成每个子目录对应的目标文件路径,并将结果赋值给变量 subdir_objs


语法解析

关键函数:$(foreach ...)
$(foreach f,$(subdir-y),$(f)/built-in.o)
  • $(foreach ...) 的功能:对列表中的每个元素执行指定的操作,并将结果拼接成一个新列表。
    • 语法$(foreach <变量>,<列表>,<操作>)
      • <变量>:循环变量(这里是 f)。
      • <列表>:要遍历的值列表(这里是 $(subdir-y))。
      • <操作>:对每个列表项执行的操作(这里是 $(f)/built-in.o)。

执行过程

假设:

subdir-y := c d
  1. 遍历 subdir-y 的值 cd
  2. 对每个值,将其替换到操作 $(f)/built-in.o 中。
    • f = c,结果是 c/built-in.o
    • f = d,结果是 d/built-in.o
  3. 最终结果是:
    subdir_objs := c/built-in.o d/built-in.o
    

语句作用

subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
  • 目的:构造所有子目录生成的目标文件路径列表。
  • 含义:对于每个子目录(cd),生成其对应的目标文件 built-in.o 的路径。

用途

  • 变量 subdir_objs 的作用
    • 在后续的规则中,用于链接这些子目录生成的目标文件。
    • 例如:
      built-in.o : $(cur_objs) $(subdir_objs)
          $(LD) -r -o $@ $^
      
      • 这里,$(subdir_objs) 会被解析为 c/built-in.o d/built-in.o,这些目标文件会被链接到顶层的 built-in.o

总结

subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
  • 作用:根据子目录列表 subdir-y,生成每个子目录的目标文件路径列表。
  • 结果:对于 subdir-y := c d,得到 subdir_objs := c/built-in.o d/built-in.o
  • 用途:用于链接来自各子目录的目标文件,形成最终的构建结果。

cur_objs := $(filter-out %/, $(obj-y))

这条语句的作用是从 $(obj-y) 中筛选出所有不是目录的目标文件,并将结果存储在变量 cur_objs 中。让我们逐步解析这个语句。

语法解析

cur_objs := $(filter-out %/, $(obj-y))
  • filter-out 是一个 GNU make 的内置函数,用于从一个列表中筛选出不符合某个模式的元素。

    • 语法是:$(filter-out <pattern>, <list>)
    • 这个函数会返回一个新列表,包含 <list> 中不匹配 <pattern> 的元素。
  • %/ 是一个模式,表示以 / 结尾的字符串,通常用于匹配目录(在 make 中,目录的名称通常以 / 结尾)。

  • $(obj-y) 是一个变量,包含了目标文件的列表。根据上下文,obj-y 是一个包含目标文件或目录的列表,类似于 display/file.o

如何工作

  1. $(filter-out %/, $(obj-y)):这条语句从 $(obj-y) 中筛选出所有不以 / 结尾的项(即不包含目录的目标文件)。因为目录名称通常以 / 结尾,%/ 模式就是用来过滤掉目录的。

  2. 结果被赋值给 cur_objs 变量。

示例

假设 $(obj-y) 包含如下项:

obj-y := display/ file1.o file2.o subdir/

执行 $(filter-out %/, $(obj-y)) 后,返回的结果会是:

file1.o file2.o

即,所有以 / 结尾的目录(如 display/subdir/)被去除了,剩下的就是目标文件 file1.ofile2.o

最终,cur_objs 的值将是:

cur_objs := file1.o file2.o

作用总结

这条语句的主要目的是:

  • $(obj-y) 中提取出所有不是目录的目标文件。
  • 将这些目标文件存储在 cur_objs 中,供后续规则使用(例如,生成 built-in.o)。

换句话说,cur_objs 只会包含普通的 .o 目标文件,而不包括目录。

dep_files := $(foreach f,$(cur_objs),.$(f).d)

cur_objs := $(filter-out %/, $(obj-y))

Makefile_B 中,语句:

dep_files := $(foreach f,$(cur_objs),.$(f).d)

的作用是构建一个依赖文件(.d 文件)的列表。让我们逐步分析这条语句。


1. foreach 函数

foreachMake 中的一个内建函数,它用于遍历一个列表,并对列表中的每个元素应用一个给定的操作。

语法:

$(foreach var,list,text)
  • var 是一个占位符,用来表示列表中的每个元素。
  • list 是你要遍历的列表。
  • text 是对每个列表元素应用的操作。

foreach 的作用是遍历 list 中的每个元素,并将 text 作为格式化字符串返回,其中 var 会被替换为列表中的当前元素。


2. cur_objs 的含义

cur_objs 是一个列表,包含当前目录中的目标文件。这个变量是通过以下语句定义的:

cur_objs := $(filter-out %/, $(obj-y))
  • $(obj-y) 包含了所有参与构建的文件或目录。
  • $(filter-out %/, $(obj-y)) 过滤掉所有以 / 结尾的元素(即目录),保留的是文件名。
  • 所以,cur_objs 是当前目录中所有非目录的目标文件(例如 .o 文件)。

3. 构建 .d 文件名

现在,回到你的问题:

dep_files := $(foreach f,$(cur_objs),.$(f).d)
  • $(foreach f,$(cur_objs),.$(f).d)cur_objs 中的每个文件 f 变成一个以 . 开头、以 .d 结尾的文件名。例如,如果 cur_objs 包含 file.oanother.o,那么这条语句会生成如下的文件名:
    • .file.o.d
    • .another.o.d

这些 .d 文件是 依赖文件,用于记录每个目标文件(例如 .o 文件)所依赖的头文件。当使用 -M-MD 编译选项时,GCC 会生成这样的 .d 文件,内容记录了文件的所有依赖。


4. dep_files 变量的作用

dep_files 最终存储了一个依赖文件名的列表,这些文件用于后续的依赖管理。它们的作用是在编译过程中追踪文件的依赖关系,确保当某个头文件发生变化时,依赖该文件的源文件会被重新编译。


5. 例子

假设 cur_objs 的值是 file.o another.o,则:

dep_files := $(foreach f,$(cur_objs),.$(f).d)

会生成:

dep_files := .file.o.d .another.o.d

这个列表会在后续的规则中使用,例如在 Makefile 中包括 .d 文件,以便在文件发生变化时自动重新编译依赖的目标。


小结

这条语句通过 foreach 函数遍历 cur_objs 列表中的每个目标文件,将每个文件转换为对应的 .d 文件名,最终生成一个 .d 文件名的列表,通常用于管理依赖关系。

dep_files := $(wildcard $(dep_files))

dep_files := $(wildcard $(dep_files))

Makefile_B 中,语句:

dep_files := $(wildcard $(dep_files))

的作用是根据 dep_files 中的文件名模式查找实际存在的文件,并将它们存储回 dep_files 变量中。让我们逐步分析这条语句。


1. wildcard 函数

wildcardMake 中的一个内建函数,它用于根据给定的模式查找符合条件的文件。具体来说,它会返回匹配模式的所有文件名。

语法:

$(wildcard pattern)
  • pattern 是一个文件名模式(可以包含通配符,如 *?)。
  • wildcard 返回一个符合模式的文件列表。

例如,$(wildcard *.o) 会返回当前目录下所有以 .o 结尾的文件名列表。


2. dep_files 的含义

在之前的语句中,dep_files 是通过 foreach 函数生成的一组文件名列表。每个文件名是一个 .d 文件名,通常用于表示依赖文件。

例如:

dep_files := $(foreach f,$(cur_objs),.$(f).d)

假设 cur_objs 包含文件 file.oanother.o,那么 dep_files 会包含如下值:

dep_files := .file.o.d .another.o.d

3. wildcard $(dep_files) 的作用

当执行:

dep_files := $(wildcard $(dep_files))

时,wildcard 会使用 dep_files 中的每个模式来查找实际存在的文件。假设 dep_files .file.o.d .another.o.dwildcard 会查找当前目录中是否存在这些文件。如果存在,就会将它们返回;如果不存在,返回空值。

举个例子:

  • 如果 .file.o.d 存在,.another.o.d 不存在,dep_files 会变成:

    dep_files := .file.o.d
    
  • 如果 .file.o.d.another.o.d 都存在,dep_files 会变成:

    dep_files := .file.o.d .another.o.d
    

4. 小结

这条语句的目的是:

  • 检查并更新 dep_files 变量,确保它只包含当前存在的 .d 依赖文件,即把那些不存在的依赖文件去掉。
  • 使用 wildcard 函数,dep_files 中的每个文件模式都会被匹配并替换为实际存在的文件列表。

这个操作通常用于确保在编译过程中使用的依赖文件是有效的,避免使用根本不存在的文件。

include $(dep_files)

ifneq ($(dep_files),)
  include $(dep_files)
endif

Makefile_B 中,语句:

ifneq ($(dep_files),)
  include $(dep_files)
endif

的作用是检查 dep_files 是否为空,如果不为空,就将 dep_files 中列出的所有文件包含到当前的 Makefile 中。这通常用于在编译过程中引入依赖文件。让我们逐步分析这条语句。


1. ifneq 条件判断

ifneqMake 中的一种条件判断语句,用于判断两个值是否不相等。如果不相等,则执行对应的代码块。

语法:

ifneq (value1, value2)
  # 如果 value1 和 value2 不相等,执行以下命令
endif

在这个语句中:

ifneq ($(dep_files),)
  • $(dep_files) 表示 dep_files 变量的值。
  • , 后面的空值表示与空字符串进行比较。
  • 如果 $(dep_files) 不是空字符串(即它包含了实际的文件名),条件成立,make 会执行 include 语句。

2. include 语句

includeMake 中用于将其他 Makefile 或文件的内容包含到当前文件中的语句。它会将指定的文件插入到当前的 Makefile 中进行解析。

语法:

include file
  • file 可以是一个 Makefile 或其他包含规则、变量的文件。
  • make 执行时,include 会读取文件内容,并将其添加到当前 Makefile 中进行处理。

在这个语句中:

include $(dep_files)
  • $(dep_files) 是一个包含依赖文件(.d 文件)列表的变量。
  • include $(dep_files) 会将这些依赖文件包含进来。$(dep_files) 可能包含多个 .d 文件。

3. dep_files 是什么?

dep_files 存储了 .d 文件的路径,这些 .d 文件通常是由编译器(例如 GCC)使用 -MD-M 选项生成的,用来记录源文件与头文件之间的依赖关系。例如:

dep_files := .file.o.d .another.o.d

这些 .d 文件的内容可能看起来像这样:

file.o: file.c header.h

包含 .d 文件后,make 会根据这些依赖关系来决定在源文件或头文件更改时需要重新编译哪些文件。


4. 小结

这段代码的作用是:

  • 检查 dep_files 是否为空:如果 dep_files 非空(即包含了依赖文件列表),则将这些文件包含进来。
  • 包含依赖文件:通过 include $(dep_files),将 .d 依赖文件包含到 Makefile 中,确保 make 在后续的构建中能够处理这些依赖关系。

这样,当源文件或头文件发生变化时,make 可以通过这些依赖文件知道哪些目标文件需要重新编译。

PHONY += $(subdir-y)

PHONY += $(subdir-y)

这句话就不用说了,当认真学习了伪目标的相关知识后,这个没什么好说的。
伪目标参考我的另一篇博文:https://blog.csdn.net/wenhao_ir/article/details/144556756

__build : $(subdir-y) built-in.o

这条语名表示目标__build依赖于两个目标,分别是$(subdir-y)built-in.o

make -C $@ -f $(TOPDIR)/Makefile.build

$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

Makefile_B 中,语句:

$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

的作用是:对于每一个在 $(subdir-y) 变量中列出的子目录,make 会进入这些子目录并使用 Makefile.build 文件进行构建。下面我们逐步分析这条语句。


1. $(subdir-y) 变量

$(subdir-y) 变量包含了所有需要构建的子目录的列表。这些子目录是通过在 Makefile_A 中添加类似 obj-y += display/ 这样的语句指定的。

例如,如果在顶层 Makefile_A 中有:

obj-y += display/
obj-y += unittest/

那么 $(subdir-y) 将会包含 displayunittest,这两个子目录。


2. make -C $@ -f $(TOPDIR)/Makefile.build

这是 make 命令的一部分,详细解释如下:

  • -C $@:这个选项告诉 make 切换到 $@ 指定的目录。$@make 规则中代表当前目标的名称。在这里,它的值将是 $(subdir-y) 列表中的每个子目录名。所以,make 会进入每个子目录进行构建。

    例如,如果 $(subdir-y) 包含 displayunittestmake 会进入 display 目录并执行构建,然后再进入 unittest 目录。

  • -f $(TOPDIR)/Makefile.build:这选项告诉 make 使用指定的 Makefile 文件进行构建。在这里,$(TOPDIR) 是顶层目录的路径,而 Makefile.build 是每个子目录内共享的构建文件。所以,make 在进入子目录后会使用 Makefile.build 文件进行具体的构建规则处理。


3. 结合起来

  • 对于 $(subdir-y) 列表中的每个子目录,make 将执行以下操作:
    • 进入该子目录。
    • 使用 -f $(TOPDIR)/Makefile.build 指定的构建文件(Makefile.build)来执行子目录的构建规则。

4. 小结

这条语句的作用是:

  • 对子目录执行递归构建:通过遍历 $(subdir-y) 中列出的子目录,在每个子目录中执行 make -f $(TOPDIR)/Makefile.build
  • 通过这种方式,子目录可以有自己的 Makefile.build 文件,而顶层 Makefile 则负责管理整个项目的构建流程,确保所有子目录的构建都能被正确调用和执行。

5. 特别注意:子目录中的Makefile文件有何用?

子目录中的Makefile文件有何用?
答:对于语句:

make -C $@ -f $(TOPDIR)/Makefile.build

隐含的把每个子目中的Makefile文件也包含进了,具体的解释如下:

是的,语句:

make -C $@ -f $(TOPDIR)/Makefile.build

确实间接地触发了每个子目录中的 Makefile 文件,但其作用和包含的方式需要进一步解释。

语句解释

  • make -C $@ -f $(TOPDIR)/Makefile.build 这条命令的意思是:
    • make 会进入 $@ 指定的目录(即当前子目录),执行该目录下的构建。
    • -C $@ 参数表示进入子目录 $@$@Makefile 中通常表示目标)。
    • -f $(TOPDIR)/Makefile.build 指定了要使用的 Makefile 文件,这里用的是顶层目录中的 Makefile.build

如何影响子目录中的 Makefile

  • 虽然 make 命令显式地指定了 $(TOPDIR)/Makefile.build,但每个子目录中的 Makefile 文件仍然会被隐式包含。为什么呢?
    • Makefile.build 中会使用 make -C 来进入子目录时,make 会去寻找并执行该子目录中的 Makefile 文件,默认情况下,make 会在每个子目录中寻找一个名为 Makefilemakefile 的文件。
    • 这样,Makefile.buildmake -C $@ 命令会触发对每个子目录中 Makefile 文件的调用。子目录中的 Makefile 负责处理该目录内的特定构建规则。

built-in.o : $(cur_objs) $(subdir_objs)......

built-in.o : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^

这条语句是 Makefile_B 中的一个构建规则,定义了如何生成目标文件 built-in.o。让我们逐步解析这条规则:

规则的格式

Makefile 中,构建规则的一般格式是:

target : prerequisites
	recipe
  • target: 目标文件,表示要生成的文件或结果。
  • prerequisites: 依赖的文件或目标,只有这些依赖文件发生变化时,make 才会重新生成目标文件。
  • recipe: 实际执行的命令,用于生成目标文件。

解析语句

built-in.o : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^
  • 目标 (built-in.o): 目标是 built-in.o,意味着 make 会尝试生成一个名为 built-in.o 的文件。

  • 依赖 ($(cur_objs) $(subdir_objs)):

    • $(cur_objs)$(subdir_objs) 是该目标的依赖文件。
    • $(cur_objs) 是当前目录中所有的 .o 目标文件(通常是由 .c 文件生成的),而 $(subdir_objs) 是来自子目录的 built-in.o 文件。subdir_objs 变量是由子目录的构建规则生成的目标文件列表。

    这意味着,built-in.o 会依赖 $(cur_objs)$(subdir_objs) 中列出的所有目标文件。在这些目标文件没有改变时,make 不会重新生成 built-in.o

  • 命令 ($(LD) -r -o $@ $^):

    • $(LD) 是链接器(ld),在此处用于链接生成目标文件。
    • -r 参数指示链接器进行“部分链接”,即将多个目标文件合并成一个目标文件(而不是生成最终的可执行文件)。
    • -o $@ 指定输出文件,$@ 是目标名称,在这个规则中,$@ 会被替换为 built-in.o
    • $^ 表示所有的依赖文件,在这里它将会被替换为 $(cur_objs) $(subdir_objs) 的内容,即所有 .o 文件的列表。

作用总结

这一行命令的作用是将所有当前目录($(cur_objs))和子目录($(subdir_objs))中的目标文件(通常是 .o 文件)链接成一个名为 built-in.o 的目标文件。此目标文件是多个对象文件的“部分链接”,即合并在一起,但并未最终生成可执行文件。

通过这种方式,built-in.o 成为一个中间目标,它将包含所有的 .o 文件,用于后续的链接阶段,或其他构建步骤。

举例

假设 $(cur_objs)file1.o file2.o$(subdir_objs)subdir1/built-in.o subdir2/built-in.o,那么执行这条规则时,命令将会是:

$(LD) -r -o built-in.o file1.o file2.o subdir1/built-in.o subdir2/built-in.o

这样,built-in.o 就包含了这些 .o 文件,作为后续构建的中间产物。

dep_file = .$@.d

dep_file = .$@.d

这条语句是为下一条语句:

%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

作准备的,我们看到语句$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<中含有dep_file这个变量。
dep_file实际上在下面的语句中被展开为依融文件的名称,如果要读懂它,需要了解运算符=和自动变量的相关知识,详情见 https://blog.csdn.net/wenhao_ir/article/details/144556756

$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

这段语句是一个 隐式规则,用于指定如何从 .c 文件生成 .o 文件。


详细解释

1. 模式规则 %.o : %.c
  • %.o 表示所有以 .o 为后缀的目标文件。
  • %.c 表示所有以 .c 为后缀的源文件。
  • % 是通配符,匹配目标和依赖的公共部分。
    比如:
    • 目标 main.o 对应的源文件是 main.c
    • 目标 utils.o 对应的源文件是 utils.c

这条规则的意思是:

对于每个 .c 文件,如何生成对应的 .o 文件。


2. 规则的命令
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

这是使用 $(CC) (即 C 编译器,例如 gcc)编译 .c 文件生成 .o 文件的命令。

(1) $(CC)
  • 表示 C 编译器。
  • 这个变量一般在 Makefile 的全局部分定义,例如 CC = gcc
(2) $(CFLAGS)
  • 编译器选项,用来设置通用的编译选项。
  • 比如 -Wall(启用所有警告),-O2(优化)等。
(3) $(EXTRA_CFLAGS)
  • 用于在特定场景下增加额外的编译选项。
  • 这个变量可以在不同的 Makefile 文件中被修改,例如为子目录或文件指定特殊的选项。
(4) $(CFLAGS_$@)
  • 表示针对特定目标的 CFLAGS
  • $@ 是自动变量,表示当前目标文件名。例如:
    • 如果目标是 main.o,则 CFLAGS_$@ 表示 CFLAGS_main.o
  • 这种方式允许为特定文件添加额外的编译选项。
(5) -Wp,-MD,$(dep_file)
  • 表示生成依赖文件:
    • -Wp 是传递给预处理器的选项。
    • -MD 命令告诉编译器在编译时生成依赖文件。
    • $(dep_file) 是依赖文件的名称。
    • 比如,当编译 main.c 时会生成隐藏文件 .main.o.d,其中记录了目标文件对源文件和头文件的依赖关系。
(6) -c
  • 表示只编译,不链接。
    • 编译器将生成目标文件(.o 文件)。
(7) -o $@
  • 指定输出文件的名称。
  • $@ 是自动变量,表示目标文件名,例如 main.o
(8) $<
  • 表示第一个依赖文件(即源文件)。
    • 在这个规则中,$< 会被替换为对应的 .c 文件,例如 main.c

工作原理

  1. 假设源文件是 main.c
  2. 执行规则时,变量的值如下:
    • $@main.o
    • $<main.c
    • dep_file.main.o.d
  3. 命令被展开为:
    gcc -Wall -O2 -g -Wp,-MD,.main.o.d -c -o main.o main.c
    
  4. 编译器执行以下操作:
    • 编译 main.c,生成目标文件 main.o
    • 同时生成依赖文件 .main.o.d,记录文件依赖关系。

示例依赖文件 .main.o.d

生成的 .main.o.d 可能包含如下内容:

main.o: main.c main.h utils.h
  • 表示 main.o 的生成依赖于 main.cmain.hutils.h
  • 如果其中任意文件发生更改,make 会自动重新编译 main.o

小结

这条规则的作用是.c 文件生成对应的 .o 文件,并记录依赖关系。通过使用变量和自动变量,规则具备了高度的灵活性和可扩展性,能够适应各种场景。


***流程完整梳理***

这个构建过程是一个典型的嵌套构建流程,分为顶层构建和递归子目录构建两部分。以下是整个构建的详细顺序解析:

1. 主构建流程从顶层的 Makefile 开始

执行 make 会触发顶层 Makefile 的目标:

顶层 Makefile 的关键目标:

  • all

    • 依赖于 start_recursive_build$(TARGET)
    • 按顺序执行:
      1. start_recursive_build:开始递归构建。
      2. $(TARGET):最终目标 test
  • start_recursive_build

    • 调用 make -C ./ -f $(TOPDIR)/Makefile.build,即在当前目录下使用 Makefile.build 进行递归构建。

2. 递归构建逻辑的执行

递归构建由 Makefile.build 负责。顶层的 obj-y 变量定义了哪些子目录需要递归构建(例如 display/unittest/)。

Makefile.build 的关键逻辑:

  • obj-y 的解析

    • obj-y 是顶层传递过来的变量,可能包括源文件和子目录:
      • 子目录通过 filter %/, $(obj-y) 提取。
      • 当前目录的目标文件通过 filter-out %/, $(obj-y) 提取。
    • 解析结果:
      • cur_objs:当前目录的 .o 文件。
      • subdir-y:子目录列表。
  • built-in.o 的生成

    • built-in.o 是当前目录的中间文件,由以下两部分链接而成:
      1. cur_objs:当前目录的 .o 文件。
      2. subdir_objs:子目录的 built-in.o 文件。
  • $(subdir-y) 的递归构建

    • 每个子目录调用 make -C $@ -f $(TOPDIR)/Makefile.build 进行递归构建。
    • 子目录 built-in.o 的生成逻辑与当前目录一致,直到所有递归层次完成。

3. 构建顺序展开

构建的实际顺序如下:

  1. 顶层 Makefile

    • 调用 start_recursive_build,启动递归构建。
    • 依次调用 Makefile.build 解析 obj-y
  2. 递归子目录构建

    • 对每个子目录 display/unittest/,依次递归构建:
      1. 进入子目录。
      2. 在子目录中调用 Makefile.build
        • 构建该子目录的目标文件和 built-in.o
        • 如果有嵌套子目录,继续递归(前提是子目录的Makefile文件中的变量obj-y添加了嵌套的子目)。
  3. 生成 built-in.o

    • 每个目录生成自己的 built-in.o 文件。
    • 逐层递归完成后,顶层目录最终获得完整的 built-in.o
  4. 链接最终目标

    • 回到顶层 Makefile
    • 使用顶层的 built-in.o 和链接选项生成最终的目标文件 test

4. 清理逻辑

Makefile 提供两个清理目标:

  • clean:删除 .o 文件和最终生成的目标。
  • distclean:删除 .o 文件、.d 文件和最终生成的目标。

这些清理目标可以随时调用,确保源目录恢复到干净状态。


5. 从 .c 文件生成 .o 文件的编译规则

下面的语句定义了如何从 .c 文件生成 .o 文件。

dep_file = .$@.d

%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

详情请见上文对这句关键代码的解读。

总结

构建的核心流程:

  1. 顶层 Makefile 开始构建。
  2. 递归调用 Makefile.build 处理每个子目录,生成 built-in.o
  3. 汇总所有 built-in.o,在顶层生成最终目标文件 test

递归构建的层次关系:

  • 顶层调用 -> 子目录调用 -> 嵌套子目录调用 -> 逐层返回。
  • 每层构建独立的中间文件 built-in.o,并将结果传递给上层。

这种嵌套构建方式具有良好的模块化特性,便于扩展和维护。

在新工程中的使用方法

将这个Makefile文件和Makefile.build文件读明白后,我们就知道了方法。
这个适合嵌套构建方式的目标生成。

首先下载源码 https://pan.baidu.com/s/1akuFrooBUPibgtPgPS2Xzw?pwd=jcat

然后把顶层的Makefile文件和Makefile.build文件复制到工程的根目录下,修改Makefile中的代码:

TARGET := test

为自己的目标名,比如mytest01

再把各个C文件放于每个子目录中,在Makefile文件中把所有的子目录添加,比如下面的代码:

obj-y += display/
obj-y += unittest/

然后在各个子目录书写Makefile文件,因为Makefile.build文件有包含当前目录中的Makefile文件的语句:

include Makefile

所以每个子目录需要书写Makefile文件。

每个子目录中的Makefile文件如下:

EXTRA_CFLAGS  := 
CFLAGS_file.o := 

obj-y += disp_manager.o
obj-y += framebuffer.o

你只需要根据实际的C文件名更改obj-y的变量的值就行了。当然,如果你子目录中还有子目录,那还需要在obj-y中添加子目录的子目录,比如下面的:

EXTRA_CFLAGS  := 
CFLAGS_file.o := 

obj-y += disp_manager.o
obj-y += framebuffer.o

obj-y += subsubdir/

这套规则会把你添加进去的所有子目录都递归完哦,每个子目录会生成一个built-in.o文件,返回给上层的调用使用,最终这一个个的built-in.o文件又会合成为一个大的built-in.o文件,然后由这个最上层的built-in.o文件生成目标文件。