欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

二探·编译与连接

发布时间:2025/3/15 编程问答 33 豆豆
生活随笔 收集整理的这篇文章主要介绍了 二探·编译与连接 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

直观上来说,编译器就是将高级语言翻译成机器语言的一种工具。因为使用机器指令或汇编语言编写程序是一件十分费事,并且相当乏味的一件事。这使得程序开发效率变得相当低下。此外,使用机器语言或汇编语言编写的程序十分依赖特定的机器,一个为某种CPU编写的程序在另外一种CPU下完全无法运行。所以研究人员期盼这样一种语言,我们可以采用类似于自然语言的语言来描述一个程序。高级语言由此诞生,它能使得程序员更加关注程序逻辑的本身,而尽量少考虑计算机本身的限制,这些交给编译器做就好了。

1.编译器做了什么?

编译过程一般可以分为六个步骤:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。整个过程如图所示:
以下面一段C语言代码为例,来详细分析这个过程: array[ index ] = ( index + 4 ) * ( 2 + 6 );扫描与词法分析
首先,源代码程序被输入到扫描器,扫描器的任务相当的简单,他只是进行简单的词法分析,运用一种类似于有限状态机(Finite State Mchine)的算法可以很轻松的将源代码的字符序列分割成一系列的记号。比如,上面那个程序包含28个非空字符,经过扫描以后,生成16个记号,如下表所示:
记号类型
array标识符
[左方括号
index标识符
]右方括号
=赋值
(左圆括号
index标识符
+加号
4数字
)右圆括号
*乘号
(左圆括号
2数字
+加号
6数字
右圆括号
词法分析产生的记号一般可以分为如下几类:关键字、标识符、字面量(包括数字、字符串等)和特殊符号(如加号、等号)。在识别记号的同时,扫描器也完成其他工作,比如将标识符存放到符号表,将数字、字符串常量存放到文字表等。 语法分析 语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。整个分析过程采用上下文无关语法的分析手段。简单地说,由语法分析器生成的语法树就是以表达式为节点的树。在C语言中,一个语句是一个表达式,而复杂的语句是很多表达式的组合。上面那个例子就是一个由赋值表达式、加法表达式、乘法表达式、数组表达式、括号表达式组成的复杂语句。改程序进过语法分析器以后会形成如下所示的语法树:

从上图我们可以看出,整个语句被看作是一个赋值表达式:赋值表达式坐标是一个数组表达式,他的右边是一个乘法表达式。符号和数字是最小的表达式,他们不是由其他的表达式来组成的,所以他们通常作为整个语法树的叶节点。对于不同的编程语言,编译器开发者只需要改变语法规则,而无需为每一个编译器编写一个语法分析器,所以它也被称为“编译器编译器”。 语义分析 语义分析仅仅是完成对表达式的语法层面的分析,但是他并不是了解这个语句是否整整有意义。编译器所能分析的语义是静态语义,所谓的静态语义是指:在编译期可以确定的语义;与之对应的是动态语义,就是只有在运行期才能确定的语义。 静态语义通常包括声明和类型的匹配、类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程,语义分析过程中,需要完成这个步骤。再例如,将一个浮点型赋值给一个指针的时候,语义分析程序会发现这个类型不匹配,编译器将会报错。动态语义一般值在运行期间出现的语义相关问题,比如将0作为除数是一个运行期间语义错误。 经过语义分析阶段后,整个语法树的表达式都被标记了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。具体如下所示:

从上图可以看到,每个表达式(包括符号和数字)都被标示了类型。 中间语言生成 现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。源代码级优化器会在源代码级别进行优化,对于上面的程序,如果细心我们会发现,(2+6)这个表达式可能被优化掉,因为他的值在编译期间就可以被确定。经过优化的语法树如下图所示:

虽然这看似是一个理所当然的操作,其实直接在语法树上做优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码,他是语法树的顺序表示,其实这已经非常接近目标代码了。但是他一般跟目标机器和运行时的环境是无关的,比如它不包含数据的尺寸、变量地址和寄存器的名字等。中间代码有很多种类型,在不同的编译器中有着不同的形式,比较常见的有:三地址码;P-代码。 中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换为目标机器代码。这样对于一些可以跨平台的编译器而言,他们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。 目标代码生成与优化 源代码级优化器产生中间代码标志着下面的过程都属于编译器后端。编译器后端主要包括代码生成器和目标代码优化器。 代码生成器将中间代码生成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。对于上面的代码,代码生成器可能会生成下面的代码序列: movl index, %ecx ;value of index to ecx addl $4, %ecx ;ecx=ecx+4 mull $8, %ecx ;ecx=ecx+8 movl index, %eax ;value of index to eax movl %ecx, array( , eax,4) ;array[ index ] = ecx最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式;使用位移来代替乘法运算、删除多余的指令。在上面的例子中,乘法是由一条相对复杂的基址比例变址寻址的lea来完成。

2.总结

现代的编译器有着异常复杂的结构,这是因为现代高级编程语言本身非常地复杂,比如C++语言的定义就极为复杂,至今没有一个编译器能够完整支持C++语言标准所规定的所有语言特性。另外现代的计算机CPU相当地复杂,CPU本身采用了诸如流水线、多发射、超标量等诸多复杂特性,为了支持这些特性,编译器的机器指令优化过程也变得十分复杂。使得编译过程更为复杂的是有些编译器支持多种硬件平台,即允许编译器编译出多种目标CPU的代码。比如著名的GCC编译器就支持几乎所有CPU平台,这也导致编译器的指令生成过程更为复杂。 经过扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化,编译器忙活了这么多个步骤之后,源代码终于被编译成了目标代码。但是这个目标代码中有一个问题:index和array的地址还没有确定。如果我们要把目标代码使用汇编器会变成真正的能够在机器上执行的指令,那么index和array的地址应该从哪里得到呢?如果index和array定义在跟上面的源代码同一个编译单元里面,那么编译器可以为index和array分配空间,确定他们的地址:那么我们又该如何定义在其他的程序模块呢? 这其中会有很多问题值得讨论,比如目标代码代码中有变量定义在其他模块中怎么办?事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以现代的编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。

总结

以上是生活随笔为你收集整理的二探·编译与连接的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。