首页 > 系统相关 >《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​

时间:2023-01-05 10:09:37浏览次数:56  
标签:MPSoC 文件 DFZU2EG Makefile calcu 编译 Linux input main

Linux C编程入门​

在Windows下我们可以使用各种各样的IDE进行编程,比如强大的Visual Studio。但是在Ubuntu下如何进行编程呢?Ubuntu下也有一些可以进行编程的工具,但是大多都只是编辑器,也就是只能进行代码编辑,如果要编译的话就需要用到GCC编译器,使用GCC编译器肯定就要接触到Makefile。本章就讲解如何在Ubuntu下进行C语言的编辑和编译、GCC和Makefile的使用和编写。通过本章的学习可以掌握Linux进行C编程的基本方法,为以后的Linux驱动学习做准备。​


Hello World!

我们所说的编写代码包括两部分:代码编写和编译,在Windows下可以使用Visual Studio来完成这两部,可以在Visual Studio下编写代码然后直接点击编译就可以了。但是在Linux下这两部分是分开的,比如我们用VIM进行代码编写,编写完成以后再使用GCC编译器进行编译,其中代码编写工具很多,比如VIM编辑器、Emacs编辑器、VScode编辑器等等,本教程使用Ubuntu自带的VIM编辑器。先来编写一个最简单的“Hello World”程序,把Linux下的C编程完整的走一遍。​

编写代码

先在用户根目录下创建一个工作文件夹:C_Program,所有的C语言练习都保存到这个工作文件夹下,创建过程如图3.1.1.1所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_示例代码


图3.1.1.1 创建工作目录​

进入图3.1.1.1创建的C_Program工作文件夹,为了方便管理,我们后面每个例程都创建一个文件夹来保存所有与本例程有关的文件,创建一个名为“3.1”的文件夹来保存我们的“Hello World”程序相关的文件,操作如图3.1.1.2所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_示例代码_02


图3.1.1.2 创建工程文件夹​

前面说了我们使用VI编辑器,在使用VI编辑器之前我们先做如下设置:​

1、设置TAB键为4字节

VI编辑器默认TAB键为8空格,我们改成4空格,用vi打开文件/etc/vim/vimrc,在此文件最后面输入如下代码:​

set ts=4​

添加完成如图3.1.1.3所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_编译器_03


图3.1.1.3 设置TAB为四个空格​

修改完成以后保存并关闭文件。​

2、VIM编辑器显示行号

VIM编辑器默认是不显示行号的,不显示行号不利于代码查看,我们设置VIM编辑器显示行号,同样是通过在文件/etc/vim/vimrc中添加代码来实现,在文件最后面加入下面一行代码即可:​

set nu​

添加完成以后的/etc/vim/vimrc文件如图3.1.1.4所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_编译器_04


图3.1.1.4 设置VIM编辑器显示行号​

VIM编辑器可以自行定制,网上有很多的博客讲解如何设置VIM,感兴趣的可以上网看一下。设置好VIM编辑器以后就可以正式开始编写代码了,进入前面创建的“3.1”这个工程文件夹里面,使用vi指令创建一个名为“main.c”的文件,然后在里面输入如下代码:​

示例代码​

#include <stdio.h>​

int main(int argc, char *argv[])​
{​
printf("Hello World!\n");​
}

编写完成以后保存退出vi编辑器,可以使用“cat”命令查看代码是否编写成功,如图3.1.1.5所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_05


图3.1.1.5 查阅程序源码​

从图3.1.1.5可以看出main.c文件是编辑成功的,代码编辑成功以后我们需要对其进行编译。​

编译代码

Ubuntu下的C语言编译器是GCC,GCC编译器在我们安装Ubuntu的时候就已经默认安装好了,可以通过如下命令查看GCC编译器的版本号:​

gcc -v​

在终端中输入上述命令以后终端输出如图3.1.2.1所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_编译器_06


图3.1.2.1 gcc版本查询​

如果输入命令“gcc -v”命令以后,你的终端输出类似图图3.1.2.1中的信息,那么说明你的电脑已经有GCC编译器了。最后下面的“gcc version 5.4.0”说明本机的GCC编译器版本为5.4.0的。注意观察在图3.1.2.1中有“Target: x86_64-linux-gnu”一行,这说明Ubuntu自带的GCC编译器是针对X86架构的,因此只能编译在X86架构CPU上运行的程序。如果想要编译在ARM上运行的程序就需要针对ARM架构的GCC编译器,也就是交叉编译器!我们进行ARM开发,因此肯定要安装针对ARM架构的GCC交叉编译器,当然了,这是后面的事,现在我们不用管这些,只要知道不同的目标架构,其GCC编译器是不同的。​

如何使用GCC编译器来编译main.c文件呢?GCC编译器是命令模式的,因此需要输入命令来使用gcc编译器来编译文件,输入如下命令:​

gcc main.c​

上述命令的功能就是使用gcc编译器来编译main.c这个c文件,过程如图3.1.2.2所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_07


图3.1.2.2 编译main.c文件​

在图3.1.2.2中可以看到,当编译完成以后会生成一个a.out文件,这个a.out就是编译生成的可执行文件,执行此文件看看是否和我们代码的功能一样,执行的方法很简单使用命令:“./+可执行文件”,比如本例程就是命令:./a.out,操作如图3.1.2.3所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_编译器_08


图3.1.2.3 执行编译得到的文件​

在图3.1.2.3中执行a.out文件以后终端输出了“Hello World!”,这正是main.c要实现的功能,说明我们的程序没有错误。a.out这个文件名是GCC编译器自动命名的,那我们能不能决定编译完生成的可执行文件名字呢?肯定可以的,在使用gcc命令的时候加上-o来指定生成的可执行文件名字,比如编译main.c以后生成名为“main”的可执行文件,操作如图3.1.2.4所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_示例代码_09


图3.1.2.4 指定可执行文件名字​

在图3.1.2.4中,我们使用“gcc main.c –o main”来编译main.c文件,使用参数“-o”来指定编译生成的可执行文件名字,至此我们就完成Linux下C编程和编译的一整套过程。​

GCC编译器

gcc命令

在上一小节我们已经使用过GCC编译器来编译C文件了,我们使用到是gcc命令,gcc命令格式如下:​

gcc [选项] [文件名字]​

主要选项如下:​

-c 只编译不链接为可执行文件,编译器将输入的.c文件编译为.o的目标文件。​

-o<输出文件名> 用来指定编译结束以后的输出文件名,如果不使用这个选项的话GCC默认编译出来的可执行文件名字为a.out。​

-g 添加调试信息,如果要使用调试工具(如GDB)的话就必须加入此选项,此选项指示编译的时候生成调试所需的符号信息。​

-O 对程序进行优化编译,如果使用此选项的话整个源代码在编译、链接的的时候都会进行优化,这样产生的可执行文件执行效率就高。​

-O2 比-O更幅度更大的优化,生成的可执行效率更高,但是整个编译过程会很慢。​

编译错误警告

在Windows下不管我们用啥编译器,如果程序有语法错误的话编译的时候都会指示出来,比如开发STM32的时候所使用的MDK和IAR,我们可以根据错误信息方便的修改bug。那GCC编译器有没有错误提示呢?肯定是有的,我们可以测试以下,新名为“3.2”的文件夹,使用vi在文件夹“3.2”中创建一个main.c文件,在文件里面输入如下代码:​

示例代码3.2 mian.c文件代码​

#include <stdio.h>​

int main(int argc, char *argv[])​
{​
int a, b;​

a = 3;​
b = 4 (1)​
printf("a+b=\n", a + b); (2)​
}

在上述代码中有两处错误:​

(1) 第一处是“b=4”少写了个一个“;”号。​

(2) 第二处应该是printf(“a+b=%d\n”, a + b);​

我们编译一下上述代码,看看GCC编译器是否能够检查出错误,编译结果如图3.2.2.1所示:​


《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_10


图3.2.2.1 错误提示​

从图3.2.2.1中可以看出有一个error,提示在main.c文件的第9行有错误,错误类型是在printf之前没有“;”号,这就是第一处错误,我们在“b = 4”后面加上分号,然后接着编译,结果又提示有一个错误,如图3.2.2.2所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_11


图3.2.2.2 错误提示​

在图3.2.2.2中,提示我们说文件main.c的第9行:printf(“a+b=\n”, a + b)有error,错误是因为太多参数了,我们将其改为:​

printf(“a+b=%d\n”, a + b);​

修改完成以后接着重新编译一下,结果如图3.2.2.3所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_12


图3.2.2.3 编译成功​

在图3.2.2.3中我们编译成功,生成了可执行文件main,执行一下main,看看结果和我们设计的是否一样,如图3.2.2.4所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_示例代码_13


图3.2.2.4 执行结果​

可以看出,GCC编译器和其它编译器一样,不仅能够检测出错误类型,而且标记除了错误发生在哪个文件、哪一行,方便我们去修改代码。​

编译流程

GCC编译器的编译流程是:预处理、汇编、编译和链接。预处理就是对程序中的宏定义等相关的内容先进行前期的处理。汇编是先将C文件转换为汇编文件。当C文件转换为汇编文件以后就是文件编译了,编译过程就是将C源文件编译成.o结尾的目标文件。编译生成的.o文件不能直接执行,而是需要最后的链接,如果你的工程有很多个c源文件的话最终就会有很多.o文件,将这些.o文件链接在一起形成完整的一个可执行文件。​

上一小节演示的例程都只有一个文件,而且文件非常简单,因此可以直接使用gcc命令生成可执行文件,并没有先将c文件编译成.o文件,然后在链接在一起。​

Makefile基础

何为Makefile

上一小节我们讲了如何使用GCC编译器在Linux进行C语言编译,通过在终端执行gcc命令来完成C文件的编译,如果我们的工程只有一两个C文件还好,需要输入的命令不多,当文件有几十、上百甚至上万个的时候用终端输入GCC命令的方法显然是不现实的。如果我们能够编写一个文件,这个文件描述了编译哪些源码文件、如何编译那就好了,每次需要编译工程的时只需要使用这个文件就行了。这种问题怎么可能难倒聪明的程序员,为此提出了一个解决大工程编译的工具:Make,描述哪些文件需要编译、哪些需要重新编译的文件就叫做Makefile,Makefile就跟脚本文件一样,Makefile里面还可以执行系统命令。使用的时候只需要一个make命令即可完成整个工程的自动编译,极大的提高了软件开发的效率。如果大家以前一直使用IDE来编写C语言的话肯定没有听说过Makefile这个东西,其实这些IDE是有的,只不过这些IDE对其进行了封装,提供给大家的是已经经过封装后的图形界面了,我们在IDE中添加要编译的C文件,然后点击按钮就完成了编译。在Linux下用的最多的是GCC编译器,这是个没有UI的编译器,因此Makefile就需要我们自己来编写了。作为一个专业的程序员,是一定要懂得Makefile的,一是因为在Linux下你不得不懂Makefile,再就是通过Makefile你就能了解整个工程的处理过程。​

由于Makefile的知识比较多,完全可以单独写本书,因此本章我们只讲解Makefile基础入门,如果想详细的研究Makefile,推荐大家阅读《跟我一起写Makefile》这份文档,文档已经放到了文件夹了,本章也有很多地方参考了此文档。​

Makefile的引入

我们完成这样一个小工程,通过键盘输入两个整形数字,然后计算他们的和并将结果显示在屏幕上,在这个工程中我们有main.c、input.c和calcu.c这三个C文件和input.h、calcu.h这两个头文件。其中main.c是主体,input.c负责接收从键盘输入的数值,calcu.h进行任意两个数相加,其中main.c文件内容如下:​

示例代码3.3.2.1 main.c文件代码​

1 #include <stdio.h>​
2 #include "input.h"​
3 #include "calcu.h"​
4 ​
5 int main(int argc, char *argv[])​
6 {​
7 int a, b, num;​
8 ​
9 input_int(&a, &b);​
10 num = calcu(a, b);​
11 printf("%d + %d = %d\r\n", a, b, num);​
12 }

input.c文件内容如下:​

示例代码3.3.2.2 input.c文件代码​

1 #include <stdio.h>​
2 #include "input.h"​
3 ​
4 void input_int(int *a, int *b)​
5 {​
6 printf("input two num:");​
7 scanf("%d %d", a, b);​
8 printf("\r\n");​
9 }

calcu.c文件内容如下:​

示例代码3.3.2.3 calcu.c文件代码​

1 #include "calcu.h"​
2 ​
3 int calcu(int a, int b)​
4 {​
5 return (a + b);​
6 }

文件input.h内容如下:​

示例代码3.3.2.4 input.h文件代码​

1 #ifndef _INPUT_H​
2 #define _INPUT_H​
3 ​
4 void input_int(int *a, int *b);​
5 #endif

文件calcu.h内容如下:​

示例代码3.3.2.5 calcu.h文件代码​

1 #ifndef _CALCU_H​
2 #define _CALCU_H​
3 ​
4 int calcu(int a, int b);​
5 #endif

以上就是我们这个小工程的所有源文件,我们接下来使用3.1节讲的方法来对其进行编译,在终端输入如下命令:​

gcc main.c calcu.c input.c -o main​

上面命令的意思就是使用gcc编译器对main.c、calcu.c和input.c这三个文件进行编译,编译生成的可执行文件叫做main。编译完成以后执行main这个程序,测试一下软件是否工作正常,结果如图3.3.2.1所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_编译器_14


图3.3.2.1 程序测试​

可以看出我们的代码按照我们所设想的工作了,使用命令“gcc main.c calcu.c input.c -o main”看起来很简单是吧,只需要一行就可以完成编译,但是我们这个工程只有三个文件啊!如果几千个文件呢?再就是如果有一个文件被修改了呢,使用上面的命令编译的时候所有的文件都会重新编译,如果工程有几万个文件(Linux源码就有这么多文件!),想想这几万个文件编译一次所需要的时间就可怕。最好的办法肯定是哪个文件被修改了,只编译这个修改的文件即可,其它没有修改的文件就不需要再次重新编译了,为此我们改变我们的编译方法,如果第一次编译工程,我们先将工程中的文件都编译一遍,然后后面修改了哪个文件就编译哪个文件,命令如下:​

gcc -c main.c​
gcc -c input.c​
gcc -c calcu.c​
gcc main.o input.o calcu.o -o main

上述命令前三行分别是将main.c、input.c和calcu.c编译成对应的.o文件,所以使用了“-c”选项,“-c”选项我们上面说了,是只编译不链接。最后一行命令是将编译出来的所有.o文件链接成可执行文件main。假如我们现在修改了calcu.c这个文件,只需要将caclue.c这一个文件重新编译成.o文件,然后再将所有的.o文件链接成可执行文件,只需要下面两条命令即可:​

gcc -c calcu.c​
gcc main.o input.o calcu.o -o main

但是这样就又有一个问题,如果修改的文件一多,我自己可能都不记得哪个文件修改过了,然后忘记编译,然后……,为此我们需要这样一个工具:​

1、如果工程没有编译过,那么工程中的所有.c文件都要被编译并且链接成可执行程序。​

2、如果工程中只有个别C文件被修改了,那么只编译这些被修改的C文件即可。​

3、如果工程的头文件被修改了,那么我们需要编译所有引用这个头文件的C文件,并且链接成可执行文件。​

很明显,能够完成这个功能的就是Makefile了,在工程目录下创建名为“Makefile”的文件,文件名一定要叫做“Makefile”!!!区分大小写的哦!如图3.3.2.2所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_编译器_15


图3.3.2.2 Makefile文件​

在图3.3.2.2中Makefile和C文件是处于同一个目录的,在Makefile文件中输入如下代码:​

示例代码3.3.2.6 Makefile文件代码​

1 main: main.o input.o calcu.o​
2 gcc -o main main.o input.o calcu.o​
3 main.o: main.c​
4 gcc -c main.c​
5 input.o: input.c​
6 gcc -c input.c​
7 calcu.o: calcu.c​
8 gcc -c calcu.c​
9 ​
10 clean:​
11 rm *.o​
12 rm main

上述代码中所有行首需要空出来的地方一定要使用“TAB”键!不要使用空格键!这是Makefile的语法要求,编写好的Makefile如图3.3.2.3所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_示例代码_16


图3.3.2.3 Makefile源码​

Makefile编写好以后我们就可以使用Make命令来编译我们的工程了,直接在命令行中输入“make”即可,Make命令会在当前目录下查找是否存在“Makefile”这个文件,如果存在的话就会按照Makefile里面定义的编译方式进行编译,如图3.3.2.4所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_17


图3.3.2.4 Make编译工程​

在图3.3.2.4中,使用命令“Make”编译完成以后就会在当前工程目录下生成各种.o和可执行文件,说明我们编译成功了。使用make命令编译工程的时候可能会提示如图3.3.2.5所示错误:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_示例代码_18


图3.3.2.5 Make失败​

图3.3.2.5中的错误来源一般有两点:​

1、Makefile中命令缩进没有使用TAB键!​

2、VI/VIM编辑器使用空格代替了TAB键,修改文件/etc/vim/vimrc,在文件最后面加上如下所示代码:​

set noexpandtab​

我们修改一下input.c文件源码,随便加几行空行就行了,保证input.c被修改过即可,修改完成以后再执行一下“make”命令重新编译一下工程,结果如图3.3.2.6所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_示例代码_19


图3.3.2.6 重新编译工程​

从图3.3.2.6中可以看出因为我们修改了input.c这个文件,所以input.c和最后的可执行文件main重新编译了,其它没有修改过的文件就没有编译。而且我们只需要输入“make”这个命令即可,非常方便,但是Makefile里面的代码都是什么意思呢?这就是接下来我们要讲解的。​

Makefile语法

Makefile规则格式

Makefile里面是由一系列的规则组成的,这些规则格式如下:​

目标…... : 依赖文件集合……​

命令1​

命令2​

……​

比如下面这条规则:​

main : main.o input.o calcu.o​
gcc -o main main.o input.o calcu.o

这条规则的目标是main,main.o、input.o和calcu.o是生成main的依赖文件,如果要更新目标main,就必须要先更新它的所有依赖文件,如果依赖文件中的任何一个有更新,那么目标也必须更新,“更新”就是执行一遍规则中的命令列表。​

命令列表中的每条命令必须以TAB键开始,不能使用空格!​

make命令会为Makefile中的每个以TAB开始的命令创建一个Shell进程去执行。​

了解了Makefile的基本运行规则以后我们再来分析一下3.3节中“示例代码3.3.2.6”中的Makefile,代码如下:​

1 main: main.o input.o calcu.o​
2 gcc -o main main.o input.o calcu.o​
3 main.o: main.c​
4 gcc -c main.c​
5 input.o: input.c​
6 gcc -c input.c​
7 calcu.o: calcu.c​
8 gcc -c calcu.c​
9 ​
10 clean:​
11 rm *.o​
12 rm main

上述代码中一共有5条规则,1~2行为第一条规则,3~4行为第二条规则,5~6行为第三条规则,7~8行为第四条规则,10~12为第五条规则,make命令在执行这个Makefile的时候其执行步骤如下:​

首先更新第一条规则中的main,第一条规则的目标成为默认目标,只要默认目标更新了那么就完成了Makefile的工作,完成了整个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 clean”的执行过程如图3.4.1.1所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_20


图3.4.1.1 make clean执行过程​

从图3.4.1.1可以看出,当执行“make clean”命令以后,前面编译出来的.o和main可执行文件都被删除掉了,也就是完成了工程清理工作。​

我们在来总结一下Make的执行过程:​

  1. make命令会在当前目录下查找以Makefile(makefile其实也可以)命名的文件。​
  2. 当找到Makefile文件以后就会按照Makefile中定义的规则去编译生成最终的目标文件。​
  3. 当发现目标文件不存在,或者目标所依赖的文件比目标文件新(也就是最后修改时间比目标文件晚)的话就会执行后面的命令来更新目标。​

这就是make的执行过程,make工具就是在Makefile中一层一层的查找依赖关系,并执行相应的命令。编译出最终的可执行文件。Makefile的好处就是“自动化编译”,一旦写好了Makefile文件,以后只需要一个make命令即可完成整个工程的编译,极大的提高了开发效率。把make和Makefile和做菜类似,目标都是呈现出一场盛宴,它们之间的对比关系如表3.4.1.1所示:​

make​

做菜​

描述​

make工具​

厨师​

负责将材料加工成“美味”。​

gcc编译器​

厨具​

加工“美味”的工具。​

Makefile​

食材​

加工美味所需的原材料。​

最终的可执行文件​

最终的菜肴​

最终目的,呈现盛宴​

表3.4.1.1 make和做菜对比​

总结一下,Makefile中规则用来描述在什么情况下使用什么命令来构建一个特定的文件,这个文件就是规则的“目标”,为了生成这个“目标”而作为材料的其它文件称为“目标”的依赖,规则的命令是用来创建或者更新目标的。​

除了Makefile的“终极目标”所在的规则以外,其它规则的顺序在Makefile中是没有意义的,“终极目标”就是指在使用make命令的时候没有指定具体的目标时,make默认的那个目标,它是Makefile文件中第一个规则的目标,如果Makefile中的第一个规则有多个目标,那么这些目标中的第一个目标就是make的“终极目标”。​

Makefile变量

跟C语言一样Makefile也支持变量的,先看一下前面的例子:​

main: main.o input.o calcu.o​
gcc -o main main.o input.o calcu.o

上述Makefile语句中,main.o input.o和calcue.o这三个依赖文件,我们输入了两遍,我们这个Makefile比较小,如果Makefile复杂的时候这种重复输入的工作就会非常费时间,而且非常容易输错,为了解决这个问题,Makefile加入了变量支持。不像C语言中的变量有int、char等各种类型,Makefile中的变量都是字符串!类似C语言中的宏。使用变量将上面的代码修改,修改以后如下所示:​

示例代码3.4.2.1 Makefile变量使用​

1变量的使用​
2 objects = main.o input.o calcu.o​
3 main: $(objects)​
4 gcc -o main $(objects)

我们来分析一下“示例代码3.4.2.1”,第1行是注释,Makefile中可以写注释,注释开头要用符号“#”,不能用C语言中的“//”或者“/**/”!第2行我们定义了一个变量objects,并且给这个变量进行了赋值,其值为字符串“main.o input.o calcu.o”,第3和4行使用到了变量objects,Makefile中变量的引用方法是“$(变量名)”,比如本例中的“$(objects)”就是使用变量objects。​

在“示例代码3.4.2.1”中我们在定义变量objects的时候使用“=”对其进行了赋值,Makefile变量的赋值符还有其它两个“:=”和“?=”,我们来看一下这三种赋值符的区别:​

1、赋值符“=”

使用“=”在给变量的赋值的时候,不一定要用已经定义好的值,也可以使用后面定义的值,比如如下代码:​

示例代码3.4.2.1 赋值符"="使用​

1 name = zzk​
2 curname = $(name)​
3 name = zuozhongkai​
4 ​
5 print:​
6 @echo curname: $(curname)

我们来分析一下上述代码,第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”来执行上述代码,结果如图3.4.2.1:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_可执行文件_21


图3.4.2.1 make执行结果​

在图3.4.2.1中可以看到curname的值不是“zzk”,竟然是“zuozhongkai”,也就是变量“name”最后一次赋值的结果,这就是赋值符“=”的神奇之处!借助另外一个变量,可以将变量的真实值推到后面去定义。也就是变量的真实值取决于它所引用的变量的最后一次有效值。​

2、赋值符“:=”

在“示例代码3.4.2.1”上来测试赋值符“:=”,修改“示例代码3.4.2.1”中的第2行,将其中的“=”改为“:=”,修改完成以后的代码如下:​

示例代码3.4.2.2 ":="的使用​

1 name = zzk​
2 curname := $(name)​
3 name = zuozhongkai​
4 ​
5 print:​
6 @echo curname: $(curname)

修改完成以后重新执行一下Makefile,结果如图3.4.2.2所示:​

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第三章 Linux C编程入门​_编译器_22


图3.4.2.2 make执行结果​

从图3.4.2.2中可以看到此时的curname是zzk,不是zuozhongkai了。这是因为赋值符“:=”不会使用后面定义的变量,只能使用前面已经定义好的,这就是“=”和“:=”两个的区别。​

3、赋值符“?=”

“?=”是一个很有用的赋值符,比如下面这行代码:​

curname ?= zuozhongkai​

上述代码的意思就是,如果变量curname前面没有被赋值,那么此变量就是“zuozhongkai”,如果前面已经赋过值了,那么就使用前面赋的值。​

4、变量追加“+=”

Makefile中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进去,此时就要使用到符号“+=”,比如如下所示代码:​

objects = main.o inpiut.o​

objects += calcu.o​

一开始变量objects的值为“main.o input.o”,后面我们给他追加了一个“calcu.o”,因此变量objects变成了“main.o input.o calcu.o”,这个就是变量的追加。​

Makefile模式规则

在3.3.2小节中我们编写了一个Makefile文件用来编译工程,这个Makefile的内容如下:​

示例代码3.4.3.1 Makefile文件代码​

1 main: main.o input.o calcu.o​
2 gcc -o main main.o input.o calcu.o​
3 main.o: main.c​
4 gcc -c main.c​
5 input.o: input.c​
6 gcc -c input.c​
7 calcu.o: calcu.c​
8 gcc -c calcu.c​
9 ​
10 clean:​
11 rm *.o​
12 rm main

上述Makefile中第3~8行是将对应的.c源文件编译为.o文件,每一个C文件都要写一个对应的规则,如果工程中C文件很多的话显然不能这么做。为此,我们可以使用Makefile中的模式规则,通过模式规则我们就可以使用一条规则来将所有的.c文件编译为对应的.o文件。​

模式规则中,至少在规则的目标定定义中要包涵“%”,否则就是一般规则,目标中的“%”表示对文件名的匹配,“%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c结尾的文件,类似与通配符,a.%.c就表示以a.开头,以.c结束的所有文件。​

当“%”出现在目标中的时候,目标中“%”所代表的值决定了依赖中的“%”值,使用方法如下:​

%.o : %.c​

命令 ​

因此“示例代码3.4.3.1”中的Makefile可以改为如下形式:​

示例代码3.4.3.2 模式规则使用​

1 objects = main.o input.o calcu.o​
2 main: $(objects) ​
3 gcc -o main $(objects)​
4 ​
5 %.o : %.c​
6命令​
7 ​
8 clean:​
9 rm *.o​
10 rm main

“示例代码3.4.3.2”中第5、6这两行代码替代了“示例代码3.4.3.1”中的3~8行代码, 修改以后的Makefile还不能运行,因为第6行的命令我们还没写呢,第6行的命令我们需要借助另外一种强大的变量—自动化变量。​

Makefile自动化变量

上面讲的模式规则中,目标和依赖都是一系列的文件,每一次对模式规则进行解析的时候都会是不同的目标和依赖文件,而命令只有一行,如何通过一行命令来从不同的依赖文件中生成对应的目标呢?自动化变量就是完成这个功能的!所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,自动化变量只应该出现在规则的命令中,常用的自动化变量如表3.4.4.1:​

自动化变量

描述

$@

规则中的目标集合,在模式规则中,如果有多个目标的话,“$@”表示匹配模式中定义的目标集合。​

$%

当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空。​

$<

依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么“$<”就是符合模式的一系列的文件集合。​

$?

所有比目标新的依赖目标集合,以空格分开。​

$^

所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^”会去除重复的依赖文件,值保留一份。​

$+

和“$^”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。​

$*

这个变量表示目标模式中"%"及其之前的部分,如果目标是test/a.test.c,目标模式为a.%.c,那么“$*”就是test/a.test。​

表3.4.4.1 自动化变量​

表3.4.4.1中的7个自动化变量中,常用的三种:$@、$<和$^,我们使用自动化变量来完成“示例代码3.4.3.2”中的Makefile,最终的完整代码如下所示:​

示例代码3.4.4.1 自动化变量​

1 objects = main.o input.o calcu.o​
2 main: $(objects)​
3 gcc -o main $(objects)​
4 ​
5 %.o : %.c​
6 gcc -c $<​
7 ​
8 clean:​
9 rm *.o​
10 rm main

上述代码代码就是修改后的完成的Makefile,可以看出相比3.3.2小节中的要精简了很多,核心就在于第5、6这两行,第5行使用了模式规则,第6行使用了自动化变量。​

Makefile伪目标

Makefile有一种特殊的目标——伪目标,一般的目标名都是要生成的文件,而伪目标不代表真正的目标名,在执行make命令的时候通过指定这个伪目标来执行其所在规则的定义的命令。​

使用伪目标的主要是为了避免Makefile中定义的只执行命令的目标和工作目录下的实际文件出现名字冲突,有时候我们需要编写一个规则用来执行一些命令,但是这个规则不是用来创建文件的,比如在前面的“示例代码3.4.4.1”中有如下代码用来完成清理工程的功能:​

clean:​
rm *.o​
rm main

上述规则中并没有创建文件clean的命令,因此工作目录下永远都不会存在文件clean,当我们输入“make clean”以后,后面的“rm *.o”和“rm main”总是会执行。可是如果我们“手贱”,在工作目录下创建一个名为“clean”的文件,那就不一样了,当执行“make clean”的时候,规则因为没有依赖文件,所以目标被认为是最新的,因此后面的rm命令也就不会执行,我们预先设想的清理工程的功能也就无法完成。为了避免这个问题,我们可以将clean声明为伪目标,声明方式如下:​

.PHONY : clean

我们使用伪目标来更改“示例代码3.4.4.1”,修改完成以后如下:​

示例代码3.4.5.1 伪目标​

 1 objects = main.o input.o calcu.o​
2 main: $(objects)​
3 gcc -o main $(objects)​
4 ​
5 .PHONY : clean​
6 ​
7 %.o : %.c​
8 gcc -c $<​
9 ​
10 clean:​
11 rm *.o​
12 rm main

上述代码第5行声明clean为伪目标,声明clean为伪目标以后不管当前目录下是否存在名为“clean”的文件,输入“make clean”的话规则后面的rm命令都会执行。​

Makefile条件判断

在C语言中我们通过条件判断语句来根据不同的情况来执行不同的分支,Makefile也支持条件判断,语法有两种如下:​

<条件关键字>​

<条件为真时执行的语句>​

endif​

以及:​

<条件关键字>​

<条件为真时执行的语句>​

else​

<条件为假时执行的语句>​

endif​

其中条件关键字有4个:ifeq、ifneq、ifdef和ifndef,这四个关键字其实分为两对、ifeq与ifneq、ifdef与ifndef,先来看一下ifeq和ifneq,ifeq用来判断是否相等,ifneq就是判断是否不相等,ifeq用法如下:​

ifeq (<参数1>, <参数>)​

ifeq ‘<参数1 >’,‘ <参数2>’ ​

ifeq “<参数1>”, “<参数2>”​

ifeq “<参数1>”, ‘<参数2>’​

ifeq ‘<参数1>’, “<参数2>”​

上述用法中都是用来比较“参数1”和“参数2”是否相同,如果相同则为真,“参数1”和“参数2”可以为函数返回值。ifneq的用法类似,只不过ifneq是用来了比较“参数1”和“参数2”是否不相等,如果不相等的话就为真。​

ifdef和ifndef的用法如下:​

ifndef <变量名>​

如果“变量名”的值非空,那么表示表达式为真,否则表达式为假。“变量名”同样可以是一个函数的返回值。ifndef用法类似,但是含义与ifdef相反。​

Makefile函数使用

Makefile支持函数,类似C语言一样,Makefile中的函数是已经定义好的,我们直接使用,不支持我们自定义函数。make所支持的函数不多,但是绝对够我们使用了,函数的用法如下:​

$(函数名 参数集合)​

或者​

${函数名 参数集合}​

可以看出,调用函数和调用普通变量一样,使用符号“$”来标识。参数集合是函数的多个参数,参数之间以逗号“,”隔开,函数名和参数之间以“空格”分隔开,函数的调用以“$”开头。接下来我们介绍几个常用的函数,其它的函数大家可以参考《跟我一起写Makefile》这份文档。​

1、函数subst

函数subst用来完成字符串替换,调用形式如下:​

$(subst <from>,<to>,<text>)​

此函数的功能是将字符串<text>中的<from>内容替换为<to>,函数返回被替换以后的字符串,比如如下示例:​

$(subst zzk,ZZK,my name is zzk)​

把字符串“my name is zzk”中的“zzk”替换为“ZZK”,替换完成以后的字符串为“my name is ZZK”。​

2、函数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”。​

3、函数dir

函数dir用来获取目录,使用方法如下:​

$(dir <names…>)​

此函数用来从文件名序列<names>中提取出目录部分,返回值是文件名序列<names>的目录部分,比如:​

$(dir </src/a.c>)​

提取文件“/src/a.c”的目录部分,也就是“/src”。​

4、函数notdir

函数notdir看名字就是知道去除文件中的目录部分,也就是提取文件名,用法如下:​

$(notdir <names…>)​

此函数用与从文件名序列<names>中提取出文件名非目录部分,比如:​

$(notdir </src/a.c>)​

提取文件“/src/a.c”中的非目录部分,也就是文件名“a.c”。​

5、函数foreach

foreach函数用来完成循环,用法如下:​

$(foreach <var>, <list>,<text>)​

此函数的意思就是把参数<list>中的单词逐一取出来放到参数<var>中,然后再执行<text>所包含的表达式。每次<text>都会返回一个字符串,循环的过程中,<text>中所包含的每个字符串会以空格隔开,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串将会是函数foreach函数的返回值。​

6、函数wildcard

通配符“%”只能用在规则中,只有在规则中它才会展开,如果在变量定义和函数使用时,通配符不会自动展开,这个时候就要用到函数wildcard,使用方法如下:​

$(wildcard PATTERN…)​

比如:​

$(wildcard *.c)​

上面的代码是用来获取当前目录下所有的.c文件,类似“%”。​

关于Makefile的相关内容就讲解到这里,本节只是对Makefile做了最基本的讲解,确保大家能够完成后续的学习,Makefile还有大量的知识没有提到,有兴趣的可以自行参考《跟我一起写Makefile》这份文档来深入学习Makefile。​


标签:MPSoC,文件,DFZU2EG,Makefile,calcu,编译,Linux,input,main
From: https://blog.51cto.com/u_15046463/5989462

相关文章

  • 自学Linux,你需要get哪些点?
    近些年来,Linux操作系统越来越受欢迎,它的开源、免费是亮点也是优点,并且稳定性和安全性也得到认可,还有Linux有很多版本供大家选择,可以定制化。据我所知,现在很多公司的开发项目......
  • Linux LVM 磁盘扩容
    LVM的基本概念物理卷physicalVolume(PV):可以在上面建立组卷的媒介,可以是硬盘分区,也可以是硬盘本身或者回环文件(loopbackfile)。物理卷包括一个特殊的header,其余部分被切割为......
  • Linux 安装seata
    目的:seata是搭配nacos使用nacos配置了seata的命名空间:490f2023-5fvf-30cc-b1cd-e0f0363f0aa61、下载seata包地址:https://github.com/search?q=seata&type=本次安装版本......
  • Linux目录结构、查看目录命令
    ###一、Linux目录结构​      设计哲学:​            一切皆文件!!!!!```​      /   根目录​            /root​......
  • linux 中sed命令将指定列替换为特定的内容
     001、[root@pc1test]#lsa.txt[root@pc1test]#cata.txtdiy5fersd34d83fdgdgxej7[root@pc1test]......
  • c_header: system()(linux; <stdlib.h>)
    c_header: system()(linux;<stdlib.h>)    一、源码 1[root@rockyc]#catstdlib_header.c2#include<stdio.h>3#include<stdlib.h>4#include......
  • linux使用dd命令备份系统并制作硬盘镜像(virt-p2v的一种方案)
    最好准备一个livecd的U盘,和一个存储备份的移动硬盘。1、备份用liveCD开机,先挂载移动硬盘分区:mount/dev/sdb5/mnt然后再备份:ddif=/dev/sdbof=/mnt/sdb_backup.img ......
  • Linux清理磁盘
    inode译成中文就是索引节点,每个存储设备(例如硬盘)或存储设备的分区被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是......
  • golang linux/windows下根据日期自动备份,恢复mysql数据表
    由于shell不熟,所以用golang进行自动化备份mysql备份某个数据库下指定日期的表。比如’tb_20221210'这样的表名1packagemain23import(4"fmt"5......
  • Linux Network IO Model、Socket IO Model - select、poll、epoll
    LinuxNetworkIOModel、SocketIOModel-select、poll、epoll目录0.引言1.IO机制简介2.阻塞式IO模型(blockingIOmodel)3.非阻塞式IO模型(......