一.GCC
Reference:
正点原子
1.1 gcc安装
sudo apt install gcc |
1.2 gcc指令格式
gcc [选项] [文件名字]
主要[选项]如下:
-c:只编译不链接为可执行文件,编译器将输入的.c 文件编译为.o 的目标文件。
-o:<输出文件名>用来指定编译结束以后的输出文件名,如果不使用这个选项的话 GCC 默
认编译出来的可执行文件名字为 a.out。
-g:添加调试信息,如果要使用调试工具(如 GDB)的话就必须加入此选项,此选项指示编
译的时候生成调试所需的符号信息。
-O:对程序进行优化编译,如果使用此选项的话整个源代码在编译、链接的的时候都会进
行优化,这样产生的可执行文件执行效率就高。
-O2:比-O 更幅度更大的优化,生成的可执行效率更高,但是整个编译过程会很慢。
1.3 GCC编译c文件
gcc hello.c |
这时在此文件的根目录下生成一个.out
文件,这个.out文件就是编译生成的可执行文件,执行的方法很简单使用命令“./+可执行文件”。
gcc hello.c -o hello |
使用gcc hello.c -o hello
来编译hello.c文件,使用参数-o
来指定编译生成的可执文件名字,此外编译hello.c文件除了gcc hello.c -o hello
格式之外,还可以写成gcc -o hello hello.c
的命令格式,hello.c所处位置,不会影响编译最后的结果。
1.4 GCC编译器的编译流程
当输入指令gcc hello.c -o hello
编译hello.c文件时实际上经历了四个流程:预处理、编译、汇编和链接。预处理就是展开所有的头文件、替换程序中的宏、解析条件编译并添加到文件中。编译是将经过预编译处理的代码编译成汇编 代码,也就是我们常说的程序编译。汇编就是将汇编语言文件编译成二进制目标文件。链接就 是将汇编出来的多个二进制目标文件链接在一起,形成最终的可执行文件,链接的时候还会涉及到静态库和动态库等问题。
1.4.1 预处理
预处理(-E):预处理指定的源文件,但不进行编译
gcc -E main.c -o main.i |
预处理器会将源代码中所有以“#”开头的指令进行处理,例如#include、#define、#ifdef等等。其中,#include指令会将指定的头文件内容插入到当前文件中,#define指令会将指定的宏定义替换为对应的值,#ifdef指令会根据条件判断是否编译某段代码。预编译通常输出的是一个预处理文件,通常以.i作为文件扩展名。
1.4.2 编译
编译(-S):编译可执行文件,但不进行汇编
gcc -S main.i -o main,s |
编译过程是将预处理文件编译成汇编代码。编译器输出的是一个汇编代码文件,通常以.s作为文件扩展名。
1.4.3 汇编
汇编(-c):汇编,汇编指定的源文件,但不进行链接
gcc -c main.s -o main.o |
GCC的汇编过程是将汇编代码转化成机器指令。汇编器输出得到是一个目标文件,通常以.o为文件扩展名。
1.4.4 链接
gcc main. o -o main |
GCC的链接过程是将多个目标文件和库文件合并成一个可执行文件的过程。在编译过程中,源代码被编译成目标文件,每个目标文件包含了一部分程序的机器代码和数据。链接器将这些目标文件合并在一起,解决符号引用,生成最终的可执行文件。
链接过程分为静态链接和动态链接两种方式。静态链接是将所有目标文件和库文件的代码和数据合并到一个可执行文件中,这样生成的可执行文件可以在没有库文件的情况下独立运行。动态链接是将程序运行所需的库文件链接到可执行文件中,在程序运行时动态加载这些库文件。
链接过程主要完成以下几个任务:
解析符号引用:在多个目标文件中可能存在相同的符号,例如函数、变量等。链接器会将这些符号引用解析为符号定义,以便生成正确的可执行文件。
合并代码和数据:链接器将多个目标文件中的代码和数据合并到一个可执行文件中,生成一个完整的程序。
重定位:当代码和数据被合并到可执行文件中时,链接器会对它们进行重定位,以确保它们能够正确地访问地址空间中的其他部分。
动态链接:在动态链接的情况下,链接器会将程序运行所需的库文件链接到可执行文件中。在程序运行时,操作系统会动态加载这些库文件,以便程序能够正常运行。
在GCC中,可以使用命令行选项-o
指定输出文件名,例如:
gcc -o hello hello.o world.o |
这个命令会将hello.o和world.o两个目标文件链接在一起,生成可执行文件hello。如果需要链接库文件,可以使用命令行选项“-l”(小写l)指定库文件名,例如:
gcc -o hello hello.o -lm |
这个命令会将数学库文件libm.a链接到可执行文件hello中。
指定头文件路径,一般都是通过-I
(大写的i)来指定,假头文件存在:
/home/hello |
可以通过-I指定
-I /home/hello |
二.Makefile
Makefile里面是一系列的规则组成的,这些规则格式如下:
<target> : <prerequisites> |
上述参数“target”叫做目标,冒号后面的部分叫做“前置条件”;第二行必须由一个tab键起首,后面跟着命令。”目标“是必须的,不可省略的; ”前置条件“和”命令都是可选的“,但两者之间必须至少存在一个。
比如下面这条规则
main : main.o input.o calcu.o |
这条规则的目标是 main,main.o、input.o 和 calcu.o
是生成 main 的依赖文件,如果要更新目标 main,就必须先更新它的所有依赖文件,如果依赖文件中的任何一个有更新,那么目标也必须更新,“更新”就是执行一遍规则中的命令列表。
命令列表中的每条命令必须以 TAB 键开始,不能使用空格!
make 命令会为 Makefile 中的每个以 TAB 开始的命令创建一个 Shell 进程去执行。
Makefile代码示例:
main: main.o input.o calcu.o |
上述代码中一共有 5 条规则,12 行为第一条规则,34 行为第二条规则,56 行为第三条规则,78 行为第四条规则,10~12 为第五条规则,make 命令在执行这个 Makefile 的时候其执行步骤如下:
首先更新第一条规则中的 main,第一条规则的目标成为默认目标,只要默认目标更新了那么就认为 Makefile 的工作。在第一次编译的时候由于 main 还不存在,因此第一条规则会执行,第一条规则依赖于文件 main.o
、input.o
和calcu.o
这个三个.o 文件,这三个.o 文件目前还都没有,因此必须先更新这三个文件。make 会查找以这三个.o 文件为目标的规则并执行。以 main.o
为例,发现更新 main.o
的是第二条规则,因此会执行第二条规则,第二条规则里面的命令为gcc -c main.c
,这行命令很熟悉了吧,就是不链接编译 main.c
,生成 main.o
,其它两个.o
文件同理。
最后一个规则目标是 clean,它没有依赖文件,因此会默认为依赖文件都是最新的,所以其对应的命令不会执行,当我们想要执行 clean 的话可以直接使用命令make clean
,执行以后就会删
除当前目录下所有的.o
文件以及 main
,因此clean
的功能就是完成工程的清理。
如果make指令没有指定目标,默认会执行makefile文件中的第一个目标。
make指令执行过程:
我们在来总结一下 Make 的执行过程:
- make 命令会在当前目录下查找以 Makefile(makefile 其实也可以)命名的文件。
- 当找到 Makefile 文件以后就会按照 Makefile 中定义的规则去编译生成最终的目标文件。
- 当发现目标文件不存在,或者目标所依赖的文件比目标文件新(也就是最后修改时间比
目标文件晚)的话就会执行后面的命令来更新目标
2.1 Makefile伪目标
Makefile 有一种特殊的目标——伪目标,一般的目标名都是要生成的文件,而伪目标不代表真正的目标名,在执行 make 命令的时候通过指定这个伪目标来执行其所在规则的定义的命令。
使用伪目标主要是为了避免 Makefile 中定义的执行命令的目标和工作目录下的实际文件出现名字冲突,有时候我们需要编写一个规则用来执行一些命令,但是这个规则不是用来创建文件的,比如在前面的示例代码中有如下代码用来完成清理工程的功能:
clean: |
上述规则中并没有创建文件 clean 的命令,因此工作目录下永远都不会存在文件 clean,当我们输入make clean
以后,后面的rm *.o
和rm main
总是会执行。可是如果我们“手贱”,在工作目录下创建一个名为“clean”的文件,那就不一样了,当执行“make clean”的时候,规则因为没有依赖件,所以目标被认为是最新的,因此后面的 rm 命令也就不会执行,我们预先设想的清理工程的功能也就无法完成。为了避免这个问题,我们可以将 clean 声明为伪目标,声明方式如下:
.PHONY : clean |
我们使用伪目标来更改示例代码 ,修改完成以后如下:
objects = main.o input.o calcu.o |
上述代码第 5 行声明 clean
为伪目标,声明 clean
为伪目标以后不管当前目录下是否存在为clean
的文件,输入make clean
的话规则后面的rm
命令都会执行。
2.2 Makefile条件判断
在 C 语言中我们通过条件判断语句来根据不同的情况来执行不同的分支,Makefile 也支持条件判断,语法有两种如下:
<条件关键字> |
其中条件关键字有 4 个:ifeq、ifneq、ifdef 和 ifndef,这四个关键字其实分为两对、ifeq 与ifneq、ifdef 与 ifndef,先来看一下 ifeq 和 ifneq,ifeq 用来判断是否相等,ifneq 就是判断是否不 相等,ifeq 用法如下:
ifeq (<参数 1>, <参数 2>) |
上述用法中都是用来比较“参数 1”和“参数 2”是否相同,如果相同则为真,“参数 1”和 “参数 2”可以为函数返回值。ifneq 的用法类似,只不过 ifneq 是用来了比较“参数 1”和“参 数 2”是否不相等,如果不相等的话就为真。
ifdef 和 ifndef 的用法如下:
ifdef <变量名> |
如果“变量名”的值非空,那么表示表达式为真,否则表达式为假。“变量名”同样可以是一个函数的返回值。ifndef 用法类似,但是含义用户 ifdef 相反
2.3 Makefile函数使用
Makefile 支持函数,类似 C 语言一样,Makefile 中的函数是已经定义好的,我们直接使用, 不支持我们自定义函数。make 所支持的函数不多,但是绝对够我们使用了,函数的用法如下:
$(函数名 参数集合) |
可以看出,调用函数和调用普通变量一样,使用符号$
来标识。参数集合是函数的多个参数,参数之间以逗号,
隔开,函数名和参数之间以空格
分隔开,函数的调用以$
开头。接下来我们介绍几个常用的函数。
- 函数 subst
函数 subst 用来完成字符串替换,调用形式如下:
$(subst <from>,<to>,<text>) |
此函数的功能是将字符串
$(subst zzk,ZZK,my name is zzk) |
把字符串“my name is zzk”中的“zzk”替换为“ZZK”,替换完成以后的字符串为“my name is ZZK”。
- 函数 patsubst
函数 patsubst 用来完成模式字符串替换,使用方法如下:
$(patsubst <pattern>,<replacement>,<text>) |
此函数查找字符串<text>
中的单词是否符合模式<pattern>
,如果匹配就用<replacement>
来替换掉,<pattern>
可以使用通配符“%”,表示任意长度的字符串,函数返回值就是替换后的字符串。如果<replacement>
中也包涵“%”,那么<replacement>
中的“%”将是<pattern>
中的那个
“%”所代表的字符串,比如:
$(patsubst %.c,%.o,a.c b.c c.c) |
将字符串“a.c b.c c.c”中的所有符合“%.c”的字符串,替换为“%.o”,替换完成以后的字 符串为“a.o b.o c.o”。
- 函数 dir
函数 dir 用来获取目录,使用方法如下:
$(dir <names…>) |
此函数用来从文件名序列<names>
中提取出目录部分,返回值是文件名序列<names>
的目录部分,比如:
$(dir </src/a.c>)
提取文件“/src/a.c”的目录部分,也就是“/src”。
- 函数notdir
函数 notdir 看名字就是知道去除文件中的目录部分,也就是提取文件名,用法如下:
$(notdir <names…>) |
此函数用与从文件名序列$(notdir </src/a.c>)
提取文件“/src/a.c”中的非目录部分,也就是文件名“a.c”。
- 函数 foreach
foreach 函数用来完成循环,用法如下:
$(foreach <var>, <list>,<text>) |
此函数的意思就是把参数<list>
中的单词逐一取出来放到参数<var>
中,然后再执行<text>
所包含的表达式。每次<text>
都会返回一个字符串,循环的过程中,<text>
中所包含的每个字符串会以空格隔开,最后当整个循环结束时,<text>
所返回的每个字符串所组成的整个字符串将会是函数foreach
函数的返回值。
- 函数 wildcard
通配符“%”只能用在规则中,只有在规则中它才会展开,如果在变量定义和函数使用时,通配符不会自动展开,这个时候就要用到函数 wildcard,使用方法如下:
$(wildcard PATTERN…) |
比如:
$(wildcard *.c)
上面的代码是用来获取当前目录下所有的.c
文件,类似%
。
2.4 Makefile变量
跟 C 语言一样 Makefile 也支持变量的,先看一下前面的例子:
main: main.o input.o calcu.o |
上述 Makefile 语句中,main.o input.o 和 calcue.o
这三个依赖文件,我们输入了两遍,我们这个 Makefile 比较小,如果 Makefile 复杂的时候这种重复输入的工作就会非常费时间,而且非常容易输错,为了解决这个问题,Makefile 加入了变量支持。不像 C 语言中的变量有 int、char等各种类型,Makefile 中的变量都是字符串!类似 C 语言中的宏。使用变量将上面的代码修改,
修改以后如下所示:
示例代码 Makefile 变量使用 |
我们来分析一下示例代码 ,第 1 行是注释,Makefile 中可以写注释,注释开头要用符号“#”,不能用 C 语言中的//
或者/**/
!第 2 行我们定义了一个变量 objects,并且给这个变量进行了赋值,其值为字符串“main.o input.o calcu.o”,第 3 和 4行使用到了变量 objects,Makefile 中变量的引用方法是$(变量名)
,比如本例中的$(objects)
就是使用变量 objects。
在“示例代码”中我们在定义变量 objects 的时候使用=
对其进行了赋值,Makefile变量的赋值符还有其它两个:=
和?=
,我们来看一下这三种赋值符的区别:
- 赋值符“=”
使用“=”在给变量的赋值的时候,不一定要用已经定义好的值,也可以使用后面定义的值,
比如如下代码:
示例代码 赋值符"="使用 |
我们来分析一下上述代码,第 1 行定义了一个变量 name,变量值为“zzk”,第 2 行也定义了一个变量curname,curname的变量值引用了变量name,按照我们C写语言的经验此时curname的值就是“zzk”。第 3 行将变量 name 的值改为了“zuozhongkai”,第 5、6 行是输出变量 curname的值。在 Makefile 要输出一串字符的话使用echo
,就和 C 语言中的printf
一样,第 6 行中的“echo”前面加了个@
符号,因为 Make 在执行的过程中会自动输出命令执行过程,在命令前面加上“@”的话就不会输出命令执行过程,大家可以测试一下不加“@命令make print
来执行上述代码,如下图所示:
在上图中可以看到curname的值不是“zzk”,竟然是“zuozhongkai”,也就是变量“name” 最后一次赋值的结果,这就是赋值符“=”的神奇之处!借助另外一个变量,可以将变量的真实 值推到后面去定义。也就是变量的真实值取决于它所引用的变量的最后一次有效值。
- 赋值符“:=”
代码如下:
示例代码 ":="的使用 |
修改完成以后重新执行一下 Makefile,结果如图 所示:
从上图 中可以看到此时的 curname 是 zzk,不是 zuozhongkai 了。这是因为赋值符“:=”不会使用后面定义的变量,只能使用前面已经定义好的,这就是“=”和“:=”两个的区别。
3、赋值符“**?=**”
“?=”是一个很有用的赋值符,比如下面这行代码:
curname ?= zuozhongkai |
上述代码的意思就是,如果变量 curname 前面没有被赋值,那么此变量就是“zuozhongkai”,如果前面已经赋过值了,那么就使用前面赋的值。
4、变量追加“**+=**”
Makefile 中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进去,此时就要使用到符号“+=”,比如如下所示代码:
objects = main.o inpiut.o |
一开始变量 objects 的值为“main.o input.o”,后面我们给他追加了一个“calcu.o”,因此变量 objects 变成了“main.o input.o calcu.o”,这个就是变量的追加。
2.4.1 Makefile 模式规则
在前面我们编写了一个 Makefile 文件用来编译工程,这个 Makefile 的内容如下:
示例代码 Makefile 文件代码 |
上述 Makefile 中第 3~8 行是将对应的.c 源文件编译为.o
文件,每一个 C 文件都要写一个对应的规则,如果工程中 C 文件很多的话显然不能这么做。为此,我们可以使用 Makefile 中的模式规则,通过模式规则我们就可以使用一条规则来将所有的.c
文件编译为对应的.o
文件。模式规则中,至少在规则的目标定定义中要包涵“%”,否则就是一般规则,目标中的“%” 表示对文件名的匹配,“%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c 结尾的文件,类似与通配符,a.%.c 就表示以 a.开头,以.c 结束的所有文件。当“%”出现在目标中的时候,目标中“%”所代表的值决定了依赖中的“%”值,使用方法如下:
%.o : %.c |
因此“示例代码 ”中的 Makefile 可以改为如下形式:
示例代码 模式规则使用 |
2.5 Makefile自动化变量
上面讲的模式规则中,目标和依赖都是一系列的文件,每一次对模式规则进行解析的时候都会是不同的目标和依赖文件,而命令只有一行,如何通过一行命令来从不同的依赖文件中生成对应的目标?自动化变量就是完成这个功能的!所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,自动化变量只应该出现在规则的命令中,常用的自动化变量如表 :
自动化变量 | 描述 |
---|---|
$@ | 规则中的目标集合,在模式规则中,如果有多个目标的话,“ $@ ”表示匹配模式中定义的目标集合。 |
$% | 当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空。 |
$< | 依赖文件集合中的第一个文件,如果依赖文件是以模式 ( 即“ % ” ) 定义的,那么$< 就是符合模式的一系列的文件集合。 |
$? | 所有比目标新的依赖目标集合,以空格分开。 |
$^ | 所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^ ”会去除重复的依赖文件,值保留一份。 |
$+ | 和“ $^ ”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。 |
$* | 这个变量表示目标模式中 “%” 及其之前的部分,如果目标是 test/a.test.c ,目标模式为 a.%.c ,那么“ $* ”就是 test/a.test 。 |
表中的 7 个自动化变量中,常用的三种:$@、$<和$^,我们使用自动化变量来完成“示例代码 ”中的 Makefile,最终的完整代码如下所示:
示例代码 自动化变量 |