编译器的工作过程(以gcc编译器为例)
leobxpan opened this issue · 5 comments
补充一下
3、确定依赖关系
Makefile的依赖关系保证两点
例如:a: b c d
1)生成a时,需要b、c、d三个文件存在;
2)b、c、d三个文件发生变更时,需要按照规则重新生成a。
对于C语言,通常用两种依赖关系
1)目标文件与源文件和头文件的依赖关系
foo.o: foo.c foo.h bar.h
依赖中列出生成foo.o的源文件foo.c,及foo.c直接或间接包含的头文件foo.h和bar.h。
只列出foo.c也可编译,但头文件更改时,foo.o不会更新,会导致Bug。也就是说foo.h和bar.h不会独立编译,它们仅用于在Makefile中补充何时需要重编foo.o的信息。
2)可执行程序与目标文件的依赖
hello: hello.o foo.o
指定生成可执行程序hello需要的目标文件。
3)其它用途
比如根据protobuffer的定义文件,生成相应的.c文件。
*. 静态链接和动态链接
静态链接不依赖库的版本,但依赖Linux内核版本。一般情况下,内核的API(系统调用)不会发生大的变化,所以静态链接的程序基本可以在各种系统上无缝运行。但是如果程序用到了一些新内核的特性,放到旧版本内核的系统上是跑不了的。
动态链接除了要求运行的机器上安装有依赖的库,往往也对库的版本和二进制格式有要求,兼容性要差的多。Linux上动态链接的程序很难在不同的Linux发行版或同一发行版的不同版本上运行。
PS:GCC、G++这些是编译器,Makefile算构建工具,负责按照依赖关系和规则生成各种目标文件。
autogen、configure、makefile算是一套东西,处于相同层次的还有
1)CMake。对Make的改进?自己有一套描述语言,新的C++项目貌似用这个比较多。
2)Bazel。使用Python语法的子集,项目还在初期,功能还在调整。适用于对跨平台兼容性要求不大的场景,用法比Makefile和CMake要简单。
以及Java界的ant,maven,gradle,etc
大家都知道,我们用c、c++写出来的程序计算机是看不懂的,计算机只能看懂由0和1组成的机器码。这个将高级语言翻译成机器语言的过程由谁来做呢?编译器。编译器的作用就是将源码翻译成计算机可以看懂的语言,并且生成计算机可以执行的程序。下面我主要介绍一下gcc(GNU Compiler Collection, GNU编译器套件, 可以编译包括c、c++、Fortran、Pascal、Objective-C、Java、Ada和Go在内的多种编程语言)的工作过程。
1、配置
编译器要得知当前的系统环境,例如你所用语言的标准库在哪里、生成的软件要安装在哪里等等。不同计算机的环境是不同的,因此编译器需要一份配置文件来获得编译参数,从而适应不同的计算机环境,从而编译出在不同计算机上都可以正常运行的代码。通常这份配置文件是由一个叫做Autoconf的软件自动生成的一个叫做configure的脚本文件,gcc通过运行这份脚本文件来获知编译参数。
2、确定标准库和头文件的位置
对于源码中用到标准库函数和头文件的情况(实际上几乎所有程序都会用到),编译器需要通过上一步生成的配置文件来获知标准库和头文件的位置。通常来说配置文件里会列出一些具体的目录,等到编译时,编译器就会到这几个目录中寻找目标。
3、确定依赖关系
文件之间往往不是独立的,而是相互依赖的。比如A文件的内容要依赖于B文件的内容(例如A文件内写有#include “B.h”)。这时候编译器就要确定编译这两个文件的先后顺序。在这种A依赖于B的情况下,编译器就要确认做到以下两点:
(1)只有在B编译完成之后,才开始编译A;
(2)每当B变化时,便重新编译A。
这种编译顺序记录在一个叫做makefile的文件里,而这个文件是在运行上文提到的configure脚本时生成的。因此在编译时,必须首先运行configure脚本文件。
4、预编译(precompilation)
我们写程序时经常会有多个文件都引用了同一个文件的情况。在这种情况下,编译器为了防止这个被引用的文件被编译多次,就会在编译源文件之前,先将全部头文件编译完。这个过程就叫做编译器的预编译。
5、预处理(preprocessing)
在这一步中,编译器会将头文件和宏替换为其具体指代的内容,还会移除代码中的注释。例如在如下代码中:
A.h:
...
int testA(int a, string b);
B.c:
#include “A.h"
int main()
{
testA(3, “test”); //aaaaaaaaaa
}
则经过预处理后,B文件会变成下面的样子:
B.c
int testA(int a, string b);
int main()
{
testA(3, “test”);
}
6、编译(Compilation)
到了这一步,编译器就会将预处理后的源码翻译成机器码。某些编译器会先将源码翻译成汇编语言,然后再翻译成机器码。转码后的文件称为对象文件。
7、链接(Linking)
这里说的链接是指静态链接,以区别于在运行时发生的动态链接(和编译器无关)。这一步的作用是将需要的外部函数的代码(通常是后缀名为.lib和.a的文件)添加到可执行文件中。
8、安装(Installation)
上一步的链接是在内存中生成了可执行文件,这一步的安装就是将可执行文件保存到用户事先规定好的目录中的过程。
9、操作系统链接
这一步的目的是通知操作系统,刚刚安装好的可执行文件已经可以被使用了。编译器通知操作系统的方式便是在文件中保存一下这个程序的元数据,例如文件名、文件描述、关联后缀名等等。例如在Linux系统下,这些信息通常保存在/usr/share/applications目录下的.desktop文件中。
10、生成安装包
到了这一步,编译器的任务就是将可执行文件和相关的数据文件等以某种数据打包好,然后交给用户。
至此,编译器的任务就告一段落了。不过上面提到了静态链接,那么这里我还想再提一下与之相对应的动态链接。
静态链接的特点是在运行前就将所有需要的外部函数库都链接到可执行文件内,这样做的好处是不管你将这个程序移植到哪里运行,都不用担心缺少某个必要的库文件;但是缺点也很明显,就是安装包的体积势必会比较大。而动态链接则是在运行时引用外部库文件,而不让库文件进入安装包。动态链接的优点就是安装包会很小,但缺点就是使用这个程序的机器上必须要安装好所需的库文件。
By Caesar
@hongruiqi 谢谢琦哥的补充~
因为静态链接的种种限制,所以现在很多软件都采取了动态链接的形式,在运行时才进行库文件的装载,它们在Linux系统下以.so为后缀。比如说PHP的一些扩展(memcached、curl等),就是这样被安装起来的...对于解释型脚本语言都可以看作是可执行程序。这样做最大的好处我觉得是解耦化,将一些不常用的组件与主程序剥离开来,只留下最核心的东西,剩下的可以依据用户的需要另行加载。所以对于大型的项目,模块化的**是十分有必要的,以上~
动态库的两个优点:
- 可以被多个程序复用。动态库的代码出bug时,只需重新编译和更新动态库本身,使用动态库的多个程序无需重编。当然,动态库如果出现严重的问题,影响的程序也更广。有利有弊吧。
- 实现插件机制,就是楼上所说的PHP扩展等。