编译流程:
预处理阶段
→
编译阶段
→
汇编
阶段
→
链接阶段
预编译阶段:
对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,主要是处理源代码文件中以"#" 开头的预编译指令,生成预编译文件。规则如下:
- 删除所有的#define,展开所有的宏定义。
- 处理所有的条件预编译指令,如" #if "、" #endif "、" #ifdef "、" #elif "和" #else "。
- 处理" #include "预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
- 删除所有的注释,"//"和"/**/"。
- 保留所有的"#pragma"编译器指令,编译器需要用到他们,如:"#pragma once"是为了防止有文件被重复引用。
- 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告时能够显示行号。
编译阶段:
将经过预处理后的预编译文件转换成特定的汇编代码,生成汇编文件。即将 .i 或者 .ii 文件进行一系列的词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
- 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
- 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
- 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
- 优化:源代码级别的一个优化过程。
- 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
- 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
汇编阶段:
汇编过程实际上指 把汇编语言代码翻译成目标机器指令的过程 。对于被翻译系统处理的每一个 C 语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
- 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
- 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
UNIX 环境下主要有三种类型的目标文件:1. 可重定位文件其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。2. 共享的目标文件这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。3. 可执行文件它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。
链接阶段:
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
- 静态链接
在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。 空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本; 更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。特点: 浪费空间,运行速度快 。运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
- 动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本;更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。特点: 速度慢,节省空间,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。异同:静态链接库 和 动态链接库 都是共享代码。如果采用静态链链接库( .lib ), lib中的指令最终都会编译到链接该静态库的exe (或 dll )文件中,发布软件时,只需要发布 exe (或 dll )文件,不需要.lib 文件。但是若使用动态链接库( . dll ), dll中的指令不会编译到exe文件 中,而是在exe 文件执行期间,动态的加载和卸载独立的 dll 文件,需要和exe文件一起发布。静态链接库和动态链接库另一个区别是静态链接库不能再包含其他动态链接库或静态链接库,而动态链接库不受此限制,动态链接库中可以再包含其他的动态链接库和静态链接库。
图解静态链接和动态链接差异
已从图上得知:静态链接中,多次使用会导致内存中存在多份冗余拷贝,所以静态库特点为:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
为什么需要动态库?
除了空间浪费还有一个问题:
静态库对程序的更新、部署和发布页会带来麻烦。如果静态库lib***.lib 更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。而使用动态库, 用户只需要更新动态库即 可, 增量更新 。所以动态库特点为:
- 动态库把对一些库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 将一些程序升级变得简单。
- 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
编译流程框架图
链接的细节补充
动态链接需要的文件名称 静态链接需要的文件名称 win lib***.dll、lib***.lib lib***.lib linux lib***.so lib***.a win环境下:静态库中的 lib :该 LIB 包含函数代码本身(即包括函数的索引,也包括实现),在编译时直接将代码加入程序当中。动态库中的 lib :该 LIB 包含了函数所在的 DLL 文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL 提供。编译器/链接器在编译/链接时需要.lib
文件来确定程序中引用的函数在DLL中的位置,然后在运行时,Windows的Loader使用.dll
文件来实际加载函数代码。linux环境下:编译器/链接器在编译/链接时直接使用
.so
文件,.so
文件既包含了程序运行时需要的实际代码,也包含了链接时需要的符号表。因此,.so
文件同时用于链接时和运行时,Linux的动态链接器用它来加载代码和解析符号。
.lib
和.dll
在Windows中是分离的,因为Windows的设计要求链接时有一个明确的导出符号表。.a
文件在Linux中用于静态链接,并不用于动态链接。Linux的.so
文件集成了Windows中.dll
和.lib
的功能,因此对于动态链接来说,.a
文件并不是必要的。Windows更倾向于明确区分导入库和动态库,而Linux则采用了一种更为统一的方式来处理动态库文件。(操作系统差异导致)
.a(Archive)和.so(Shared Object):
都是由一些目标文件而来