文章目录
- 1. gcc
- 1.1 编译
- 1.1.1 预处理
- 1.1.2 编译
- 1.1.3 汇编
- 1.1.4 编译器的发展和编译器自举
- 1.2 链接
- 1.2.1 静态链接
- 1.2.2 动态链接
- 2. makefile
- 2.1 makefile初阶
- 2.1.1 两个疑点
- 2.2 makefile进阶
- 2.2.1 解决普适性
- 2.2.2 解决多文件编译链接
1. gcc
gcc是Linux中,一款常用的用于编译C语言的编译器。通过gcc,可以一键实现C语言的编译和链接,进而生成可执行文件。
与集成开发环境IDE不同,gcc可以通过特定命令选项,分段式地完成编译和链接。
1.1 编译
C语言的编译过程分为:预处理、编译和汇编。
1.1.1 预处理
预处理中,主要完成的是头文件的包含、宏的替换、注释代码删除和条件编译。
要想源文件执行到预处理为止,可以使用以下指令:
gcc -E code.c -o code.i
其中,-E
选项表示做预处理,其后紧跟的是相应的源文件,-o
后紧跟的是生成的目标文件,在Linux中,习惯以.i
为后缀。
此处,对条件编译进行一些扩展。条件编译本质上就是一种代码裁剪,在不同的条件下,条件编译中保留的代码是不同的。很多软件的收费版和免费版,或是OS级别的代码裁剪,都是应用条件编译才得以实现的。
1.1.2 编译
编译的过程整体很复杂,涉及到词法分析,语法分析(生成解析树,即parse tree),语义分析和编译优化等等,这里不作具体介绍。
要想源文件执行到编译结束未知,可以使用以下指令:
gcc -S code.c -o code.s
其中,-S
选项表示到编译未知,其后紧跟的是源文件或经过预处理的文件,得到的目标文件,通常以.s
为后缀。
C语言源文件,经过编译之后,得到的就是汇编语言。
1.1.3 汇编
汇编操作是将经过编译得到的汇编语言,进一步转化成机器可识别的代码,即二进制文件。
汇编操作如下:
gcc -c code.s -o code.o
经过汇编操作,生成的可执行文件一般以.o
为后缀。
1.1.4 编译器的发展和编译器自举
编译器的发展,贯穿整个计算机发展的始终。
- 二进制编码:计算机只能识别二进制,因此在计算机刚发明的时候,都是直接使用二进制进行编码,但这种编码的方式太过复杂,而且效率太低。
- 汇编语言:为了方便代码的编写,汇编语言因此产生。汇编语言用一些助记符代替二进制指令,然后通过汇编器再翻译成二进制指令让计算机读取,简化了程序编写,并提高了效率。
- 高级程序语言:虽然汇编语言相比于二进制编码有所改进,但是依然不是很接近自然语言,不太符合人认识并思考世界的方式,高级程序语言因此产生,如C、C++等。但是,用这些语言写出的程序,还需经过翻译方能被计算机识别,由于在语言的整个发展过程中,已经实现了汇编器,所以只需站在巨人的肩膀上,把这种高级程序语言翻译成汇编语言,再利用汇编器,将汇编语言翻译成二进制即可。
另外,再来介绍编译器自举。
编译器自举,实质上就是编译器的自我迭代更新,形象地说,就是“自己把自己举高”。
编译器自举是如何实现的呢?在汇编语言的发明过程中,汇编语言的编译器最初是用二进制写的,得到汇编语言原始编译器后,我再使用汇编语言实现一个编译器的代码,再将这部分代码通过二进制写的汇编语言编译器进行编译,由此就得到了用汇编语言实现的汇编器,这个编译器通过自身迭代更新的过程,我们称之为“编译器自举”。
1.2 链接
经过编译得到的文件是无法直接执行的,因为还欠缺一些其依赖的东西。
比如说,我们虽然包含了头文件,但头文件中,只有相应函数的声明,并没有相应函数的实现,所以要想正常执行程序,还需要经过链接。链接讲得通俗些,就是拿到这些声明函数具体实现的过程。
经过链接之后,我们就能得到可执行文件。
链接的指令如下:
gcc code.o -o code.exe
直接gcc即可,不需要添加额外的选项。
使用./执行相应的可执行文件
讲完基本操作后,我们来大致讲一讲链接的实现。链接分为静态链接和动态链接,分别对应静态库和动态库。
1.2.1 静态链接
静态链接链接的是静态库,静态库中有头文件声明函数的具体实现。
静态库的命名,Linux中习惯命名为xxx.a
,Windows中习惯命名为xxx.lib
静态链接,之所以称为静态,是因为这种链接方式,实质上是把所需要函数的具体实现直接拷贝一份到目标文件中。因此,由静态链接生成的可执行文件,往往比较大,但是由于这个文件具有其正常运行所需的全部代码, 因此静态链接得到的文件,不依赖于库,具有独立性。
1.2.2 动态链接
动态链接链接的是动态库,动态库中也有头文件声明函数的具体实现。
动态库的命名,Linux中习惯命名为xxx.so
,Windows中习惯命名为xxx.dll
gcc默认进行的链接操作是动态链接,链接到动态库上。我们可以使用ldd
命令,来查看一个可执行文件具体链接的库,也可以使用file
命令,来查看一个文件的具体信息,进而了解链接情况。
动态链接相比于静态链接,使用更为广泛。动态链接,之所以称为动态,因为它是程序运行时的链接。实质上,动态链接并不会将具体的函数实现直接拷贝到目标文件中,而是给一个信息,在程序实际运行起来时,要执行相应的动态库中的函数时,便通过之前留下的信息,跳转到动态库中,然后将相应的函数载入内存,进行执行,函数执行完后,再从动态库跳转回原有程序,继续往下执行。
通过上述过程,我们可以理解,使用动态链接得到的可执行文件通常不会很大,因为并未直接进行代码的拷贝,但是这样的可执行文件,由于不具备支持程序运行的全部代码,因此会对动态库具有依赖性,如果动态库遭到破坏,可执行文件可能就无法再正常执行。
并且,由于动态链接得到的可执行文件在具体运行时需要进行跳转,因此执行的速度通常会慢于静态链接得到的可执行文件。
2. makefile
在了解gcc之后,我们已经可以在linux下,进行代码的编译、链接生成可执行文件并执行了,但是每次都要用gcc进行编译链接,是否过于繁琐,况且还要考虑多文件编译的情况,这样就更加麻烦了,那么有没有什么更简单的方式,能帮助我们进行大型工程的编译呢?
所以,我们要引入make和makefile。
首先,我们要弄清楚make和makefile之间的关系。make
是linux下的一个指令,makefile
则本质是一个文件,一个可以用make指令,按一定规则进行解析的文件。
借用make和makefile,我们就可以实现对大型工程的自动化编译。
看到这里,可能会有人想问,既然makefile本质是一个文件,那么这个文件名,一定要命名为makefile吗?
答案是,并不是必须的,但必须命名为GNUmakefile/makefile/Makefile这三者中的一个,因为make指令查找相关文件时,只会按照这三者的顺序进行查找,因此makefile是不能随意命名的。
2.1 makefile初阶
make指令直接使用即可,关键在于makefile这个文件该如何写。
makefile中,最重要的两点就是依赖关系和依赖方法。
依赖关系包含目标文件和依赖文件列表,依赖方法则是利用依赖文件列表中的文件去生成目标文件的具体指令。
这其实很好理解,拿做苹果派的过程做一个解释。依赖文件列表就是原材料苹果,目标文件就是苹果派,依赖方法就是将苹果做成苹果派的具体步骤。要想将一个苹果做成苹果派,这三者缺一不可。
上图中,code
就是目标文件,目标文件和依赖文件列表用冒号分隔,code.c
属于依赖文件列表中的文件。
第二行,使用tab键开头, 这点非常重要,千万不能用空格代替,而后面紧跟的就是具体的依赖方法。
这样,我们就完成了一个最简单的makefile,具体使用时,直接make即可。
很自然地,我们希望自动化构建的同时,也能够实现删除相关的功能,这个也可以用makefile实现。
在此,我们先引入一个伪目标的概念。
在makefile中,可以使用.PHONY:
来修饰一个目标文件,此时,这个目标文件即为伪目标——并不是真正的目标,也就是说不会实际生成一个目标文件,实质上只是代表一个指令,也可以理解为代表依赖方法。伪目标有一个特性,即伪目标总是被执行的。
因此,如果一个指令,我们总是希望它执行,就可以通过伪目标实现,比如说此处的删除。
上图中的clean就是一个伪目标,具体操作时我们使用make clean,来执行实际对应的依赖方法rm -rf code。
2.1.1 两个疑点
在上述讲解中,细心的读者会发现有两个疑点。
其一是,为什么第一次编译时直接make即可,不需要跟目标文件,而第二次清除时,却需要使用make + clean。
其二是,什么叫总是被执行的,难道还有不被执行的情况吗?
对于第一个问题,单用make时,默认执行的是makefile文件中,自上向下扫描中的第一条指令,而make后若接具体的目标文件,则按相应的依赖方法执行。
对于第二个问题,请看下面的例子:
所以说,指令存在不被执行的情况。
为什么指令会不被执行呢?这与目标文件和依赖文件的修改时间有关。make在解释makefile中的相关指令时,它会将目标文件的修改时间和依赖文件的修改时间进行对比,只有在两种情况下才会执行指令。
- 目标文件还未生成。这个无需多言。
- 目标文件已经生成了,但是依赖文件的修改时间要晚于目标文件的修改时间,此时也会执行相关指令。因为这说明,依赖文件在目标文件生成后,进行了修改,因此目标文件需要更新;如果不满足这个时间条件,说明目标文件不需要更新,因此相关指令就不会被执行。
而此前已说过,伪目标总是被执行的,因此我们如果将code设置为伪目标,那么对应的依赖方法将总是被执行,从这点上,我们可以窥见,为什么伪目标总是被执行之因:伪目标会绕过目标文件和依赖文件修改时间的比较。
我们可以看到,将code设置成为伪目标后,其对应的依赖方法"gcc code.c -o code"便可以总是被执行。
2.2 makefile进阶
首先介绍一下makefile中的一个推导过程。
如果我直接使用make指令,需要用到code.o用以生成code,但是code.o不存在,因此会往下找,依次经过code.o->code.s->code.i->code.c,此时发现code.c存在,便执行相关的依赖方法生成code.i,然后按照code.i->code.s->code.o->code的顺序依次生成。这便是整个推导过程的逻辑,实质上是一个递归与回溯的过程。
但是这样来写makefile,局限还是较大,其一是无法实现普适性,因为是针对特定名称的源文件进行自动化编译;其二是无法实现多文件的编译链接,而这恰恰是大型项目所必备的。
以下,我们在此二点上对makefile进行优化。
2.2.1 解决普适性
首先,实现普适性。在makefile中,实现普适性,有点类似于宏的定义。
使用SRC代表code.c,使用OBJ代表code,使用GCC代表gcc -o,这样要修改时,只需修改赋值符后相关的内容,方便了修改,提高了普适性。
下面相关内容中,使用$(),表示符号对应的具体内容,如 $(OBJ)就表示code; $@对应的是目标文件, $^对应的是依赖文件列表中的所有文件,在上述例子中, $@就表示code, $^就表示code.c。
另外,在makefile中,如果想要进行注释,使用#。
同时,在使用make指令时,发现相关的依赖方法会回显,如果不想要回显,可以在依赖方法前加上@.
2.2.2 解决多文件编译链接
对于SRC,有两种方式可以获取当前目录下所有的以.c为后缀的文件,一种是使用shell命令行,另一种则是使用wildcard函数获取,具体写法见上图。
对于OBJ,由于源文件是分别经过编译器编译生成目标文件,即一个.c为后缀的文件,对应一个.o为后缀的文件,因此OBJ要与SRC对应起来,所以使用上图写法,OBJ对应的是将SRC中以.c为后缀的文件替换为以.o为后缀所对应的那些文件。
对于链接的操作,$^表示的是依赖文件列表中的全体文件,在依赖方法中,具体而言,是将全体依赖文件一起拿来操作,正好对应将所有目标文件链接,生成一个可执行文件的逻辑。
对于编译的操作,这个操作比较特殊,首先要分别使用%.o个和%.c,分别对应当前目录下所有要生成的目标文件和现有的源文件。在相应的依赖方法中,$<也表示依赖文件列表中的所有文件,但与 $^不同的是,在具体操作时, $<是将依赖文件列表中的文件一个一个拿出来执行依赖方法以生成目标文件——在这个编译过程中,就是一个一个地拿出以.c为后缀的源文件,每个源文件分别经过编译,生成对应的以.o为后缀的目标文件。
在makefile中,有了上述内容后,我们就可以实现多源文件的编译链接操作了。
如果想要具体展现多文件编译和链接的过程,可以在makefile中添加如下内容:
这样,我们就可以看到具体的编译链接过程了。