【嵌入式】Makefile 学习笔记记录 | 嵌入式Linux

文章目录


前言

开发板平台:飞凌嵌入式ElfBoard ELF-1

参考视频和资料:飞凌嵌入式ElfBoard ELF-1软件学习书册

韦东山:https://www.bilibili.com/video/BV1kk4y117Tu?p=6&vd_source=3018264d4331e8fc267f9d68c24ee20f

一、Makefile的引入------最简单的gcc编译过程

keil,mdk,avr这些工具全自动编译的内部机制依然是makefile

这里我们先随便写两个C文件(a.c 和b.c),用最传统的gcc编译一下:

a.c:

c 复制代码
#include <stdio.h>
void funB();
int main() {
	funB();
	return 0;
}

b.c:

c 复制代码
#include <stdio.h>
void funB() {
	printf("hello B!\n");
}

然后我们上传到虚拟机上进行编译:

bash 复制代码
gcc -o test a.c  b.c

然后执行

bash 复制代码
./test

从a.c b.c到可执行文件test经历了什么?:

总的来说就是四步:预处理,编译,汇编,链接 (一般来说前三步统称为编译)

a.c ->a.s->a.o

b.c ->b.s->b.o

最后两个.o链接在一起生成可执行文件test

我们可以在gcc命令后加上 -v看到编译链接的完整过程:

bash 复制代码
gcc -o test a.c  b.c -v

具体内容很多,就不一一截图了:

这样gcc有个很明显的缺点:

不论a.c b.c有没有被更改过,每次gcc都会重新编译链接所有的C文件。有的时候我们只修改了很小一部分的C文件,但此时我们gcc会全部重新编译,这很耽误时间。

makefile就能解决这个问题。

它可以把刚刚gcc这个过程解构成一系列小的编译过程:

bash 复制代码
gcc -c -o a.o a.c
gcc -c -o b.o b.c
gcc -o test  a.o b.o

这三句命令的含义如下:

gcc -c -o a.o a.c:这个命令使用 GCC 编译器将源文件 a.c 编译成目标文件 a.o。具体解释如下:

-c 选项表示只进行编译,而不进行链接,生成目标文件。

-o a.o 选项指定输出文件的名称为 a.o。

a.c 是源文件的名称。

gcc -c -o b.o b.c:这个命令与第一条类似,将源文件 b.c 编译成目标文件 b.o。

gcc -o test a.o b.o:这个命令使用 GCC 编译器将目标文件 a.o 和 b.o 进行链接,生成一个名为 test 的可执行文件。

-o test 选项指定输出文件的名称为 test。

a.o b.o 是链接的目标文件。

综合起来,这三条命令用于分别编译两个源文件 a.c 和 b.c,然后将生成的目标文件 a.o 和 b.o 链接在一起,形成一个名为 test 的可执行文件。
makefile如何自己的这些文件被修改了?:

在第一行 判断到a.c比a.o新,就说明a.c被更新过了,就可以重新编译

在第二行 判断到b.c比b.o新,就说明b.c被更新过了,就可以重新编译

在第三行 判断到a.o b.o比test新,就说明a.o b.o被更新过了,就可以重新编译

二、Makefile的规则

makefile的基本语法格式为:


目标文件:依赖文件

TAB 命令


当依赖比目标的时候 or 该目标文件直接不存在的时候,就会执行命令

我们在刚刚的文件夹中新建一个makefile:

然后执行make两此看一下效果:

bash 复制代码
make
make

第一次Make正确得运行了我们makefile里面写的所有语句

第二次make因为没有检测到依赖项更新,所以并没有重新编译

我们再做一个小实验,即只修改a.c ,只修改b.c 以及同时修改a.c和b.c再分别执行make的效果:

可以看到,我们修改了啥,make时也只会重新执行依赖项变动的语句,没有修改的部分不会重新执行命令。

(touch 命令是用来更新文件的访问和修改时间戳的工具,如果文件不存在,则会创建一个空白文件。它不会修改文件的内容,只是更新文件的元数据。所以,touch 命令不会改变文件的内容,只是改变文件的时间戳相当于变相修改了文件

三、Makefile的语法

3.1、通配符

我们再增加一个c.c文件,里面有一个函数func():

c 复制代码
#include <stdio.h>
void funC() {
	printf("hello C!\n");
}

再把a.c里的内容改一下,把函数func加上去:

c 复制代码
#include <stdio.h>
void funB();
void funC();
int main() {
	funB();
	funC();
	return 0;
}

再使用通配符对makefile里面的语句进行一定的修改:

1)$^表示所有的依赖文件

2)使用 \< **表示第一个依赖文件** (源文件),使用 @ 表示目标文件

这样就可以把gcc -o -c a.o a.c 和gcc -o -c b.o b.c合并成一句话了

我们在虚拟机上看一下效果:

这里make没啥问题,正常工作了。

3.2、假想目标 .phony

phony,英语单词,主要用作形容词、名词,作形容词时译为"假的,欺骗的"

我们首先补一下make clean 的内容 我们修改一下makefile中的内容:

增加了一句话:

bash 复制代码
clean:
	rm *.o test

Makefile 中还包含了一个 clean 目标,用于清理生成的目标文件和可执行文件:

当执行 make clean 时,它将删除当前目录下的所有 .o 文件和 test 可执行文件

我们上虚拟机实验一下:

发现所有的.o文件和可执行文件test均被清除了,这就是make clean的作用

在讲完clean之后,正式引出我们的makefile的一个语法:


make [目标]

比如说make clean,就会寻找makefile里面名为clean的目标,执行其tab后的命令
如果make后没有加东西(直接就是make),那么系统会默认自动执行第一个目标的命令,在上文中,就会自动执行test目标的命令。


在这个规则下有一个bug,就是遇到同名文件 的情况(这里以clean同名文件为例子),**因为在makefile里并没有clean的依赖项文件。**之前是因为没有clean这个东西,所以执行会很顺利。

我们在虚拟机上新建一个名为clean的文件,我们先make,再执行make clean。此时根据makefile的规则,系统没有检测到clean发生改变(因为存在同名文件clean),那么执行make clean时就不会正确执行:

解决办法:我们把clean设置为假想目标就能解决这个问题

我们稍微改一下makefile:

这里使用了 .PHONY 目标,它告诉 Make 这个目标不对应真实的文件名。这样做的目的是防止与同名的实际文件冲突,同时确保即使存在同名文件,make clean 也能正常执行。

这回我们再实验一下就对了:

3.3、即时变量 延时变量

我们写一个新的makefile:

这个 Makefile 定义了两个变量 A 和 B,然后在 all 目标中使用了这两个变量。以下是中文解释:

A := abc 表示定义了一个变量 A,其值为 "abc"。:= 是一种赋值方式,表示覆盖先前的值。(即刻确定 )

B = 123 表示定义了一个变量 B,其值为 "123"。= 也是一种赋值方式,但是它是延迟赋值,即在使用变量时才会展开。(延迟确定

all 目标中使用了 echo (A) 和 echo (B) 分别输出变量 A 和 B 的值。在 Makefile 中,$() 用于引用变量的值echo 是一个在命令行中常用的命令,用于将文本输出到标准输出设备(通常是终端)。在类Unix系统(如Linux)和类似的命令行环境中,echo 命令通常用于显示文本。

我们make一下看看效果:

我们可以在echo前面加上@ :

这样就不会打印命令本身了:

到这里我们还看不出即时变量和延时变量的区别,我们再makefile添一些代码:

A是即可确定,但此时C还并没被赋值,所以这时会打印空

B是使用时才确定,所以不会为空

我们实验一下:

我们如果更改C=abc的位置呢:

最后结果还是不影响:(即C位置不影响B,系统会对makefile整体进行分析)

最后我们再介绍两种符号:+= ?=

在makefile里我们做如下更改:

make一下看看效果:

1)+= 运算符:用于追加值到变量。使用 += 时,它会将右侧的值追加到已经存在的变量值的末尾。如果变量之前未定义,则行为类似于简单的赋值。

2)?= 运算符:用于给变量赋值,但仅在该变量之前未定义时才赋值。使用 ?= 时,它会检查变量是否已经定义,如果已定义则不进行赋值,否则将变量赋予指定的默认值。

总结:

1):= 即时变量

2)= 延时变量

3)?=延时变量 是第一次定义才起效果

4)+= 附加 它是即时变量还是延时变量 取决于前面

四、Makefile的函数

4.1、foreach

foreach 是GNU Make中的一个函数,用于进行循环迭代。其基本语法如下:

bash 复制代码
$(foreach var, list, text)

var: 循环中的临时变量,表示每次迭代中的当前元素。

list: 要迭代的列表,可以是以空格分隔的多个元素。

text: 在每次迭代中对var进行操作的文本块。

我们写一个新的makefile:

A=a b c: 定义了一个变量A,包含三个元素a、b和c。

B=(foreach f, (A), $(f).o): 使用foreach循环,将A中的每个元素加上.o后缀,并将结果存储到变量B中。

all:: 定义了一个目标规则名为all。@echo B = $(B): 在执行all目标时,打印出变量B的值。

最后结果:

4.2、filter

filter 是GNU Make中的一个函数,用于从列表中筛选出符合指定条件的元素。其基本语法如下:

bash 复制代码
$(filter pattern..., text)

pattern...: 一个或多个模式,用于指定筛选条件。可以包含通配符 %。

text: 要进行筛选的文本块,通常是一个以空格分隔的元素列表。

filter函数会返回text中符合给定模式的元素列表。模式之间使用空格分隔。

filter函数还支持反向操作,使用filter-out可以筛选出不符合指定条件的元素。例如:

bash 复制代码
C = $(filter-out a%, $(A))

在这个例子中,匹配A中不以字母'a'开头的元素。

我们写一个新的Makefile:

C = a b c d/: 定义了一个变量C,包含四个元素a、b、c和d/。

D = (filter %/, ©): 使用filter函数,从C中筛选出以'/'结尾的元素,存储到变量D中 。在这个例子中,D的值为d/。

E = (filter-out %/, ©): 使用filter-out函数,从C中筛选出不以'/'结尾的元素,存储到变量E中。在这个例子中,E的值为a b c。

最后结果:

4.3、wildcard

wildcard 是 GNU Make 中的一个函数,用于匹配文件名模式,返回匹配到的文件列表。其基本语法如下:

bash 复制代码
$(wildcard pattern)

pattern: 文件名模式,可以包含通配符 * 和 ?。
wildcard 函数会返回符合指定文件名模式的文件列表 。通常,这个函数用于获取文件列表并将其赋值给一个变量,以便在 Makefile 中进一步处理这些文件。

我们写一个新的makefile:

files=$(wildcard *.c):使用 wildcard 函数匹配当前目录下所有以 .c 结尾的文件 ,并将结果存储到变量 files 中。

最后结果:

扩展应用:可以用检查目录下面哪些文件是真实存在的

定义了一个变量 files2 包含了一组源文件名,然后使用 wildcard 函数来获取这些文件的实际存在 的文件列表并存储在变量 files3 中。(注意d.c e.c是不存在的)

最后结果:

4.4、patsubst

patsubst 是在 Makefile 中用来替换模式的函数之一。它用于将一个字符串中符合指定模式的部分替换成另一个模式。基本语法是:

bash 复制代码
$(patsubst pattern,replacement,text)

pattern 是要匹配的模式,可以包含 % 通配符,表示零个或多个字符。

replacement 是替换的模式。

text 是要进行替换操作的原始文本。

我们改一下makefile:

Makefile 中,定义了一个变量 files2 包含了一组源文件名,其中包含一个不是以 .c 结尾的文件 abc。接着,使用 patsubst 函数将每个源文件名的扩展名从 .c 替换为 .d,并将结果存储在变量 dep_files 中。

最后结果:

五、Makefile升级

5.1、包含头文件在内的依赖关系(自动生成依赖文件)

我们使用前面一样的程序来进行实验:

a.c:

c 复制代码
#include <stdio.h>
void funB();
void funC();
int main() {
	funB();
	funC();
	return 0;
}

b.c:

c 复制代码
#include <stdio.h>
void funB() {
	printf("hello B!\n");

}

c.c:(进行了一点点修改)

c 复制代码
#include <stdio.h>
#include "c.h"
void funC() {
	printf("This is C=%d\n",C);
}

新建一个c.h

c 复制代码
#define C 1 

然后我们make加执行一下:

我们这里做一个小小的改动,把头文件中define的数从1改为2:

c 复制代码
#define C 2 

然后我们再make一下看看效果:

我们可以看到make并没有顺利执行,而且C依然为1 ,说明还是有点小问题

那这是因为什么原因导致的呢:?

因为我们的c.c是依赖于c.h的,但是makefile中并没有把这种依赖关系写出来,所以makefile也不知道c.h更新了。

我们再makefile中重新添加了:

bash 复制代码
c.o:c.c c.h

然后我们再make一下就对了:

但是这样做是不可能的,在大型项目里面,我们每一个C文件几乎都有头文件,我们不可能手动把这些头文件的依赖关系一行行在makefile全部都写出来,我们需要自动去生成这些规则。


在讲解今天的正式内容前,我们先介绍三个查看依赖关系的命令:

bash 复制代码
gcc -M c.c

上述命令会使用 gcc 编译器并使用 -M 选项来生成 c.c 源文件的依赖关系,是立刻打印出来

bash 复制代码
gcc -M -MF c.d c.c

这个命令也会生成 c.c 源文件的依赖关系,但是通过 -MF c.d 选项指定了输出文件为 c.d。这意味着生成的依赖关系将被保存到 c.d 文件中 ,而不是输出到标准输出流。

bash 复制代码
gcc -c -o c.o c.c -MD -MF c.d

这个命令用于编译 c.c 源文件为目标文件 c.o。参数 -c 表示编译成目标文件,-o c.o 指定输出文件为 c.o。-MD 选项用于生成 .d 文件,-MF c.d 则指定生成的依赖关系文件为 c.d。这意味着除了生成目标文件 c.o 外,还会生成一个描述依赖关系的文件 c.d。

按照我们刚刚介绍的gcc依赖关系规则,我们修改一下makefile:

(ls 命令用于列出目录中的文件和子目录。而 ls -a 命令也会列出目录中所有的文件和子目录,包括以 . 开头的隐藏文件或隐藏目录,这些文件或目录在普通的 ls 命令中是不可见的。)

从这里我们可以看到这些依赖关系被自动生成出来了

刚刚makefile里的这句话就可以不用写了:

bash 复制代码
c.o:c.c c.h

我们再改一下makefile:

这一行使用了 Makefile 中的函数 patsubst,它用于替换模式。具体来说,% 是一个通配符,表示每个目标文件名。 ( p a t s u b s t (patsubst %,.%.d, (patsubst(objs)) 的作用是将目标文件列表中的每个目标文件名(a.o, b.o, c.o)替换成相应的依赖关系文件名(.a.o.d, .b.o.d, .c.o.d)。

可以看到我们的依赖文件已经被生成出来了。

接下来我们再改一改,把这些检测到的依赖文件包含进去:

bash 复制代码
objs=a.o b.o c.o
dep_files:=$(patsubst %,.%.d,$(objs))
dep_files:=$(wildcard $(dep_files))

这里定义了目标文件列表 objs 和依赖关系文件列表 dep_files。使用 patsubst 函数将目标文件列表转换为相应的依赖关系文件列表,然后通过 wildcard 函数获取实际存在的依赖关系文件。

bash 复制代码
ifneq ($(dep_files),)
include $(dep_files)
endif

这里使用条件语句检查是否存在依赖关系文件列表 dep_files,如果存在,则通过 include 关键字包含这些依赖关系文件。这样,Make 就能够了解源文件之间的依赖关系,从而在需要重新编译时执行相应的规则。

我们这时候试一下修改c.h里的宏定义如下:

c 复制代码
#define C 3 

这时候我们再make就不会再出现之前的情况了:即宏定义改了make之后打印出来依然不变的情况

最后结果:

我们可以看到改为了3,make后就自动识别了头文件的依赖关系,打印出来了3,实现了我们最初的目标:自动识别依赖关系(自动生成依赖文件)。

5.2、添加CFLAGS

我们在makefile里添加几条命令:

CFLAGS=-Werror 设置了一个编译选项,即启用了 -Werror。这个选项的含义是将所有警告视为错误,即编译过程中如果产生了任何警告,就会导致编译失败。这样做的目的是强制要求代码中不允许存在任何警告,以确保代码的质量和稳定性。

CFLAGS 是一个用于存储传递给 C 编译器的额外参数和标志的 Makefile 变量。这些参数和标志可以影响编译的行为,例如警告级别、优化选项、头文件路径等 在 Makefile 中使用 CFLAGS 变量有助于集中管理编译选项,使得构建过程更加灵活和易于维护。

当你设置 CFLAGS 时,可以包含各种编译器选项,这些选项会影响代码的编译和生成。以下是一些常见的 CFLAGS 选项的例子:

启用调试信息:

bash 复制代码
CFLAGS = -g

这个选项启用了编译器生成的调试信息,有助于在调试阶段中进行源代码级别的调试。

优化级别:

bash 复制代码
CFLAGS = -O2

这个选项启用了优化级别 2,以提高生成代码的运行性能。可选的优化级别包括 -O0(无优化)、-O1、-O2、-O3 等。

指定头文件搜索路径:

bash 复制代码
CFLAGS = -I/path/to/include

这个选项指定了编译器在搜索头文件时要查找的路径。

定义宏:

c 复制代码
CFLAGS = -DDEBUG

这个选项定义了一个名为 DEBUG 的宏,可以在源代码中使用条件编译。

关闭某些警告:

bash 复制代码
CFLAGS = -Wno-unused-variable

这个选项关闭了关于未使用变量的警告。

启用全部警告:

bash 复制代码
CFLAGS = -Wall -Wextra

这个选项启用了大多数可用的警告,帮助开发者发现潜在的问题。

指定目标架构:

bash 复制代码
CFLAGS = -march=native

这个选项根据编译运行代码的计算机的架构进行优化。

来个实际点的makefile例子:

bash 复制代码
CC = gcc
CFLAGS = -g

# 目标文件
TARGET = my_program

# 源文件
SRC = main.c

all: $(TARGET)

$(TARGET): $(SRC)
    $(CC) $(CFLAGS) -o $(TARGET) $(SRC)

clean:
    rm -f $(TARGET)

在这个例子中:

CC 定义了编译器的可执行文件(gcc)。
CFLAGS 包含了编译器选项 -g,表示启用调试信息。

TARGET 定义了目标文件的名称为 my_program。

SRC 定义了源文件的名称为 main.c。

Makefile 中的规则:

all 是默认目标,它依赖于 $(TARGET),当你运行 make 时,它将编译生成目标文件。

$(TARGET) 的规则指定了如何生成目标文件。在这里,它使用 gcc 编译器,传递了 CFLAGS 和源文件,生成可执行文件 my_program。

clean 规则用于清理生成的文件,执行 make clean 将删除可执行文件。

六、通用Makefile模板

这个意思就是找一个经典的makefile模板 以后要写自己的makefile就在这个基础上改就行了

相关推荐
ajassi200012 分钟前
开源 java android app 开发(十一)调试、发布
android·java·linux·开源
爱意随风起风止意难平26 分钟前
如何用AI赋能学习
人工智能·学习·aigc
小李飞刀李寻欢38 分钟前
使用kubeadm部署Kubernetes(k8s)集群的步骤
linux·服务器·ubuntu·kubernetes·k8s
运维成长记1 小时前
阿里云实践创建实例步骤
linux·运维·服务器·阿里云·云计算
viperrrrrrrrrr71 小时前
大数据学习(137)-大数据组件运行时角色
大数据·hive·学习·flink·spark
THe CHallEnge of THe BrAve1 小时前
Linux检验库是否安装成功
linux·运维·服务器
月白风清江有声1 小时前
lsprepost岩石爆炸仿真——学习札记2
学习
万变不离其宗_81 小时前
echarts使用笔记
前端·笔记·echarts
重庆小透明1 小时前
【从零学习JVM|第七篇】快速了解直接内存
学习
梦境虽美,却不长2 小时前
C语言 学习 宏命令(预处理) 2025年6月9日14:41:39
c语言·开发语言·学习