提到“编译原理”,大部分开发者的第一反应就是羞涩难懂。确实,编译原理这部分在计算机学习中是比较难以理解的一部分。
程序编译过程剖析图
由上图我们可以得到:
1、组成一个可执行程序的每一个源文件是通过编译器编译后转成目标文件代码。
2、链接器会把每个目标文件捆绑到一起,与此同时也会引入标准C函数库中任何被该程序用到的函数。
源文件->可执行文件全过程剖析图
由上图我们可以得到:
1、程序运行要经过2个环境:编译环境+运行环境
2、编译环境包括:编译过程(预处理,编译,汇编)、链接
3、运行环境:执行可执行程序代码
编译环境全流程图
编译环境
预处理阶段:去掉注释;#include文件包含;#define符号替换。
编译阶段:语法分析;语义分析,符号汇总;源代码转为汇编代码。
汇编:形成符号表;汇编指令->二进制指令。
链接:合并段表;符号表的合并和符号表的重定位。
运行环境
1、程序必须载入内存中,在有操作系统的环境中,此过程由操作系统完成。独立环境中程序必须手工完成,也可以通过可执行代码植入只读内存来完成。
2、程序执行开始,调用main()函数。
3、开始执行程序代码。此时程序将使用一个运行时堆栈,存储函数的局部变量,函数参数,返回数据和返回地址。同时也可以使用静态内存,存储于内存中的变量在程序的整个运行过程中一直保留。
4、终止程序,正常终止main()函数,也可能会中途意外终止。
说了这么多,感觉还是不够细致,我想大部分同学对编译器的工作过程还不是十分理解,那么我就再把编译器的过程详细讲讲。
源码想要被运行,必须先转成二进制的机器码。这就是编译器的任务。
1、配置
编译器在工作之前,需要知道当前系统环境,比如标准库在哪,软件安装到哪里,需要安装哪些组件等等。这是因为不同计算机的系统环境不一样,通过制定编译参数,编译器就可以灵活适应环境,编译出各个环境都能运行的机器码。这个确定编译参数的步骤就叫配置。
2、确定标准库和头文件位置
源码一定会用到标准库函数和头文件。它们可以存放在系统的任意目录下,编译器实际上没办法自动监测到它们的位置,需要通过第一步的配置文件找到它们。
3、确定依赖关系
对于大型项目来说,源码文件之间往往存在依赖关系,编译器需要确定编译的先后顺序。假定A文件依赖B文件,编译器应该保证做到当B文件完成编译后,才开始编译A文件,当A文件发生变化,A文件会被重新编译。
4、头文件预编译
不同的源码文件可能引用同一个头文件。编译的时候头文件也必须一起编译。为了节约时间,编译器会在编译源码之前,先编译头文件。保证乐头文件只需要编译一次不必每次用到的时候,都重新编译。需要注意的是并不是头文件的所有内容都会被预编译。用来声明宏的#define命令,就不会被预编译。
5、预处理
预编译完成之后,编译器就开始替换源码中的头文件和宏。
6、编译
预处理完成之后,编译器就开始生成机器码。对于某些编译器来说还存在一个中间步骤,会先把源码转成汇编语言,然后再把汇编语言转成机器码。生成目标代码文件object file。
7、链接
目标代码文件还不能运行,必须把它们捆绑到一起,生成可执行文件。这个过程中会从标准库函数中引入用到的函数,添加到可执行文件中。这种通过将外部函数添加到可执行文件的方式,叫做静态链接(static linking)。对应的还有动态链接(dynamic linking)。
8、安装
上一步链接是在内存中进行的,即编译器在内存中生成了可执行文件。现在就需要将可执行文件保存到用户事先指定的安装目录。这个过程看似简单(将可执行文件拷贝到安装目录),实则非常复杂,需要完成创建目录,保存文件,设置权限等等。就不详细展开说明了。总之这个过程就是安装。
9、通知操作系统
可执行文件安装后,必须以某种方式通知操作系统,让其知道可以使用这个程序了。
10、运行安装包
刚才在详细介绍编译过程中,曾提到动态链接(Dynamic linking)。正常情况下,完成以上步骤后程序就可以运行了。至于运行期间发生的事情,与编译器没有半毛钱关系。但是开发者可以在编译期间选择可执行文件链接外部函数库的方式,选择用静态链接方式(编译时链接),还是动态链接方式(运行时链接)。
前面说过,静态链接就是把外部函数库,拷贝到可执行文件中。这样的好处是使用范围比较广,不用担心用户机器缺少某个库文件。缺点也很明显就是安装包会比较大,而且多个程序之间,无法共享库文件。动态链接的好处就是多个应用程序可以共享文件,缺点是用户必须事先安装好库文件,而且版本和安装位置都必须符合要求,否则就无法正常运行。现实中大部分软件都采用动态链接,共享库文件。这种动态共享的库文件,Linux平台的后缀名是.so文件;Windows平台的后缀名为.dll文件;Mac平台的后缀名是.dylib文件。