提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


提示:以下是本篇文章正文内容,下面案例可供参考


1. 基本概念

先看看下述的简单例子:
  以下脚本将输出文件的 text section 定位在 0x10000, data section 定位在 0x8000000:

  1. SECTIONS
  2. {
  3. . = 0x10000;
  4. .text : { *(.text) }
  5. . = 0x8000000;
  6. .data : { *(.data) }
  7. .bss : { *(.bss) }
  8. }

解释一下上述的例子:
1️⃣ = 0x10000 : 把定位器符号置为 0x10000 (若不指定, 则该符号的初始值为 0)。
2️⃣ .text : {*(.text) } : 将所有 (*符号代表任意输入文件) 输入文件的.text section合并成一个.text section, 该 section 的地址由定位器符号的值指定, 即 0x10000
3️⃣ = 0x8000000 :把定位器符号置为 0x8000000
4️⃣ data : {*(.data) }:将所有输入文件的.data section合并成一个.data section, 该 section 的地址被置为 0x8000000
5️⃣ bss : {*(.bss) }:将所有输入文件的.bss section 合并成一个.bss section,该 section 的地址被置为 0x8000000 +.data section 的大小。
连接器每读完一个 section 描述后, 将定位器符号的值增加该 section 的大小. 注意: 此处没有考虑对齐约束。
  链接器是用来控制如何将多个输入文件拼凑成一个输出文件的,我们把输入文件内的 section 称为输入 section(input section), 把输出文件内的 section 称为输出 section(output section)。每个输出 section 都包括两个地址:VMA(Virtual Memory Address, 虚拟内存地址或程序地址空间地址) 和 LMA(Load Memory Address, 加载内存地址或进程地址空间地址), 通常 VMALMA 是相同的。但在嵌入式系统中, 经常存在加载地址和执行地址不同的情况: 比如将输出文件加载到开发板的 flash 中 (由 LMA 指定),而在运行时将位于 flash 中的输出文件复制到SDRAM 中 (由 VMA 指定),这个程序的搬移是用户自己实现的或者芯片厂商实现的(比如 stm32 是在 __main中实现的)。

2. GNU-ARM 默认的链接脚本

GNU for Arm 工具链包含一个默认的链接脚本文件,这个脚本是针对使用工具链的标准库和标准startup启动文件工程的。这个默认脚本中,不包含存储器分块的 MEMORY 指令,即只适用于单存储器的情况。可以通过下面的指令将默认的链接脚本输出到 default.lds 文件。该 lds 内容比较多,这里就不贴具体内容了,感兴趣的可以看一下。

arm-none-eabi-ld   —verbose   > default.lds

3. 一个链接脚本文件的构成

链接脚本文件主要包含两大部分(与 TI-DSP 中的 CMD 文件类似):将存储器分块的 MEMORY 和将输入段合成输出段并填充到分块后的存储器的 SECTION。可以没有 MEMORY 部分,但必须有 SECTION 部分。没有 MEMORY部分时,则暗示系统只有一种类型的存储器,比如只有 ROM 或者只有 RAM
  先来看将存储器分块的 MEMORY 部分,它是用来补充 SECTIONS 命令的,用来描述目标 CPU 中可用的内存区域。它是可选的,如果没有这个命令,LD 会认为 SECTIONS 描述的相邻的内存块之间有足够可用的内存。其实很容易理解但是却很少用,在 SECTIONS 中每个段的分布都没有考虑 ARM 能够寻址的地址区域中,ROM,RAM,FLASH 是不是连续的。如果不是连续的怎么办?1MEMORY1 就是设置各个区的起始位置,大小,属性的命令,在一个脚本中只能有一个。
  一个最简单的例子如下所示,将整个存储空间分为 FLASH 和 RAM 两个区域,这两个区域都可读可执行,但 FLASH 区域不能写。

  1. MEMORY
  2. {
  3. FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
  4. RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
  5. }

MEMORY 命令的语法是:

  1. MEMORY
  2. {
  3. name (attr) : ORIGIN = origin, LENGTH = len
  4. ...
  5. }

其中:

  • name:一个用户定义的名字,Linker 将在内部使用它,所以别把它和 SECTIONS 里用到的文件名,段名等搞重复了,它要求是独一无二的。
  • Attr :如同它的名字一样,这是内存段的属性描述(不区分大小写)。
    • `R’   Read-only sections.
    • `W’   Read/write sections.
    • `X’   Sections containing executable code.
    • `A’   Allocated sections.
    • `I’   Initialized sections.
    • `L’   Same as I.
    • `!’   Invert the sense of any of the following attributes.
  • ORIGIN:区域的起始地址。
  • LENGTH:区域的长度。

再来看 SECTION 部分,它是脚本文件中最重要的元素,不可缺省。它的作用就是用来描述输出文件的布局。 SECTIONS 命令的语法:

  1. SECTIONS
  2. {
  3. ...
  4. secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  5. { contents } >region :phdr =fill
  6. ...
  7. }

这些参数中,只有 secnamecontents 是必须的,其他都是可选的参数。也就说它的最简单的格式就是:

  1. SECTIONS
  2. {
  3. ...
  4. secname : {
  5. contents
  6. }
  7. ...
  8. }

secname 定义了段名,其实最开始就忽略了一个重要的因素,arm-none-eabi-ld 脚本需要描述输入和输出,而表面上一看却看不出来什么是输出什么是输入,其实 secnamecontents 就是描述这两个信息的参数。secname是输出文件的段,即输出文件有哪些段,而 contents 就是描述输出文件的这个段是由哪些文件的那些段构成的。明确这个了就不难理解为什么 SECTIONS 命令什么都可以不要就是不能没有这两个参数了。

  • secname:定义段,这里的段不一定必须是数据和代码,你可以创建专门用来存放字库和图片段。
  • contents :它的语法比较复杂。

    1️⃣ 你可以简单的把输入文件的段写到代码中,此时被列的目标文件中所有的代码都被链接到.data 中去了。
             .data : {main.o led.o}
    2️⃣ 进一步,指出哪些输入文件的哪些段写入到输出文件的哪个段。下面的脚本表示输入文件 main.o.data段和.text 段,加上led.o.data 段一起输出为输出文件中的.data 段 (当然了,实际应用中一般是不会将 text 段和 data 段输出到一个段的)。
        .data : {
            main.o(.data)
            main.o(.text) // 也可以这样写 main.o(.data .text) 或者 main.o(.data , .text)
            led.o(.data)
          }

    3️⃣ 还可以使用通配符指定所有文件的那些段写入到输出文件的哪个段,下面的脚本表示所有输入文件的.data段合并为输出文件中的.data 段。
             .data : { *(.data)}

下面是一个比较完整的链接脚本,其中命令语句 ENTRY 用于定义入口地址, =20000000;是定义 VMA 地址的,另外一个可以定义 VMA 地址的地方为.text 0x30000000 : AT (_eisr_vector),即紧跟在输出段名后面、冒号前面。同样有两个地方可以定义 LMA,例子中的两个 AT 处。ALIGN 用于定义对齐方式。命令语句*(EXCLUDE_FILE(isr.o) .text)表示除去 isr.o 的所有文件的.text 段。> SRAM>FLASH 表示运行时,将相应的输出段放在那块存储器。命令语句 KEEP(*(.isr_vector))是防止.isr_vector段在链接的过程中被优化掉的。

另外,脚本中还有一些赋值语句,比如_edata = .;,将_eisr_vector、_edata、_text、_etext 等称为特殊符号,它们虽然是在链接脚本中定义的,但是你可以在 C 程序中通过声明外部变量引用它们,比如:

  1. extern unsigned long _edata;
  2. extern unsigned long _etext;
  3. extern unsigned long _end;

链接器会在将程序最终链接成可执行文件的时候将这些特殊符号解析成正确的值。这些特殊符号主要用于 LMAVMA 不同时的代码搬运。

  1. MEMORY
  2. {
  3. FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
  4. RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
  5. }
  6. ENTRY(_start)
  7. SECTIONS
  8. {
  9. . = 0x20000000;
  10. .isr_vector : ALIGN(4)
  11. {
  12. KEEP(*(.isr_vector))
  13. isr.o(.text)
  14. . = ALIGN(4);
  15. _eisr_vector = .;
  16. }>FLASH
  17. .text 0x30000000 : AT (_eisr_vector)
  18. {
  19. _text = .;
  20. *(EXCLUDE_FILE(isr.o) .text)
  21. *(.rodata)
  22. . = ALIGN(4);
  23. _etext = .;
  24. } > FLASH
  25. .data :
  26. {
  27. _data = .;
  28. *(.data)
  29. . = ALIGN(4);
  30. _edata = . ;
  31. } > SRAM AT>_etext
  32. .bss (NOLOAD) :
  33. {
  34. _bss = . ;
  35. *(.bss)
  36. . = ALIGN(4);
  37. _ebss = . ;
  38. } > SRAM
  39. _end = . ;
  40. }

1. Makefile 简介

Makefile 是和 make 命令一起配合使用的,很多大型项目的编译都是通过 Makefile 来组织的,如果没有 Makefile,那很多项目中各种库和代码之间的依赖关系不知会多复杂。Makefile 的组织流程的能力如此之强,不仅可以用来编译项目, 还可以用来组织我们平时的一些日常操作. 这个需要大家发挥自己的想象力。

1.1 Makefile 主要的 5 个部分 (显示规则, 隐晦规则, 变量定义, 文件指示, 注释)

Makefile 基本格式如下:

target ... : prerequisites ... 
        command 
        ...

其中,
target - 目标文件, 可以是 Object File, 也可以是可执行文件
prerequisites - 生成 target 所需要的文件或者目标
command - make 需要执行的命令 (任意的 shell 命令), Makefile 中的命令必须以 [tab] 开头
几个注意点:
显示规则 :: 说明如何生成一个或多个目标文件 (包括 生成的文件, 文件的依赖文件, 生成的命令)
隐晦规则 :: make 的自动推导功能所执行的规则
变量定义 :: Makefile 中定义的变量
文件指示 :: Makefile 中引用其他 Makefile; 指定 Makefile 中有效部分; 定义一个多行命令
注释   :: Makefile 只有行注释 “#”, 如果要使用或者输出 “#” 字符, 需要进行转义, “#”

1.2 GNU make 的工作过程

  1. 读入主 Makefile (主 Makefile 中可以引用其他 Makefile)
  2. 读入被 include 的其他 Makefile
  3. 初始化文件中的变量
  4. 推导隐晦规则, 并分析所有规则
  5. 为所有的目标文件创建依赖关系链
  6. 根据依赖关系, 决定哪些目标要重新生成
  7. 执行生成命令

2. Makefile 初级语法

2.1 Makefile 规则

2.1.1 规则语法

规则主要有 2 部分:依赖关系 和 生成目标的方法.
语法有以下 2 种:(command 太长时,可以用 “\” 作为换行符)

target ... : prerequisites ... 
        command 
        ... 
target ... : prerequisites ; command 
        command 
        ...

2.1.2 规则中的通配符

  • :: 表示任意一个或多个字符
    ? :: 表示任意一个字符
    […] :: ex. [abcd] 表示 a,b,c,d 中任意一个字符, [^abcd]表示除 a,b,c,d 以外的字符, [0-9]表示 0~9 中任意一个数字
    ~ :: 表示用户的 home 目录

2.1.3 路径搜索

当一个 Makefile 中涉及到大量源文件时 (这些源文件和 Makefile 极有可能不在同一个目录中)。这时, 最好将源文件的路径明确在 Makefile 中,便于编译时查找。Makefile 中有个特殊的变量 VPATH 和关键字 vpath 都是完成这个功能的。指定了 VPATH 和 / 或 vpath 之后, 如果当前目录中没有找到相应文件或依赖的文件,Makefile 会到 VPATH 和 / 或 vpath 指定的路径中再去查找。VPATH 用于设定所有类型的文件的搜索路径,vpath 用于设定指定后缀类型文件的搜索路径。

VPATH 的使用方法:
VPATH = directorie1 : directorie2……
vpath 使用方法:
vpath :: 当前目录中找不到文件时, 就从中搜索
vpath :: 符合格式的文件, 就从中搜索
vpath :: 清除符合格式的文件搜索路径
vpath :: 清除所有已经设置好的文件路径


VPATH src:../parent-dir       


VPATH %.h ./header 


VPATH %.h 


VPATH

2.2 Makefile 中的变量

2.2.1 变量定义 (= or := or ?=)

OBJS = programA.o programB.o 
OBJS-ADD = $(OBJS) programC.o 

OBJS := programA.o programB.o 
OBJS-ADD := $(OBJS) programC.o 

OBJS ?= programA.o programB.o 
OBJS-ADD ?= $(OBJS) programC.o

其区别在于,

  • := 只能使用前面定义好的变量,
  • \= 可以使用后面定义的变量,
  • ?= 只有前面没有定义时才起作用。

示例如下:


OBJS2 = $(OBJS1) programC.o 
OBJS1 = programA.o programB.o 
all: 
        @echo $(OBJS2) 

$ make 
programA.o programB.o programC.o

OBJS2 := $(OBJS1) programC.o 
OBJS1 := programA.o programB.o 
all: 
        @echo $(OBJS2) 

$ make 
programC.o

OBJS2 ?=    programC.o 
OBJS1 = programA.o 
OBJS1 ?= programB.o 
all: 
@echo $(OBJS1) 
@echo $(OBJS2) 

$ make 
programA.o 
programC.o

2.2.2 变量替换


SRCS := programA.c programB.c programC.c 
OBJS := $(SRCS:%.c=%.o) 

all: 
    @echo "SRCS: " $(SRCS)
    @echo "OBJS: " $(OBJS) 


$ make 
SRCS:  programA.c programB.c programC.c 
OBJS:  programA.o programB.o programC.o

2.2.3 变量追加值 +=


SRCS := programA.c programB.c programC.c 
SRCS += programD.c 
all: 
    @echo "SRCS: " $(SRCS) 


$ make 
SRCS:  programA.c programB.c programC.c programD.c

2.2.4 变量覆盖 override

一般情况下,make 命令行中定义的变量优先级最高,它可以覆盖掉 makefile 脚本中定义的变量。如果想要
使 Makefile 中定义的变量比 make 命令参数中指定的变量优先级高,则需要附加 override 属性。
语法:
override <variable> = <value>
override <variable> := <value>
override <variable> += <value>


SRCS := programA.c programB.c programC.c 
all: 
        @echo "SRCS: " $(SRCS) 

$ make SRCS=nothing 
SRCS:    nothing 


override SRCS := programA.c programB.c programC.c 
all: 
        @echo "SRCS: " $(SRCS) 

$ make SRCS=nothing 
SRCS:    programA.c programB.c programC.c

2.2.5 目标变量

作用是使变量的作用域仅限于这个目标 (target),而不像之前例子中定义的变量,对整个 Makefile 都有效。

语法:
<target ...> :: <variable-assignment>
<target ...> :: override <variable-assignment>(override 作用参见 变量覆盖的介绍)


SRCS := programA.c programB.c programC.c 

target1: TARGET1-SRCS := programD.c 
target1: 
        @echo "SRCS: " $(SRCS) 
@echo "SRCS: " $(TARGET1-SRCS) 

target2: 
        @echo "SRCS: " $(SRCS) 
        @echo "SRCS: " $(TARGET1-SRCS) 


$ make target1 
SRCS:    programA.c programB.c programC.c 
SRCS:    programD.c 

$ make target2          <-- target2 中显示不了  $(TARGET1-SRCS) 
SRCS:    programA.c programB.c programC.c 
SRCS:

2.3 Makefile 命令前缀

Makefile 中书写 shell 命令时可以加 2 种前缀 @-, 或者不用前缀.
3 种格式的 shell 命令区别如下:

  • 不用前缀 :: 输出执行的命令以及命令执行的结果, 出错的话停止执行
  • 前缀 @ :: 只输出命令执行的结果, 出错的话停止执行
  • 前缀 - :: 命令执行有错的话, 忽略错误, 继续执行

all: 
        echo "没有前缀" 
        cat this_file_not_exist 
        echo "错误之后的命令"              <--  这条命令不会被执行 


$ make 
echo "没有前缀"                          <--  命令本身显示出来 
没有前缀                                        <--  命令执行结果显示出来 
cat this_file_not_exist 
cat: this_file_not_exist: No such file or directory 
make: *** [all] Error 1 


all: 
        @echo "没有前缀" 
        @cat this_file_not_exist 
        @echo "错误之后的命令"              <--  这条命令不会被执行 


$ make 
没有前缀                                                  <--  只有命令执行的结果,  不显示命令本身 
cat: this_file_not_exist: No such file or directory 
make: *** [all] Error 1 


all: 
        -echo "没有前缀" 
        -cat this_file_not_exist 
-echo "错误之后的命令"              <--  这条命令会被执行 


$ make 
echo "没有前缀"                          <--  命令本身显示出来 
没有前缀                                        <--  命令执行结果显示出来 
cat this_file_not_exist 
cat: this_file_not_exist: No such file or directory 
make: [all] Error 1 (ignored) 
echo "错误之后的命令"              <--  出错之后的命令也会显示 
错误之后的命令                            <--  出错之后的命令也会执行

2.4 伪目标

伪目标并不是一个 “目标 (target)”,不像真正的目标那样会生成一个目标文件。
典型的伪目标是 Makefile 中用来清理编译过程中中间文件的 clean 伪目标, 一般格式如下:

.PHONY: clean   <-- 这句没有也行, 但是最好加上 
clean: 
    -rm -f *.o

2.5 引用其他的 Makefile

语法: include <filename> (filename 可以包含通配符和路径),最好在 include 前面加一个 “-”


all: 
        @echo "主  Makefile begin" 
        @make other-all 
        @echo "主  Makefile end" 

include ./other/Makefile 


other-all: 
        @echo "other makefile begin" 
        @echo "other makefile end" 


$ ll 
total 20K 
-rw-r--r-- 1 wangyubin wangyubin    125 Sep 23 16:13 Makefile
-rw-r--r-- 1 wangyubin wangyubin    11K Sep 23 16:15 makefile.org      <--  这个文件不用管 
drwxr-xr-x 2 wangyubin wangyubin 4.0K Sep 23 16:11 other 
$ ll other/ 
total 4.0K 
-rw-r--r-- 1 wangyubin wangyubin 71 Sep 23 16:11 Makefile 

$ make 
主  Makefile begin 
make[1]: Entering directory `/path/to/test/makefile' 
other makefile begin 
other makefile end 
make[1]: Leaving directory `/path/to/test/makefile' 
主  Makefile end

2.6 查看 C 文件的依赖关系

写 Makefile 的时候,需要确定每个目标的依赖关系。GNU 提供一个机制可以查看 C 代码文件依赖那些文件,这样我们在写 Makefile 目标的时候就不用打开 C 源码来看其依赖那些文件了。前面已经有所涉及。 比如, 下面命令显示内核源码中 virt/kvm/kvm_main.c 中的依赖关系

$ cd virt/kvm/ 
$ gcc -MM kvm_main.c   
kvm_main.o:  kvm_main.c  iodev.h  coalesced_mmio.h  async_pf.h      <--  这句就可以加到  Makefile  中作为编译 
kvm_main.o  的依赖关系

2.7 make 退出码

Makefile 的退出码有以下 3 种:

  • 0 :: 表示成功执行
  • 1 :: 表示 make 命令出现了错误
  • 2 :: 使用了 “-q” 选项, 并且 make 使得一些目标不需要更新

2.8 指定 Makefile, 指定特定目标

默认执行 make 命令时,GNU make 在当前目录下依次搜索下面 3 个文件 “GNUmakefile”, “makefile”, “Makefile”。找到对应文件之后,就开始执行此文件中的第一个目标 (target)。如果找不到这 3 个文件就报错.
非默认情况下, 可以在 make 命令中指定特定的 Makefile 和特定的 目标.

$ make    –f    anyfile    clean

2.9 make 参数介绍

make 的参数很多, 可以通过 make –h/–help 去查看, 下面只介绍几个我认为比较有用的.

参数 含义
--debug[=FLAGS] 输出 make 的调试信息, FLAGS 可以是 a, b, v
-j [N] 同时运行的命令的个数, 也就是多线程执行 Makefile
-r 禁止使用任何隐含规则
-R 禁止使用任何作用于变量上的隐含规则
-B 假设所有目标都有更新, 即强制重编译
-f 指定任意文件执行
-C [directory] 改变当前目录,用于嵌套执行 make
-w 打印出 make 过程中的当前目录,用具记录嵌套执行的过程

2.10 Makefile 隐含规则

这里只列一个和编译 C 相关的。编译 C 时,<n>.o 的目标会自动推导为 <n>.c


main : main.o 
        gcc -o main main.o 


main : main.o 
        gcc -o main main.o 

main.o: main.c        <-- main.o  这个目标是隐含生成的 
        gcc -c main.c

2.11 隐含规则中的 命令变量 和 命令参数变量

2.11.1 命令变量, 书写 Makefile 可以直接写 shell 时用这些变量.

下面只列出一些 C 相关的,最好还是自己重新定义

变量名 含义
RM rm -f
AR ar
CC cc
CXX g++

2.11.2 命令参数变量

最好还是自己重新定义

变量名 含义
ARFLAGS AR 命令的参数
CFLAGS C 语言编译器的参数
CXXFLAGS C++ 语言编译器的参数

2.12 自动变量

Makefile 中很多时候通过自动变量来简化书写, 各个自动变量的含义如下:

自动变量 含义
$@ 目标集合
$% 当目标是函数库文件时, 表示其中的目标文件名
$< 第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标
$? 比目标新的依赖目标的集合
$^ 所有依赖目标的集合, 会去除重复的依赖目标
$+ 所有依赖目标的集合, 不会去除重复的依赖目标
$* 这个是 GNU make 特有的, 其它的 make 不一定支持

3.1 嵌套 Makefile

在 Makefile 初级语法中已经提到过引用其它 Makefile 的方法。 这里有另一种写法,且可以向引用的其它
Makefile 传递参数。
示例: (不传递参数, 只是调用子文件夹 other 中的 Makefile)


all: 
        @echo "主  Makefile begin" 
        @cd ./other && make 
        @echo "主  Makefile end" 


other-all: 
        @echo "other makefile begin" 
        @echo "other makefile end" 


$ ll 
total 28K 
-rw-r--r-- 1 wangyubin wangyubin    104 Sep 23 20:43 Makefile 
-rw-r--r-- 1 wangyubin wangyubin    17K Sep 23 20:44 makefile.org      <--  这个文件不用管 
drwxr-xr-x 2 wangyubin wangyubin 4.0K Sep 23 20:42 other 
$ ll other/ 
total 4.0K 
-rw-r--r-- 1 wangyubin wangyubin 71 Sep 23 16:11 Makefile 

$ make 
主  Makefile begin 
make[1]: Entering directory `/path/to/test/makefile/other' 
other makefile begin 
other makefile end 
make[1]: Leaving directory `/path/to/test/makefile/other' 
主  Makefile end

示例: (用 export 传递参数)


export VALUE1 := export.c        <--  用了  export,  此变量能够传递到  ./other/Makefile  中 
VALUE2 := no-export.c            <--  此变量不能传递到  ./other/Makefile  中 

all: 
        @echo "主  Makefile begin" 
        @cd ./other && make 
        @echo "主  Makefile end"

other-all: 
        @echo "other makefile begin" 
        @echo "VALUE1: " $(VALUE1) 
        @echo "VALUE2: " $(VALUE2) 
        @echo "other makefile end" 


$ make 
主  Makefile begin 
make[1]: Entering directory `/path/to/test/makefile/other' 
other makefile begin 
VALUE1:    export.c                <-- VALUE1  传递成功 
VALUE2:                                    <-- VALUE2  传递失败 
other makefile end 
make[1]: Leaving directory `/path/to/test/makefile/other' 
主  Makefile end

补充 export 语法格式如下:
• export variable = value
• export variable := value
• export variable += value

3.2 定义命令包

命令包有点像是个函数, 将连续的命令合成一条, 减少 Makefile 中的代码量, 便于以后维护.
语法:
define <command-name>
command
...
endef


define run-hello-makefile 
@echo -n "Hello" 
@echo " Makefile!" 
@echo "这里可以执行多条  Shell  命令!" 
endef 

all: 
        $(run-hello-makefile) 


$ make 
Hello Makefile! 
这里可以执行多条  Shell  命令!

3.3 条件判断

条件判断的关键字主要有 ifeq ifneq ifdef ifndef

<conditional-directive> 
<text-if-true> 
endif 


<conditional-directive> 
<text-if-true> 
else 
<text-if-false> 
endif

示例: ifeq 的例子, ifneq 和 ifeq 的使用方法类似, 就是取反


all: 
ifeq ("aa", "bb") 
        @echo "equal" 
else 
        @echo "not equal" 
endif 


$ make 
not equal

示例: ifdef 的例子, ifndef 和 ifdef 的使用方法类似, 就是取反


SRCS := program.c 

all: 
ifdef SRCS 
        @echo $(SRCS) 
else 
        @echo "no SRCS" 
endif 


$ make 
program.c

3.4 Makefile 中的函数

Makefile 中自带了一些函数, 利用这些函数可以简化 Makefile 的编写.
函数调用语法如下:
$(<function> <arguments>)

${<function> <arguments>}

  • 是函数名
  • 是函数参数

3.4.1 字符串函数

字符串替换函数: $(subst <from>,<to>,<text>)
功能: 把字符串<text> 中的 <from> 替换为 <to>
返回: 替换过的字符串


all: 
        @echo $(subst t,e,maktfilt)    <--  将 t 替换为 e 


$ make 
makefile

模式字符串替换函数: $(patsubst <pattern>,<replacement>,<text>)
功 能 : 查 找<text> 中 的 单 词 (单 词 以 “空 格”, “tab”, “换 行” 来 分 割 ) 是 否 符 合 <pattern>, 符 合 的 话 , 用 <replacement> 替代.
返回: 替换过的字符串


all: 
        @echo $(patsubst %.c,%.o,programA.c programB.c) 


$ make 
programA.o programB.o

去空格函数:$(strip <string>)
功能: 去掉 <string> 字符串中开头和结尾的空字符
返回: 被去掉空格的字符串值


VAL := "              aa    bb    cc " 

all: 
        @echo "去除空格前: " $(VAL) 
        @echo "去除空格后: " $(strip $(VAL)) 


$ make 
去除空格前:                  aa    bb    cc   
去除空格后:      aa bb cc

查找字符串函数: $(findstring <find>,<in>)
功能: 在字符串 <in> 中查找 <find> 字符串
返回: 如果找到, 返回 <find> 字符串, 否则返回空字符串


VAL := "              aa    bb    cc " 

all: 
        @echo $(findstring aa,$(VAL)) 
        @echo $(findstring ab,$(VAL)) 


$ make 
aa

过滤函数:$(filter <pattern...>,<text>)
功能: 以 <pattern> 模式过滤字符串 <text>, 保留 符合模式 <pattern> 的单词, 可以有多个模式
返回: 符合模式 <pattern> 的字符串


all: 
        @echo $(filter %.o %.a,program.c program.o program.a) 



$ make 
program.o program.a

反过滤函数: $(filter-out <pattern...>,<text>)
功能: 以 <pattern> 模式过滤字符串 <text>, 去除 符合模式 <pattern> 的单词, 可以有多个模式
返回: 不符合模式 <pattern> 的字符串


all: 
        @echo $(filter-out %.o %.a,program.c program.o program.a) 


$ make 
program.c

排序函数: $(sort <list>)
功能: 给字符串 <list> 中的单词排序 (升序)
返回: 排序后的字符串


all: 
        @echo $(sort bac abc acb cab) 


$ make 
abc acb bac cab

取单词函数: $(word <n>,<text>)
功能: 取字符串 <text> 中的 第<n>个单词 (n 从 1 开始)
返回: <text> 中的第<n>个单词, 如果<n><text> 中单词个数要大, 则返回空字符串


all: 
        @echo $(word 1,aa bb cc dd) 
        @echo $(word 5,aa bb cc dd) 
        @echo $(word 4,aa bb cc dd) 


$ make 
aa 

dd

取单词串函数: $(wordlist <s>,<e>,<text>)
功能: 从字符串<text>中取从<s>开始到<e>的单词串.<s><e>是一个数字.
返回: 从<s><e>的字符串


all: 
        @echo $(wordlist 1,3,aa bb cc dd) 
        @echo $(word 5,6,aa bb cc dd) 
        @echo $(word 2,5,aa bb cc dd) 



$ make 
aa bb cc 

bb

单词个数统计函数: $(words <text>)
功能: 统计字符串 <text> 中单词的个数
返回: 单词个数



all: 
        @echo $(words aa bb cc dd) 
        @echo $(words aabbccdd) 
        @echo $(words ) 


$ make
4 
1
0

首单词函数: $(firstword <text>)
功能: 取字符串<text> 中的第一个单词
返回: 字符串 <text> 中的第一个单词


all: 
        @echo $(firstword aa bb cc dd) 
        @echo $(firstword aabbccdd) 
        @echo $(firstword ) 


$ make 
aa 
aabbccdd

3.4.2 文件名函数

取目录函数: $(dir <names...>)
功能: 从文件名序列 <names> 中取出目录部分
返回: 文件名序列 <names> 中的目录部分


all: 
        @echo $(dir /home/a.c ./bb.c ../c.c d.c) 


$ make 
/home/ ./ ../ ./

取文件函数:$(notdir <names...>)
功能: 从文件名序列 <names> 中取出非目录部分
返回: 文件名序列 <names> 中的非目录部分


all: 
        @echo $(notdir /home/a.c ./bb.c ../c.c d.c) 


$ make 
a.c bb.c c.c d.c

取后缀函数: $(suffix <names...>)
功能: 从文件名序列 <names> 中取出各个文件名的后缀
返回: 文件名序列 <names> 中各个文件名的后缀, 没有后缀则返回空字符串


all: 
        @echo $(suffix /home/a.c ./b.o ../c.a d) 


$ make 
.c .o .a

取前缀函数: $(basename <names...>)
功能: 从文件名序列 <names> 中取出各个文件名的前缀
返回: 文件名序列 <names> 中各个文件名的前缀, 没有前缀则返回空字符串


all: 
        @echo $(basename /home/a.c ./b.o ../c.a /home/.d .e) 


$ make 
/home/a ./b ../c /home/

加后缀函数: $(addsuffix <suffix>,<names...>)
功能: 把后缀 <suffix> 加到 <names> 中的每个单词后面
返回: 加过后缀的文件名序列


all: 
        @echo $(addsuffix .c,/home/a b ./c.o ../d.c) 



$ make 
/home/a.c b.c ./c.o.c ../d.c.c

加前缀函数: $(addprefix <prefix>,<names...>)
功能: 把前缀 <prefix> 加到 <names> 中的每个单词前面
返回: 加过前缀的文件名序列


all: 
        @echo $(addprefix test_,/home/a.c b.c ./d.c) 


$ make 
test_/home/a.c test_b.c test_./d.c

连接函数: $(join <list1>,<list2>)
功能: <list2> 中对应的单词加到 <list1> 后面
返回: 连接后的字符串


all: 
        @echo $(join a b c d,1 2 3 4) 
        @echo $(join a b c d,1 2 3 4 5) 
        @echo $(join a b c d e,1 2 3 4) 


$ make 
a1 b2 c3 d4 
a1 b2 c3 d4 5 
a1 b2 c3 d4 e

3.4.3 foreach

语法: $(foreach <var>,<list>,<text>)


targets := a b c d 
objects := $(foreach i,$(targets),$(i).o) 

all: 
        @echo $(targets) 
        @echo $(objects) 


$ make 
a b c d 
a.o b.o c.o d.o

3.4.4 if

这里的 if 是个函数, 和前面的条件判断不一样, 前面的条件判断属于 Makefile 的关键字
语法:
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)


val := a 
objects := $(if $(val),$(val).o,nothing) 
no-objects := $(if $(no-val),$(val).o,nothing) 

all: 
        @echo $(objects) 
        @echo $(no-objects) 


$ make 
a.o 
nothing

3.4.5 origin - 判断变量的来源

语法:$(origin <variable>)
返回值有如下类型:

类型 含义
undefined 没有定义过
default 是个默认的定义, 比如 CC 变量
environment 是个环境变量, 并且 make 时没有使用 -e 参数
file 定义在 Makefile 中
command line 定义在命令行中
override 被 override 重新定义过
automatic 自动化变量

val-in-file := test-file 
override val-override := test-override 

all: 
        @echo $(origin not-define)        
        @echo $(origin CC)                        
        @echo $(origin PATH)                  
        @echo $(origin val-in-file)        
        @echo $(origin val-in-cmd)        
        @echo $(origin val-override) 
        @echo $(origin @)                          


$ make val-in-cmd=val-cmd 

undefined 
default 
environment 
file 
command line 
override 
automatic

3.4.7 shell

语法: $(shell <shell command>)
它的作用就是执行一个 shell 命令, 并将 shell 命令的结果作为函数的返回.

3.4.8 make 控制函数

产生一个致命错误: $(error <text ...>)
功能: 输出错误信息, 停止 Makefile 的运行


all: 
        $(error there is an error!) 
        @echo "这里不会执行!" 


$ make 
Makefile:2: *** there is an error!.    Stop.

输出警告: $(warning <text ...>)
功能: 输出警告信息, Makefile 继续运行


all: 
        $(warning there is an warning!) 
        @echo "这里会执行!" 


$ make 
Makefile:2: there is an warning! 
这里会执行!

3.5 Makefile 中一些 GNU 约定俗成的伪目标

如果有过在 Linux 上从源码安装软件的经历的话, 就会对 make clean, make install 比较熟悉.
像 clean, install 这些伪目标, 广为人知, 不用解释就大家知道是什么意思了.
下面列举一些常用的伪目标, 如果在自己项目的 Makefile 合理使用这些伪目标的话, 可以让我们自己的 Makefile
看起来更专业, 呵呵 😃

伪目标 含义
all 所有目标的目标,其功能一般是编译所有的目标
clean 删除所有被 make 创建的文件
install 安装已编译好的程序,其实就是把目标可执行文件拷贝到指定的目录中去
print 列出改变过的源文件
tar 把源程序打包备份. 也就是一个 tar 文件
dist 创建一个压缩文件, 一般是把 tar 文件压成 Z 文件. 或是 gz 文件
TAGS 更新所有的目标, 以备完整地重编译使用
check 或 test 一般用来测试 makefile 的流程

实现的功能描述如下:假设一个工程的工根目录中存在多个不同深度的子目录,子目录用 subxxxx 表示,
Subx 为第一级子目录,subxx 为第二集子目录,以此类推。比如,第一级子目录包括 sub1、sub2、sub3,sub1
下面的子目录为 sub1x,sub2 下面的子目录为 sub2x,sub3 下面的子目录为 sub3x;同理,第三级子目录 sub231
表示 sub2 子目录底下的 sub23 子目录下的子目录。只要工程所有的源文件都放在这些目录中(即所有源文件都
包含在工程目录内),则就可以使用该 makefile 进行处理。
第一版的代码如下(若要在处理过程中显示各种信息,可以通过 echo 指令实现):


MKDIR = -mkdir 
RM = rm 
RMFLAGS = -fr 

NM_TARGET = a_default 


DIR_OBJS = objs 
DIR_BINS = bins 
DIR_DEPS = deps 
DIR_CLEAN = $(DIR_OBJS) $(DIR_BINS) $(DIR_DEPS) 



NOTHING = 
CHILD_DIRS = $(shell ls -R | grep :$) 
CHILD_DIRS := $(patsubst %:,%$(NOTHING),$(CHILD_DIRS)) 
INC_PATH := $(patsubst .%,-I.%,$(CHILD_DIRS)) 
CHILD_DIRS := $(filter ./%,$(CHILD_DIRS)) 
CHILD_DIRS := $(patsubst ./%,$(NOTHING)%,$(CHILD_DIRS)) 



vpath %.h $(CHILD_DIRS) 
vpath %.c $(CHILD_DIRS) 
vpath %.d $(DIR_DEPS) 
vpath %.o $(DIR_OBJS) 
vpath %.out $(DIR_BINS) 
vpath %.bin $(DIR_BINS) 
vpath %.hex $(DIR_BINS) 
vpath %.elf $(DIR_BINS) 




SRCS_EXCLUDE = 
SRCS := $(shell ls -R | grep .c$) 
SRCS := $(filter %.c,$(SRCS)) 
SRCS := $(filter-out $(SRCS_EXCLUDE),$(SRCS)) 

OBJS = $(notdir $(SRCS:.c=.o)) 
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS)) 
BINS = $(NM_TARGET).elf 
BINS :=$(addprefix $(DIR_BINS)/, $(BINS)) 
DEPS = $(notdir $(SRCS:.c=.d)) 
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS)) 
MAPS := $(NM_TARGET).map 
MAPS := $(addprefix $(DIR_BINS)/, $(MAPS)) 


PATH_TOOLCHAIN = 
CROSS_COMPILE ?= arm-none-eabi- 
OPTS_CPU ?= -mcpu=cortex-m3 -mthumb -nostartfiles 
OPTS_DIAG ?= -Wall -Wextra 
OPTS_CLIB ?= --specs=nano.specs 
OPTS_LDS ?= -T sections.lds 
OPTS_INC1 ?= 
OPTS_INC := $(INC_PATH) $(OPTS_INC1) 
OPTS_LIB ?= 
OPTS_DEF ?= 
debug ?= y 
ifeq ($(debug), y) 
OPTS_DEBUG ?= -g2 
OPTS_OPTIM ?= -O2 
else 
OPTS_DEBUG ?= 
OPTS_OPTIM ?= -Os 
endif 

USER_CCFLAGS = 
USER_LDFLAGS = -Wl,-Map,$(MAPS) 

TOOL_CHAIN = $(PATH_TOOLCHAIN)$(CROSS_COMPILE) 
CC := $(TOOL_CHAIN)gcc 
LD := $(TOOL_CHAIN)g++ 
OBJCP := $(TOOL_CHAIN)objcopy 
OBJDP := $(TOOL_CHAIN)objdump 
RDELF := $(TOOL_CHAIN)readelf 
OBJSZ := $(TOOL_CHAIN)size 
CCFLAGS  :=  $(OPTS_CPU)  $(OPTS_DIAG)  $(OPTS_CLIB)  $(OPTS_DEBUG)  $(OPTS_OPTIM)  $(OPTS_INC) 
$(OPTS_DEF) $(USER_CCFLAGS) 
LDFLAGS  :=  $(OPTS_CPU)  $(OPTS_DIAG)  $(OPTS_CLIB)  $(OPTS_DEBUG)  $(OPTS_OPTIM)  $(OPTS_INC) 
$(OPTS_DEF) $(OPTS_LDS) $(OPTS_LIB) $(USER_LDFLAGS) 
IHEX = $(NM_TARGET).hex 
IHEX := $(addprefix $(DIR_BINS)/, $(IHEX)) 
IBIN = $(NM_TARGET).bin 
IBIN := $(addprefix $(DIR_BINS)/, $(IBIN)) 
DISASSEM = $(NM_TARGET).s 
DISASSEM := $(addprefix $(DIR_BINS)/, $(DISASSEM)) 


.PHONY: ALL just_compile just_link rebuild clean 
ALL: $(BINS) 
  @echo "All needed have been created!" 


ifeq ("$(wildcard $(DIR_DEPS))", "") 
DEPS_DIR_DEPS := $(DIR_DEPS) 
endif   
ifneq ($(MAKECMDGOALS), clean) 
-include $(DEPS) 
endif 




$(DIR_CLEAN): 
  @echo "Creating directory $@ ..." 
  $(MKDIR) $@ 
  @echo $(SRCS) 


$(BINS): $(DIR_BINS) $(OBJS) 
  $(LD) $(LDFLAGS) -pipe -o $@ $(filter %.o, $^) 

$(DIR_OBJS)/%.o: $(DIR_OBJS) %.c 

  $(CC) $(CCFLAGS) -pipe -o $@ -c $(filter %.c, $^) 

$(DIR_DEPS)/%.d: $(DEPS_DIR_DEPS) %.c 
  @echo "Making $@ ..." 
  @set -e;\ 
  $(RM) $(RMFLAGS) $@ ; \ 
  $(CC) -MM $(INC_PATH) $(filter %.c, $^) > $@.$$$$ ; \ 
  sed 's,\($*\)\.o[ :]*,objs/\1.o $@: ,g' < $@.$$$$ > $@ ; \ 
  $(RM) $(RMFLAGS) $@.$$$$   

just_compile: $(OBJS) 
  @echo "Just compiling $(filter %.o, $^) ..." 

just_link: $(BINS) 
  @echo "Just linking $(filter %.o, $^) ..." 

mk_ihex: $(IHEX) 
  @echo intel hex format file : $^ created...   
$(IHEX): $(DIR_BINS) $(BINS) 
  $(OBJCP) -g -S -O ihex $(filter %.elf, $^) $@ 

mk_bin: $(IBIN) 
  @echo intel hex format file : $^ created...   
$(IBIN): $(DIR_BINS) $(BINS) 
  $(OBJCP) -g -S -O binary $(filter %.elf, $^) $@ 

mk_size: $(DIR_BINS) $(BINS) 
  $(OBJSZ) -B $(BINS) 

mk_deassem: $(DIR_BINS) $(BINS) 
  $(OBJDP) -D -S $(BINS) > $(DISASSEM) 

clean: 
  $(RM) $(RMFLAGS) $(DIR_CLEAN)

可以实现以下几种功能:

  1. 对单个. c 源文件进行编译操作;
  2. 对多个. c 源文件进行编译操作;
  3. 对整个工程所有的. c 源文件进行编译操作;
  4. 仅对整个工程进行链接操作;
  5. 对整个工程进行增量式编译、链接操作,也就是只编译修改了的源文件,然后进行链接 (rebuild modified);
  6. 对整个工程进行强制编译、链接操作,而不管源文件是否已经改动 (rebuild All);
  7. 清除整个编译、链接过程生成的所有文件。
  8. 生成 hex 或者 bin 文件,可以是编译、链接,然后调用生成 hex/bin,也可以直接调用生成 hex/bin
  9. 反汇编
  10. 代码跟踪

在不考虑配置的情况下,实现以上功能所需要的命令行调用如下:

编号 功能 调用方式(强制编译时启用[-B]选项)
1 编译 LED.c 文件 make just_compile SRCS=LED.c [-B]
2 编译 LED.c 和 Cordic.c make just_compile SRCS=”LED.c Cordic.c” [-B]
3 编译整个工程的*.c 文件 make just_compile [-B]
4 仅链接整个工程 make just_link [-B]
5 增量式编译、链接整个工程 make
6 强制编译、链接整个工程 make –B
7 清理 make clean
8 生成 hex 或者 bin 文件 make mk_ihex/mk_bin [-B]
#-B 表示重新编译链接然后生成 hex/bin
9 反汇编 make mk_deassem [-B]
10 0x2000000E
在那个源文件的什么地方
make adr2line P_ADDR=2000000E [-B]

以上功能中,8~10 功能有点特殊,如果在编译链接之前直接执行它们,则会先编译、链接,然后再对生成
的目标文件执行 8-10 的功能。
对于存在非工程目录下的库 (源代码方式提供) 的情况,这里提供两个办法:

  1. 在库的下面也写一个 makefile,然后用工程的 makefile 去调用库下面的 makefile。
  2. 将库先通过 arm-none-eabi-ar 打包成. a(静态库)或者. so(动态库),然后通过 - L 和 - l 选项将库包含到链接
    时的命令行中
    https://blog.csdn.net/fdcp123/article/details/114589045?spm=1001.2014.3001.5502