美文网首页简书收藏--编译原理
修养 第二章 编译 和 链接

修养 第二章 编译 和 链接

作者: RX78178 | 来源:发表于2016-12-12 17:35 被阅读106次

    所有这一张中的示例代码,以 Hello World 和 hello.c 为基准
    2.1 被隐藏了的过程
    GCC 编译过程分析 —— 预处理,编译,汇编,链接:


    原书图

    2.1.1 预编译 —— 处理 hello.c 文件 输出 hello.i 文件
    首先是源代码文件 hello.c 和 相关文件的头文件(譬如 stdio.h)等,先经由预编译器预编译,cpp 预编译成一个 .i 文件。
    预编译过程主要处理那些源代码中的以“#”开始的预编译指令,譬如“#include”,“#define”等,处理规则如下:
    1、将所有的“#define”删除,并且展开所有的宏定义。(将宏定义替换成实际代码)
    2、处理所有条件预编译指令,譬如“#if”,“#ifdef”,“#elseif”等等。
    3、处理“#include”预编译指令,将包含的晚间插入到预编译指令的位置。这个过程是递归进行的,所以,当 include 的头文件中包含了别的头文件,那么那些额外的头文件,会递归的被插入。这样也导致了最终的代码比编写的时候要多。
    4、删除所有的注释
    5、添加行号和文件名标示。譬如“#2”,“hello.c”,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时能够显示行号。
    6、保留所有的 #program 编译器指令,因为编译器需要使用它们。
    通过查看预编译后的文件,可以确定宏定义是否是正确的,或者头文件是否包含正确。

    2.1.2 编译 —— 处理 .i 文件,输出 .s 文件
    编译的整个过程就是把预编译完的进行一系列处理产生相应的汇编代码文件。
    编译的步骤:扫描源文件 -> 语法分析 -> 语义分析 -> 源代码优化 -> 代码生成 -> 目标代码优化

    2.1.3 汇编 —— 处理 .s 文件,输出 .o 文件
    汇编过程,是由汇编器将汇编代码转变成机器可以执行的指令,每一个汇编语句都会对应一条机器指令。汇编器做的事儿,是比较简单的,就是做翻译,把编程语言翻译成汇编语句,不考虑语法,语义,也不做指令优化(编译过程已经处理了),最终生成 .o 文件,即目标文件

    2.1.4 链接 —— 将 多个 .o 文件,库文件 链接在一起
    链接器做的事儿,就好比是一个组装车间。编译器、汇编器把各个 .c 文件分头处理,做成目标文件;而链接器做的是将这些目标文件组装在一起,让原本互不相识的目标文件认清彼此(有时候,a.c 会 用到 b.c中的方法或是变量,链接过程正是为了确定所有变量和方法的地址而存在的)。
    由于编译器是不知道变量和方法的地址的,编译器处理的是模块,至于这个模块要放在哪里是不知道的。
    链接器做的就是将这些模块组合在一起,明确各个变量 和 方法 的地址。

    2.2 编译器做了些什么 —— 将高级的编程语言翻译成机器语言
    编译步骤 :扫描 -> 语法分析 -> 语义分析 -> 源代码优化 -> 代码生成 -> 目标代码优化


    原书图

    示例代码 :
    array [index] = ( index + 4 ) * ( 2 + 6 ) ,文件名:CompilerExpression.c

    2.2.1 词法分析:
    源代码首先被输入到扫描器(Scanner),完成词法分析。词法分析,使用的是一种类似于 “有限状态机”的算法,将源代码的字符串分割成一系列的记号(Token):


    原书图

    词法分析产生的记号,由扫描器存放到对应的表中,譬如,将标识存放到符号表,将数字存放到文字表等。

    2.2.2 语法分析
    语法分析器,对由扫描器产生的记号进行语法分析,从而产生语法树。采用的是上下文无关语法的分析手段。产生的语法树:


    原书图

    可见,整个表达式被视作复制表达式。符号和数字是最小的表达式,是整个语法树的节点。
    在语法分析的同时,运算符号的优先级 和 含义也被确定下来了。如上图所示,乘法运算的优先级 高于 加法。
    有些符号拥有多重含义,譬如 * ,即可以表示乘法,有可能表示 对指针取内容。
    特别注意:如果出现表达式不合法,比如各种括号不匹配,表达式中缺少操作符等,编译器就会报告语法分析阶段的错误。

    2.2.3 语义分析
    语义分析器,完成语义分析。语法分析,仅仅是完成了表达式语法层面的分析,但是它并不了解这个语句是否真正有意义。
    譬如C语言中,两个指针做乘法运算,在语义上是合法的,但是确实没有意义的(指针取值之后做乘法是有意义的,但是指针指向的是地址,直接做乘法是无意义的)
    编译器所能分析的语义是静态语义,对于动态语义编译器是无能为力的。譬如在运行时,从服务器获得一个数据,和服务器的约定是服务器传一个字符串,receiver进行强转,转成int型。然而,在运行时如果服务器传的是一个数组,那么强转必然失败,还会导致程序崩溃。
    静态语义 —— 在编译期间可以确定的语义。通常包括声明和类型的匹配。
    动态语义 —— 只有在运行期间才能确定的语义

    经过语义分析阶段后,整个语法树的表达式都被标示了类型。如果有些类型需要做隐式转换,语义分析程序会在语法书中插入相应的转换节点。标示于以后的语法树:


    原书图

    2.2.4 中间语言生成 —— 源码级别的优化过程
    源代码优化器 —— 会在源代码级别进行优化。上述示例代码中,(2 + 6)这个表达式可以被优化掉,因为它的值在编译期就可以被确定。优化后的语法树:


    原书图

    由于直接在语法树上做优化比较困难。所以,源代码优化器,往往将整个语法树转换成中间代码。
    中间代码 —— 语法树中元素的顺序表示,他已经非常接近目标代码了。但是其一般跟目标机器和运行时环境是无关的。

    三地址码:x = y op z 。这个三地址码表示将变量 y 和 z 进行 op 处理后赋值给x。将上述的语法树翻译成三地址码后的结果:
    t1 = 2 + 6
    t2 = index + 4
    t3 = t2 * t1
    array[index] = t3
    可以看到,使用了三地址码后,整个代码全部变成了 由 3 个变量地址 和 一个 运算符 构成的 算式集合。
    每一个算式 都很简单,是将原先的复杂算式做了简化,也是对 语法树的顺序翻译。
    优化程序在三地址码基础上进行优化时,会将 2 + 6 的结果计算出来,得到 t1 = 8。同时,还可以省去一个临时变量 t3,因为 t2 可以重复利用。
    优化结果:
    t2 = index + 4
    t2 = t2 * 8
    array[index] = t2

    相当重要的一点是 —— 中间代码使得编译器可以被分为前端和后端。前端负责产生同机器无关的中间代码,后端将中间代码转换成目标机器代码。如此,在跨平台时,编译器可以使用同一个前端,搭配不同的后端。这样一来,可以大大简化跨平台编译的工作量。

    2.2.5 目标代码生成与优化
    编译器后端主要包括代码生成器 和 目标代码优化器。
    代码生成器 —— 将中间代码 转换成 机器代码。这个过程十分依赖于目标机器,因为各种机器的特性不同。
    示例代码的机器码 :(使用 x86 的汇编语言,假设 index 的类型为 int ,array 的类型为 int 型数组)


    原书图

    最后目标代码优化器对于上述的目标代码进行优化 —— 譬如选择合适的寻址方式,使用位移来代替乘法运算,删除多余的指令等。
    到目前为止,目标代码算是完成了,只剩下一个问题 —— array 和 index 的地址如何确定。事实上,定义其他模块的全局变量 和 函数在最终运行时的绝对地址都要在最终连接的时候才能确定。现代编译器可以将一个源代码文件,编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来,形成可执行文件。

    2.3 链接器年龄比编译器长
    重定位 —— 在最早期的计算机中,重新计算各个目标的地址(重新计算各个目标文件中的变量和方法的地址)过程被称为重定位

    链接的实质 —— 通过符号,使不同模块间得以通信。之所以是符号,原因是计算机的发展上,最一开始使用的是纸带,上面直接写机器代码。而后有了汇编,汇编语言使用符号表示 变量 和 函数,以及 所做的操作。符号一词因运而生。
    编译器将各个模块编译,而连接器将各个编译完了的模块链接在一起。

    2.4 模块拼装 —— 静态链接
    链接(Linking)就是组装模块的过程。链接的主要工作,就是讲各个模块间相互引用的部分,都处理好,使得各个模块之间能够正确的衔接。
    链接器所做的事儿,从原理上来说,就是把一些指令对其他符号地址的引用加以修正。因为,每当在程序中修改了代码,会导致部分代码中,增加,减少,或者是调换了变量,函数的位置。结果是,在汇编器中,变量 和 函数的符号(Symbol)不变,但是位置变了,也就是符号不变,地址变了。所以,就要重新计算新的地址。
    在没有链接器的时代,这个重新推算地址的过程就是重定位,是人工完成的。链接器做的,就是代替人工,根据符号,将机器代码重定位。

    链接过程主要包括 —— 地址和空间分配,符号决议,重定位
    最基本的静态链接过程 :从 .c 到 .o(目标文件) 是编译器做的,最后一步是链接器做的链接过程。


    原书图

    在链接时,目标文件 和 库 一起链接,形成最终的可执行文件。库其实是一组目标文件的包,是一些最常用的代码从编译成目标文件后打包存放。
    Object 文件,中文译为 中间目标文件比较合适,简称 目标文件,很多时候也称之为模块。

    了解链接器的执行过程:
    假设,我们有个全局变量叫做 var ,它在目标文件 A 里面。我们在目标文件 B 里面要访问这个全局变量,比如我们在目标文件 B 里面有这么一条指令:


    原书图

    这条指令就是给这个 var 变量赋值 0x2a,相当于 C 语言里面的语句 var = 42。然后我们汇编目标文件 B(将汇编码 转换成 机器码),得到这条指令的机器码:


    原书图
    由于在汇编目标文件 B 的时候,汇编器并不知道变量 var 的目标地址,所以汇编器再没法确定地址的情况下,将这个条 mov 指令的目标地址置为 0,等待链接器将目标文件 A 和 B 连接起来的时候再将其修正。
    假设 A 和 B 连接后,变量 var 的地址确定下来为 0x1000,那么链接器将会把这个指令的目标地址部分,修改成 0x1000。
    这个地址修正的过程也被叫做 “重定位”, 每个被修正的地方叫做一个 “重定位入口”。

    重定位所做的,就是给程序中每个这样的绝对地址引用的位置 “打补丁”,使他们指向正确的地址。

    相关文章

      网友评论

        本文标题:修养 第二章 编译 和 链接

        本文链接:https://www.haomeiwen.com/subject/qavmmttx.html