读书笔记--《程序员的自我修养》第2章:编译和链接

一、从源码到可执行文件的过程

分为4个步骤:预处理(prepressing)、编译(compilation)、汇编(assembly)和链接(linking)。如图所示
这里写图片描述

1、预编译
(1)首先,源代码文件和相关的头文件,会被预编译器预编译为一个.i文件。
对于C++程序来说,它的源代码文件的扩展名可能是.cpp或.cxx,头文件的扩展名可能是.hpp,而预编译后的文件扩展名是.ii。
预编译过程相当于如下命令: g c c E h e l l o . c o h e l l o . i cpp hello.c > hello.i
(2)预编译过程主要处理那些源代码中以“#”开始的预编译指令。比如“#include”,”#define”等。主要规则如下:
这里写图片描述
预编译后的.i文件不包含任何宏定义,因为所有的宏都已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们不确定宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

(2)编译
编译就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。核心部分。编译过程相当于如下命令: g c c S h e l l o . i o h e l l o . s gcc -S hello.c -o hello.s 都可以得到汇编输出文件hello.s。
对于C语言代码来说,预编译和编译的程序是cc1;对于C++来说,是cc1plus;Objective-C是cc1obj; fortran是f771;Java是jc1。所以实际上gcc这个命令只是这些后台程序的包装,它会根据不同的参数要求来调用预编译编译程序cc1、汇编器as、连接器ld。

(3)汇编
汇编器是将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。此汇编过程比较简单,只需根据汇编指令和机器指令的对照表一一翻译就可以了。可以调用汇编器as来完成: a s h e l l o . s o h e l l o . o gcc -c hello.s -o hello.o 或 $gcc -c hello.c -o hello.o

(4)链接

二、编译

编译过程一般分为6个过程:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。过程如图所示:
这里写图片描述
以一段C代码为例讲述这个过程:
array[index] = (index+4)*(2+6)
CompilerExpression.c

1、词法分析
首先源代码被输入扫描器,扫描器对其进行词法分析。运用一种类似于有限状态机的算法可以很轻松地将源代码地字符序列分割成一系列的记号(Token)。
这里写图片描述
这里写图片描述
词法分析产生的记号一般可以分为:关键字、标识符、字面量(包括数字、字符串等)和特殊符号(如加号等号)。在识别记号的同时,扫描器也完成了其他工作,如将标识符存放到符号表,将数字、字符串常量存放到文字表等。

2、语法分析
接下来语法分析器对扫描器产生的记号进行语法分析,从而产生语法树。整个分析过程采用了上下文无法语法的分析手段。由语法分析器生成的语法树就是以表达式为节点的树。上述代码产生的语法树如图所示:
这里写图片描述

3、语义分析
接下来进行语义分析。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否有意义。编译器所能分析的语义分为静态语义和动态语义。
经过语义分析阶段以后,整个语法树的表达式都被标识了类型。如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。上述语法树经过语义分析后变为如图所示形式。
这里写图片描述

4、中间语言生成
现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。我们这里所描述的就是源码级优化。在上例中,(2+6)这个表达式可以被优化掉,因为它的值在编译期就能被确定。经过优化的语法树如图所示:
这里写图片描述
我们可以看到(2+6)这个表达式被优化成8。直接在语法树上作优化比较困难,所以一般是源代码优化器将整个语法树转换成中间代码,它是语法树的顺序表示,其实已经很接近目标代码了。它一般跟目标机器和运行环境无关。中间代码有很多类型,比较常见的有:三地址码和P-代码。
基本的三地址码是这样的:x = y op z ,这里op操作可以是加减乘除等运算。
上例中的代码三地址码表示为:
t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array[index] = t3

这里利用了三个临时变量t1、t2和t3。在三地址码的基础上进行优化时,优化程序会将
2+6的值直接计算出来,得到t1=6,然后将后面代码中的t1替换成数字6。还可以省去一个临时变量t3,因为t2可以重复利用,经过优化的代码如下所示:
t2 = index + 4
t2 = t2 * 8
array[index] = t2
中间代码使得编译器可以分为前端和后端。前端负责产生机器无关的中间代码,后端将中间代码转换成目标机器代码。

5、目标代码生成与优化
编辑器后端包括代码生成器和目标代码优化器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数据类型等。上例的中间代码如下:
这里写图片描述
最后目标代码优化器对上述代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。上例中,乘法由一条相对复杂的基址比例变址寻址的lea指令完成,随后由一条mov指令完成最后的赋值操作,这条mov指令的寻址方式与lea是一样的。
这里写图片描述
经过扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化等步骤后,源代码变成了目标代码,打还是index和array的地址还没有确定。

三、链接

1、重新计算各个目标的地址的过程叫做重定位

2、汇编语言使用符号来标记位置。符号用来表示一个地址,这个地址可能是一段子程序的起始地址,也可以是一个变量的起始地址。

3、人们将代码按照功能或性质划分,分别形成不同的功能模块,不同的模块之间按层次结构或其他结构来组织。在c语言中,最小的单位是变量和函数,若干个变量和函数组成一个模块,存放在一个.c的源代码文件里。然后这些源代码文件按照目录结构来组织。在Java中,每个类是一个基本模块,若干个类模块组成一个包,若干个包组合成一个程序。

4、最常见的C/C++模块之间通信有两种方式,一种是模块间的函数调用,另一种是模块间的变量访问。函数访问和变量访问都需要知道函数和变量的地址,因此两种方式可归为一种方式,那就是模块间符号的引用。

5、模块间依靠符号来通信类似于拼图,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合。模块间的拼接过程就是链接。

四、模块拼接–静态链接

链接就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。
链接过程主要包括地址和空间分配、符号决议和重定位
如果我们在程序模块main.c中使用另一个模块func.c中的函数foo(),则我们必须知道foo的地址。但是由于每个模块都是单独编译的,因此在编译main.c时并不知道foo的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等最后链接的时候由连接器将这些指令的目标地址修正。这个地址修正的过程称为重定位,每个要被修正的地方叫一个重定位入口。重定位所做的就是给程序中每个这样的绝对地址引用的位置打补丁,使它指向正确的地址。

猜你喜欢

转载自blog.csdn.net/qq_15727809/article/details/82620840